Skip to content
Engineering Atlas
SystemsMoncton CRM — Settlement Agency Case Management Platform

Moncton CRM — Settlement Agency Case Management Platform

A bilingual case management platform for Canadian settlement agencies, replacing spreadsheets and paper files with one system covering the full newcomer client journey from inquiry to IRCC reporting.

Status
In Progress
Timeline
Mar 2026 – Present
Reading Time
8 min
Domains
Full-Stack Architecture, Access Control, Compliance
Technologies
Next.js, NestJS, PostgreSQL, Prisma, Redis, AWS ECS Fargate
Contents

Executive Summary

Moncton CRM is a secure, bilingual case management platform for Canadian settlement agencies supporting refugees and newcomers. Work has covered full-stack ownership of the product: a Next.js frontend talking to a NestJS/Prisma/PostgreSQL backend exclusively through Server Actions, role- and department-scoped access control for five staff roles (plus agency-defined custom roles), a unified client activity timeline across audit logs, referrals, and intake, and IRCC/iCARE federal reporting. The platform runs on AWS ECS Fargate behind an Application Load Balancer, with Microsoft SSO for staff authentication.

Business Context

Problem
Settlement agencies were managing the newcomer client journey — first contact, intake, casework, referrals to other departments, appointments, and federal IRCC reporting — across spreadsheets, paper files, and disconnected tools. There was no single system tracking a case from open to closure, no structured way to keep a referral from being closed out before the receiving department confirmed it, and IRCC/iCARE reporting data had to be assembled by hand for every submission.
Users
Internal agency staff only — clients do not get a self-service portal in Phase 1. Five built-in roles (Admin, Manager, Lead, Staff, Front-Desk) with hierarchical, department-scoped visibility, plus custom roles agencies can define on top of the built-in set.
Business Goals
Replace scattered tooling with one system spanning inquiry, intake, case management, referrals, appointments, and IRCC reporting. Enforce that every user only sees data appropriate to their role and department. Support agencies operating in both English and French.
Success Metrics
A case's full lifecycle — inquiry through closure — traceable in one system with an audit trail. Referrals can't silently fall through; open referrals block case closure. IRCC/iCARE XML generated from data already captured during normal casework instead of assembled by hand. Role- and department-scoped visibility holds at every screen and endpoint.
Environment
Development environment on AWS ECS Fargate (ca-central-1), NestJS API against PostgreSQL via Prisma, staff authenticating through Microsoft SSO.
Stakeholders
A team lead who owns cloud/infrastructure configuration under normal circumstances; settlement-agency stakeholders whose casework processes the platform digitizes; agency staff across five roles using the system day to day.

Constraints

Government reporting requirements (IRCC/iCARE)

The platform has to capture the specific fields Canada's IRCC newcomer-services program requires and generate compliant XML for federal submission — the data model is shaped by that reporting requirement, not just by what's convenient for casework.

Role- and department-scoped visibility

Front-Desk and Staff see only their own clients, Leads see their team, Managers see their department, Admins see the organization — and agencies can define custom roles on top of the built-in five, so access control couldn't be hardcoded to a fixed role list.

Bilingual by requirement

Agencies operate in English and French. Every core workflow needs a real French equivalent maintained alongside the English one, not a translation layer bolted on afterward.

PIPEDA-sensitive client data

Client records include immigration status, family composition, and other PII protected under Canadian privacy law, which meant audit trails and encrypted storage were requirements from the start, not hardening added later.

System Blueprint

The Next.js frontend reaches the NestJS API only through Server Actions, the API enforces role- and department-scoped access via Guards and permission decorators before any Prisma query runs, and background work like appointment reminders is queued through Redis/BullMQ instead of running inline on the request path.

Frontend

Next.js App Router application covering inquiry, intake, case, referral, and appointment workflows for staff, with a bilingual English/French UI.

Next.jsTypeScript

Server Actions

Mediates every data read and write from the frontend, so there is no data-fetching endpoint directly callable from the browser and auth tokens stay server-side.

Next.js Server Actions

API

NestJS REST API authenticating staff via JWT and Microsoft SSO, enforcing role- and department-scoped authorization through Guards and permission decorators, validating every request through DTO pipelines.

NestJS

Data layer

Relational models for users, roles, organizations, departments, clients, inquiries, intake records, referrals, appointments, documents, and audit logs.

PostgreSQLPrisma

Background jobs

Redis-backed BullMQ queues handle appointment reminders and notifications asynchronously, off the request path.

RedisBullMQ

Deployment

Containerized frontend and backend services on AWS ECS Fargate behind an Application Load Balancer, with configuration injected from AWS Systems Manager Parameter Store.

AWS ECS FargateAWS ALBAWS SSM
  • Frontend → Server Actions → NestJS API (no client-callable data-fetching endpoint)
  • NestJS API → Guards/permission decorators → Prisma/PostgreSQL (role- and department-scoped queries)
  • Case, referral, and intake events → unified client activity timeline
  • Appointment and notification triggers → BullMQ queue → async processing
  • ECS Fargate tasks → ALB → health checks and target groups

Architecture

Server Actions as the only data boundary

Every read and write — inquiry, intake, case, referral, appointment — goes through a Next.js Server Action rather than a REST route the browser calls directly, keeping authentication tokens and session handling server-side across a data model that includes PIPEDA-protected client PII.

Role- and department-scoped authorization at the API

NestJS Guards and permission decorators enforce visibility by role and department before a query runs, so a Front-Desk or Staff request can't return records outside its assigned scope regardless of what the frontend asks for — the same boundary that fetches data is the boundary that scopes it.

Unified client activity timeline

Audit logs, referral events, intake updates, and inquiry changes are combined into one paginated, chronological timeline per client, with actor resolution and organization scoping, instead of staff reconstructing a client's history from separate logs.

Bilingual UI via structured localization

English and French are maintained through structured translation files and reusable localization patterns across core modules, so a new feature ships in both languages rather than treating French as a follow-up pass.

Deployment topology

Frontend and backend run as separate containerized services on AWS ECS Fargate behind an Application Load Balancer, with environment configuration injected via AWS Systems Manager Parameter Store at task startup.

Engineering Decisions

Server Actions vs. a client-facing REST API

Problem
A conventional REST layer would make every case, client, and intake endpoint directly reachable from the browser, independent of whatever role checks the UI performed — a real risk given the client data involved is PIPEDA-protected PII.
Options
  • Expose a REST API the frontend calls directly.
  • Route all data access through Next.js Server Actions, invoked only from the application's own server-rendered flows.
Decision
Moved data access behind Server Actions.
Trade-offs
Couples data access more tightly to Next.js, making it harder to expose the same API to a future non-Next.js client (e.g. a mobile app) without rework.
Outcome
No client data endpoint is directly callable outside the application's own server-rendered flow.

Inline role checks vs. centralized scoping at the API boundary

Problem
With five built-in roles plus agency-defined custom roles, and visibility scoped by both role and department, a check duplicated across every controller risked being forgotten on a new endpoint.
Options
  • Check role/department scope inline in each controller or service method.
  • Centralize role- and department-scoping in NestJS Guards and permission decorators applied at the route level.
Decision
Centralized scoping in Guards and decorators.
Trade-offs
Adds a layer of indirection a new engineer has to learn before adding a route, instead of an inline check that's locally visible in the method.
Outcome
Every new endpoint inherits the same scoping model by default instead of re-implementing it.

Synchronous vs. queued appointment reminders

Problem
Sending appointment reminder emails and notifications synchronously inside the request/response cycle would block API responses on external delivery, with no natural retry path when delivery failed.
Options
  • Send reminders and notifications synchronously within the handling request.
  • Queue them through Redis-backed BullMQ and process asynchronously.
Decision
Moved reminders and notifications to BullMQ queues.
Trade-offs
Adds Redis as an operational dependency and requires monitoring queue health instead of trusting synchronous success or failure.
Outcome
Reminder and notification delivery no longer blocks API requests, and failed jobs retry independently of the request that triggered them.

Production Stories

Standing up the dev environment the night before a client demo

2026-05-21
Problem
The dev-environment deployment on ECS Fargate was unreachable the night before the first client demo, with the team lead who normally owns cloud configuration unavailable at that hour.
Investigation
Fixing one symptom kept surfacing the next: a corrected PORT environment variable exposed an ALB target group still pointed at the old port, which exposed a health check path returning a redirect, which exposed a container health check command still hardcoded to the old port, which exposed a security group never opened for the new port — six independent misconfigurations in total, each hidden behind the one before it.
Root Cause
Changing the frontend's listening port from 4000 to 3000 touched five separate places in the ECS/ALB configuration that each needed the same update independently — task definition, target group, ELB health check, container health check command, and security group — plus one unrelated issue where the frontend's API_URL pointed at a subdomain that didn't resolve.
Resolution
Traced and corrected each layer in turn — task definition PORT, target group port registration, health check path and success codes, container health check command, security group inbound rule, and the API_URL SSM parameter — verifying end to end after each fix rather than assuming one correction covered the rest.
Reflection
None of the six fixes were individually hard; the difficulty was that each one looked like the whole problem until the next symptom appeared. Documented the full trace and shared it with the team the next day so the same chain wouldn't need to be re-diagnosed from scratch.

Lessons Learned

  1. Centralizing role- and department-scoping at the API boundary, instead of inline per controller, means new endpoints inherit correct access control by default.

  2. A single config change (a listening port) can require updates in several independent places — task definition, target group, health checks, security group — and each one left stale looks like a separate bug.

  3. Writing up a debugging trace as documentation the team can reuse is worth more than the fix itself; the fix only helps once, the documentation helps every time the pattern recurs.



Command Palette

Search for a command to run...