Architecture & application buildout

How SmarCoders is built, layer by layer.

A containerized monorepo: a NestJS 11 API + worker, a Next.js 15 frontend, PostgreSQL 17 and Redis 7 behind an nginx TLS proxy. All clinical data is Synthea-synthetic, yet the platform enforces HIPAA-grade access control, append-only audit logging, anti-enumeration auth and rate limiting throughout.

Archetype · SaaS Protocol · HTTPS only Security baseline · HIPAA Build depth · comprehensive 14 architectural invariants v1 · ICD-10-CM
01

System overview

A local-first Docker stack (cloud-portable) that moves every chart from intake to audit in one workflow.

The workflow

Coders code synthetic charts (ICD-10-CM diagnoses + procedures). The system validates against format / sequencing / comorbidity rules. Supervisors audit and return structured feedback. Managers watch a live queue/metrics dashboard.

Three roles

Default-deny RBAC. Coders see assigned charts and code them; supervisors audit and give feedback; managers import, assign, and watch metrics. Tenant scope is always applied on top of role.

Synthetic, yet HIPAA-grade

All patient data is generated by Synthea — no real PHI. The platform still enforces access control, audit logging, anti-enumeration auth, rate limiting and at-rest encryption posture for PHI-equivalent handling.

02

Component map

Request path top to bottom: browser → TLS proxy → API / SSR → data & jobs.

Client
Browser Next.js 15 · React 19 TanStack Query v5 SSE live dashboard
HTTPS · REST + SSE · httpOnly cookie (JWT)
Edge
nginx 1.27 TLS · HSTS · HTTP→HTTPS · header buffers
/api/*  ·  / (static / SSR)
Application
NestJS 11 API auth · users · charts · coding · validation · audit · queue · metrics · imports · webhooks · audit-log · notifications · health Next.js server SSR
enqueue jobs  ·  pg (parameterized, scoped)
Data & async
PostgreSQL 17 encrypted volume · JSONB charts Redis 7 BullMQ · SSE cache Worker import · outbox-relay · email · metrics
placeholder-gated · degrade gracefully
External (gated)
Resend email Datadog APM Sentry errors AWS Secrets Manager
03

Layers & data flow

Controllers stay thin; services hold logic; only the server layer touches Postgres.

LayerLocationResponsibility
Presentationapps/webServer Components for first paint of queue/metrics; Client Components + TanStack Query for live data; SSE subscription for the dashboard (<1s). All design values come from packages/tokens.
APIapps/api/src/modulesNestJS controllers → services. Cross-cutting via guards/interceptors: AuthGuard (JWT cookie), RolesGuard (RBAC), RateLimitGuard, AuditInterceptor, SentryGlobalFilter.
Data accessapps/api/src/serverThe only layer permitted to import the pg pool. Every scoped query goes through withOrgScope(orgId, fn) / withUserScope(userId, fn) (INV-1).
Persistencedb/migrationsPostgreSQL 17. Forward-only numbered SQL migrations. JSONB for stored FHIR chart payloads.
Async / jobsapps/api/src/jobsBullMQ on Redis. Processors: import (Synthea bundle → charts), outbox-relay (publish events), email (drain pending), metrics (recompute rollups).

Coding-loop request path

A coder opens an assigned chart → enters codes with rationale → validation rules run (format / sequencing / comorbidity) and map errors back to the offending row → coder submits → chart enters the supervisor audit queue → supervisor scores, leaves findings + training feedback, and approves or returns. Every state-changing action writes an append-only, hash-chained audit_log row.

04

Stack & dependencies

Pinned versions; external SaaS is placeholder-gated and degrades gracefully when keys are absent.

DependencyVersionRole
Node.js22 LTSRuntime
Next.js / React15.x / 19.xFrontend (App Router) + UI
@tanstack/react-query5.xData fetching / cache
Tailwind CSS3.4.xStyling (token-bound)
NestJS11.xBackend framework
@nestjs/bullmq + bullmq11.x / 5.xBackground jobs
pg8.xPostgres driver (raw, parameterized)
ioredis5.xRedis client
zod3.xValidation (shared schemas)
argon20.41.xPassword hashing (native; never substitute — INV-14)
jose5.xJWT sign/verify
pino9.xStructured JSON logging
PostgreSQL / Redis / Nginx17 / 7 / 1.27Database / queue+cache / TLS proxy
Playwright1.xE2E — one spec per workflow
resend · dd-trace · @sentry/* · aws-sdkgatedEmail / APM / errors / secrets — placeholder-gated
05

Reliability patterns

Dual-writes, inbound webhooks and email all avoid the "wrote one side, lost the other" failure mode.

Transactional outbox INV-5

  1. A domain mutation and its outbox_events row are written in one DB transaction.
  2. The outbox-relay worker publishes at-least-once.
  3. Consumers are idempotent, so replays are safe.

Webhook verify-first INV-4 · INV-6

  1. Verify HMAC-SHA256 over the raw body before any DB read.
  2. Dedupe via processed_events UNIQUE(event_id).
  3. Only then dispatch to the service.

Email outbox guaranteed-delivery

  1. Never sync-send inside a request handler.
  2. Insert into pending_emails in the triggering txn.
  3. Drain with capped backoff → DLQ on repeated failure.

Worker topology

No LLM agents at runtime — "orchestration" is the BullMQ worker topology. The API process enqueues jobs and never blocks a request on email/import. The worker process (separate container, same image) runs all processors plus the outbox/email relays on a fixed interval. SSE streams queue counters from the API, backed by a short-TTL Redis cache refreshed by the metrics job.

06

Data model

FKs scoped by org_id throughout. Authoritative DDL lives in db/migrations.

Core tables
organizations
users                 role: manager | supervisor | coder · UNIQUE(lower(email))  [INV-3]
charts                synthetic encounter · JSONB payload · specialty · difficulty · status · assignee
                      status: draft | pending_audit | completed | returned        [ADR-016]
chart_codes           ICD-10 dx/proc · rationale · sequence · is_principal
validation_results    format / sequencing / comorbidity rule outcomes
audits                supervisor review · score · decision
audit_findings        structured findings on an audit
feedback              training feedback to the coder
audit_log             append-only · hash-chained (prev_hash + row_hash)          [INV-12]
outbox_events         transactional outbox for dual-write                        [INV-5]
processed_events      inbound webhook idempotency · UNIQUE(event_id)             [INV-4]
pending_emails        email outbox (drain with backoff → DLQ)
import_jobs           Synthea / FHIR import job tracking
code_reference        ICD-10-CM seed (public-domain descriptors)
07

Key decisions (ADRs)

The choices that shaped the buildout, with the alternative that was rejected and why.

DecisionChoiceAlternativeWhy
Code set, v1ICD-10-CM only (procedures by code)Full CPT descriptorsCPT is AMA license-restricted — bundling descriptors would breach the license ADR-002
DB accessRaw pg + params + scope wrappersPrisma / TypeORMKeeps the tenant-scope boundary mechanically lintable ADR-003
Real-timeSSEWebSocketPush is one-way (server→client); simpler over existing HTTPS ADR-004
Dual-writeTransactional outbox + polling relayDebezium CDCNo extra infra at medium scale ADR-005
Password hashargon2id (native)bcryptjsNative primitive, no substitution ADR-006
SessionshttpOnly cookie JWT (jose), CSRF-guardedHeader bearer tokensAvoids JS-readable token theft ADR-007
DeployLocal Docker stack (cloud-portable)Direct cloudExternals unprovisioned at build time; gated + graceful ADR-008
Monorepo PMnpm workspacespnpmpnpm absent in build env; Node 22 + npm 10 present ADR-012

ADR-016 — fixed read-only coding panel

UI e2e exposed invented statuses (in_progress, unassigned) where the schema uses draft|pending_audit|completed|returned. canEdit never matched a coder's draft → permanent read-only panel. Fixed to isCoder && (draft|returned).

ADR-015 — ambiguous org_id 500

Charts-list query LEFT JOIN users; org_id on both tables made WHERE org_id = $1 ambiguous → Postgres error → 500 on the coder queue. Fixed by aliasing every filter column with c..

08

Architectural invariants

14 rules — 12 machine-checked by scripts/invariant-lint.mjs, 2 boundary-audited at the Drift Detection Gate. Changing a rule means amending §9 + invariants.json + an ADR together; never disabling one to pass a gate.

IDRuleCheck
INV-1No raw pg pool construction/use (getPool(/new Pool(/pool.query() outside apps/api/src/server/**. Modules use server-layer helpers + the txn client from withTransaction.forbidden-pattern
INV-2A design-token file is the single source of color/type/spacing, copied verbatim from DESIGN-TEMPLATE.required-file
INV-3users enforces unique (case-insensitive) email to prevent duplicate-account enumeration.unique-constraint
INV-4Webhook processed_events has UNIQUE(event_id) so replays are idempotent.unique-constraint
INV-5outbox_events table exists (transactional outbox for dual-write).required-file
INV-6In every webhook controller, signature verification precedes any DB read.boundary-order
INV-7Auth rate-limit guard precedes the password-hash compare in the login path.boundary-order
INV-8No hard-coded secrets / placeholder credentials committed in source.forbidden-pattern
INV-9.env.example exists and enumerates every env var.required-file
INV-10UI coverage: every screen route renders, every non-internal endpoint is referenced by UI source, every workflow has an e2e spec to its terminal step.ui-coverage
INV-11Health endpoint exists and reports dependency status.required-file
INV-12Audit log is append-only & hash-chained; no UPDATE/DELETE on audit_log anywhere.forbidden-pattern
INV-13Reverse proxy + Node header-buffer sizing present (nginx large_client_header_buffers / Node max-http-header-size).manual
INV-14argon2 (not bcryptjs/argon2-browser) is the password hash primitive; no substitution.forbidden-pattern
09

Threat model

STRIDE per trust boundary — the mitigation is wired into the architecture, not bolted on.

BoundaryThreatMitigation
AuthSpoofing / repudiationAnti-enumeration (uniform shape + timing), rate-limit fires before hash compare (INV-7), audit-logged
Chart accessElevation / disclosureDefault-deny tenant + role guards; cross-tenant isolation tests
Audit logTamperingAppend-only, hash-chained (prev_hash + row_hash), verified by a job (INV-12)
Webhook ingressSpoofing / replayHMAC verify-first + processed_events idempotency (INV-4, INV-6)
OutboxConsistency / dual-writeSingle-txn write + idempotent consumers (INV-5)
Secrets / at-restDisclosureSecrets Manager / KMS, TLS-only, encrypted Postgres volume

Security Audit Gate (ADR-014)

AUTH-1 (critical, fixed) — registration with an existing email no longer authenticates as the existing account; it returns a shape-identical 201 with no session, preserving anti-enumeration without account takeover. CFG-1 (fixed)assertProdSecrets() blocks production boot on weak/missing JWT_SECRET/WEBHOOK_SIGNING_SECRET. DEP-1 (accepted) — transitive postcss XSS is dev-toolchain only, not runtime-reachable; tracked for the next Next.js bump.

10

Buildout waves

Built as a single orchestrated run across 5 dependency-ordered waves (ADR-011) — one consistent contract layer authored centrally, each feature run as its own iteration.

Wave 0 — Foundation
  • F-01 scaffold, config, health, migrations
  • F-02 tokens, app shell, landing
  • packages/shared + packages/tokens
Wave 1 — Identity & ingress (parallel)
  • F-03 Auth + RBAC
  • F-04 Users + seed
  • F-05 Synthea import
Wave 2 — Core coding loop (seq on W1)
  • F-06 Queue + assign
  • F-07 Chart + coding panel
  • F-08 Validation engine
Wave 3 — Review & observability (parallel on W2)
  • F-09 Audit + feedback
  • F-10 Metrics / SSE / export
  • F-11 Notifications · F-12 Audit-log
Wave 4 — Reliability & handoff (seq)
  • F-13 Outbox + webhooks
  • F-14 invariant-lint + smoke + Claude scaffold
Quality gates
  • lint:invariants must pass
  • npm test validation engine + signatures
  • smoke-test.sh full workflow matrix
  • Playwright e2e per workflow
11

Deployment

One docker compose up brings the whole stack to https://localhost.

ServiceImageRole
dbpostgres:17-alpineDatabase (encrypted volume pgdata)
redisredis:7-alpineQueue + SSE cache
api(build)NestJS REST + SSE
worker(same image)import · outbox · email · metrics
web(build)Next.js SSR
nginxnginx:1.27-alpineTLS termination / reverse proxy
Bring it up
npm install
npm run build -w @smartcoders/shared
bash scripts/gen-certs.sh
docker compose up -d --build
# → https://localhost
bash scripts/smoke-test.sh

Required env: DATABASE_URL, REDIS_URL, JWT_SECRET, SESSION_COOKIE_NAME, APP_URL, WEBHOOK_SIGNING_SECRET. Optional/gated: RESEND_API_KEY, SENTRY_DSN, DD_*, AWS_*. Every var is enumerated in .env.example.

← Gallery