Nexuscai · Engineering blueprint

Soltron CRM — how we build it

The complete backend and system plan for Les Cours Hyperbaric Centre's clinic manager: the architecture, data model, security and Law 25 posture, performance, the AI booking path, testing, hosting, and the phased roadmap. Written to be executed — nothing here is left for "we'll decide later." Building has not started; this is the ready-to-go plan.

Client Les Cours Hyperbaric Centre / Groupe SoltronPrepared by DevelopmentStatus Ready to execute
01

Scope & guiding principles

Build a bespoke clinic-management system the front desk lives in, without touching the Medesync medical record — architected so the lean first release grows into the full product without rework.

The two scopes, held at once

The client team agreed to keep Release 1 lean: the booking calendar, with patient records staying in Medesync. But we design the schema and services for the full system (patients, billing, packages, reports, leads) from day one, so nothing has to be re-architected later. The difference between R1 and later is which UI ships, not which foundation exists.

The one rule that sets everything

Capture first, report second. No-shows, cancellation timing, booking source and package usage can only be counted if recorded the moment they happen — they can't be backfilled. So the capture layer (§4) is built in R1 even though the reports that read it come later. This costs almost nothing now and is the difference between "we can report on this" and "that data is gone."

Non-negotiable principles

02

Architecture overview

A single Next.js application (UI + API in one deployable) on top of PostgreSQL, self-hosted in a Canadian region, with a thin real-time channel for the live board and a signed endpoint for the AI receptionist.

For a single clinic (tens of staff, hundreds of bookings/day, thousands of patients) a modular monolith is the correct choice — not microservices. It is simpler to secure, cheaper to run, easier to reason about, and fast enough by an order of magnitude. We keep clean module boundaries (booking, patients, billing, packages, reports, auth) so pieces could be extracted later, but we do not pay the distributed-systems tax now.

                         ┌──────────────────────────────────────────────┐
   Staff browsers  ───►  │  nginx (TLS 1.3, HSTS, security headers,      │
   (desktop/tablet)      │        rate-limit)  — Canadian region        │
                         └───────────────┬──────────────────────────────┘
                                         │
   Retell voice AI  ─── signed ─────►    │  ┌──────────────────────────┐
   (FR/EN, guardrailed)                  └─►│  Next.js app (TypeScript) │
                                            │  • React UI (the demo)    │
                                            │  • Route handlers / RPC   │
   Web widget (later) ── signed ──────────►│  • Server actions         │
                                            │  • Auth + RBAC middleware │
                                            │  • Booking service (1 path)│
                                            └───────┬─────────┬─────────┘
                                                    │         │  SSE (live board)
                                          Prisma ORM│         └──────────────► browsers
                                                    ▼
                                       ┌────────────────────────────┐
                                       │ PostgreSQL 16 (Canada)     │
                                       │ • EXCLUDE constraint       │
                                       │ • append-only audit trigger│
                                       │ • materialized report views│
                                       │ • encrypted at rest + PITR │
                                       └────────────────────────────┘
                                       Encrypted object storage (CA) — consent scans (later phase)
  

Environments: dev (local, Postgres 16 + seed), staging (mirror of prod, client UAT), production (Canadian region). Promotion is via GitLab CI; database migrations run automatically and are reversible.

03

Technology stack

Deliberately boring, proven choices — the scaffold already uses most of them, so R1 starts from a running codebase.

LayerChoiceWhy
App frameworkNext.js 15 · TypeScriptOne deployable (UI + API), the scaffold already uses it, strong typing end-to-end.
DatabasePostgreSQL 16The EXCLUDE conflict constraint, range types, materialized views, PITR — nothing else does this as cleanly.
ORM / migrationsPrismaType-safe queries + versioned, reversible migrations. Raw SQL for the GiST constraint & triggers.
ValidationZodOne schema per input, shared client/server, parse-don't-validate.
AuthAuth.js (credentials) + argon2idSession cookies (httpOnly, SameSite), DB-backed sessions, role in JWT/session. Optional TOTP MFA for admins.
Real-timeSSE + Postgres LISTEN/NOTIFYLive board updates across stations without a websocket server; trivial behind nginx.
TimeLuxonDST-correct America/Montreal; store UTC, render Montreal, never infer tz.
Files (later)Canadian S3-compatible + presignedEncrypted object storage in Canada for consent scans; never in the app server.
Background jobspg-boss (Postgres-backed)Package-expiry sweeps, report refresh, reminders — no extra infra (uses the DB).
Error trackingGlitchTip / self-hosted Sentry (CA)Errors + performance, hosted in Canada so no PII leaves the region.
TestsVitest + PlaywrightUnit (engine), integration (API+DB), E2E (the harness we already built).
CI/CD & VCSSelf-hosted GitLab + GitLab CIAlready ours; lint → typecheck → test → build → deploy pipeline.
Runtime/deployDocker + systemd behind nginxReproducible, easy rollback; PM2 acceptable alternative.
04

Data model & the capture layer

The foundation. Every field that a future report or the billing engine will need is present from R1, even where its UI ships later.

Core entities

TableHoldsR1?
resource_typesKind of bookable unit (HBOT, Laser) — data, not codeYes
resourcesChambers + laser: name, display order, active, ai_bookableYes
servicesCatalog: default/min/max duration, buffer, price, allowed resource typesYes
therapists / usersStaff, the 4 roles, therapist assignmentYes
patientsLean contact record (name, phone, email) + acquisition_source + emergency contactYes*
appointmentsThe core row — capture fields belowYes
weekly_hours / date_exceptionsOperating window per weekday + per-date overridesYes
resource_downtimeChamber out-of-service periods (so downtime ≠ poor utilisation)Yes
notes / attachmentsSession notes, consent scansLater*
invoices / invoice_items / tax_lines / paymentsBilling ledger — multiple tax lines, discounts, refunds first-classLater
package_templates / patient_packages / package_redemptionsPrepaid packages — the "sold but not delivered" engineLater
leadsInquiry follow-up (Brevo replacement)Later
audit_logAppend-only, every mutation, who/what/whenYes

* Scope note: whether consent scans + session notes live here (vs. Medesync) is the open patient-data decision. The schema supports both; UI ships per that call.

The appointment — capture fields (built R1, un-backfillable)

-- statuses: the full nine, so cancellation & no-show reports are ever possible
status        ENUM(requested, confirmed, checked_in, in_treatment, completed,
                   cancelled_by_client, cancelled_by_clinic, no_show, rescheduled)
booked_at     timestamptz   -- when the booking was CREATED (≠ start; powers lead-time/demand)
starts_at / ends_at   timestamptz   -- the actual appointment (UTC)
buffer_minutes int       -- cleaning time, attached to THIS booking, removable, excluded from reports
cancelled_at  timestamptz   -- to compute cancellation notice period
cancel_reason text
rescheduled_from_id  fk    -- trace a moved booking
booking_source ENUM(13 values)  -- "how did you hear about us" — Valerie's #1 report
booking_channel ENUM(voice_ai, web, phone, walk_in, staff)
-- patient.acquisition_source (first-touch) is SEPARATE from appointment.booking_source (per-booking)

The conflict rule — enforced by Postgres

CREATE EXTENSION btree_gist;
occupancy = tstzrange(starts_at, ends_at + (buffer_minutes||' min')::interval, '[)')  -- half-open
ALTER TABLE appointments ADD CONSTRAINT one_patient_per_chamber
  EXCLUDE USING gist (resource_id WITH =, occupancy WITH &&)
  WHERE (status IN ('requested','confirmed','checked_in','in_treatment','completed'));

Half-open ranges make back-to-back bookings legal (a buffer ending 10:45 and the next start at 10:45 don't overlap); the WHERE means cancelled/no-show free the slot instantly while the row stays for reporting.

Migrations

Prisma migrations for tables/columns; the EXCLUDE constraint, the audit trigger, and materialized views ship as raw-SQL migration steps. Every migration is reversible and runs in CI against a throwaway DB before prod.

05

Booking engine & concurrency

One pure function validates every booking; the database is the final arbiter; two writers can never take the same slot.

06

API design & the single write path

Server actions and route handlers, all funnelling booking mutations through one service. Reads are typed queries; writes are validated, authorized, audited.

SurfaceEndpoints (representative)
ScheduleGET /api/day?date= · POST /api/appointments · PATCH/DELETE /api/appointments/:id
PatientsGET/POST/PATCH /api/patients · GET /api/patients/:id
Billing/api/invoices · /api/quotes · /api/payments (later)
Packages/api/packages · /api/patient-packages · redemption on appointment.completed (later)
ReportsGET /api/reports/:name?from=&to= (reads materialized views)
AI (signed)POST /api/ai/availability → /api/ai/hold → /api/ai/confirm
LiveGET /api/day/:date/stream (SSE)
07

Authentication & roles

Four roles, enforced on the server for every action — never trusting the browser.

ActionAdminReceptionistTherapistBilling
Book / move / cancel appointmentsview + status
Set no-show / completed
Remove a cleaning bufferper-instance
Create invoices / take payment / refundinvoice only✓ (refund = admin)
View reportslimitedfinancial
Settings (chambers, hours, users)
08

Security & Law 25 compliance

Defense-in-depth for a Quebec clinic handling patient-adjacent data — hardened at every layer and audited before go-live.

An honest word on "100% secure"

No system — ours or anyone's — is literally 100% secure; a plan that claimed so would be lying. What we commit to is a professionally hardened, defense-in-depth posture that meets Quebec Law 25 / PIPEDA, plus an independent security review and penetration test before go-live. That is how real security is done: layers, least privilege, monitoring, and an outside set of eyes.

The layers

LayerControls
TransportTLS 1.3, HSTS preload, secure headers (CSP, X-Frame-Options DENY, X-Content-Type-Options, Referrer-Policy), no mixed content.
At restFull-disk encryption (LUKS) on DB & storage, encrypted backups, optional column-level encryption for the most sensitive fields.
AuthNargon2id, secure sessions, lockout, optional MFA for admins (see §7).
AuthZServer-side RBAC on every route + data-scoping; deny by default; the client is never trusted.
InputZod validation everywhere; parameterized queries via Prisma (no string SQL); React auto-escapes output → XSS-safe; strict CSP.
UploadsType + size allow-list, content-sniffing, antivirus scan, stored out-of-app in encrypted CA storage via presigned URLs; never executed.
AuditAppend-only audit_log written by a DB trigger inside the same transaction — the app role can INSERT only; tamper-evident, optionally shipped off-box.
AbuseRate limiting (login, AI endpoint, reports), CSRF tokens on state-changing requests, locked-down CORS.
SecretsNever in the repo; environment-injected, least-privilege DB roles, rotation schedule, a secrets vault in prod.
AI endpointDedicated service credential, HMAC-signed requests, strict rate limit, idempotency, and fewer privileges than staff (create-only, no override) — see §11.
Dependenciesnpm audit / Snyk in CI, pinned versions, minimal surface, regular patching.
Backups & DRAutomated daily + point-in-time recovery, encrypted, Canadian region, restores tested on a schedule, documented RTO/RPO.
MonitoringError tracking (CA-hosted), security/auth alerts, uptime, log retention.

Law 25 / PIPEDA specifics

09

Performance & optimization

At clinic scale the system is inherently fast; we still design so it stays instant and never does needless work.

10

Real-time & multi-user

When several patients are seated at 9:00 across chambers, every station's board updates live.

11

AI voice booking (bilingual, guardrailed)

One French/English AI receptionist that only books, stays on-script, and writes straight into this calendar — replacing today's unguarded agent.

Fixing the current liability

The existing agent has no guardrails (it was made to recite the US Constitution for 15 minutes, and there's a 17-minute off-topic call — billed per minute). The new agent is hardened: strict scope, short max-duration, and no ability to go off-book.

12

Files & documents

Consent scans and session notes — built the moment the patient-data scope is confirmed.

13

Testing & QA

The booking rules are safety-critical, so they get the most tests; everything ships behind CI.

14

Hosting, DevOps & backups

15

Migration & go-live

No export from Medesync (confirmed not feasible). We start clean and let history build.

  1. Seed the clinic config — chambers + laser, services, operating hours, users/roles.
  2. Cutoff date agreed with the client; from that date, bookings live in the new system.
  3. ~2 weeks of current bookings keyed in by staff (a fast entry screen). This is the clinic using its own operational data — not the Medesync "export" that triggers TELUS's legal process.
  4. UAT on staging with Luisa (workflow) and Valerie (report definitions) signing off.
  5. Go-live: switch the AI + front desk to the new calendar; Medesync stays for the medical record.
  6. Reporting note: figures start counting at go-live — month one of data is month one. (Exactly why the capture layer is R1.)
16

Phased roadmap

The order is chosen so each phase proves the foundation and unblocks the next. Estimates are engineering-time ranges for planning, not quotes.

Phase 0

Foundation & hardening

Repo, CI/CD, environments, Postgres in a CA region, auth + RBAC, audit trigger, security headers, backups. The secure skeleton.

~1–1.5 wk
Phase 1

Capture layer + Daily Schedule

The full appointment schema (9 statuses + capture fields), the EXCLUDE constraint, the booking service, and the calendar UI (from the demo) wired to it — with live board (SSE). This is the agreed Release 1.

~2–3 wk
Phase 2

AI booking path

The signed availability/hold/confirm API + the bilingual guardrailed agent writing into the calendar; capture booking source.

~1–1.5 wk
Phase 3

Patients (per scope)

Contact records, emergency contact; notes + consent scans if the patient-data scope is confirmed.

~1–2 wk
Phase 4

Invoicing, quotes & billing history

Multi-tax-line invoices, quote→invoice, the ledger; payments tracked (Moneris external).

~2 wk
Phase 5

Packages

Templates, patient packages, redemption on completed session — the sold-vs-delivered engine.

~1–1.5 wk
Phase 6

Reports + dashboard

Materialized views + Valerie's 13 reports in her priority order; the management dashboard.

~2–3 wk
Phase 7

Leads CRM (Brevo replacement)

Inquiry board, call logs, won/lost.

~1 wk
Phase 8

Hardening, pen-test, UAT & go-live

Load + security testing, the independent review, staging UAT sign-off, migration + cutover.

~1.5–2 wk

Phases 3–7 can be re-ordered to the client's priority; only Phase 0 and the Phase-1 capture layer are strict prerequisites.

17

Risks & mitigations

RiskMitigation
Reporting can't be backfilledCapture layer ships in R1 regardless of when reports are built.
Two systems → double entry (Medesync + ours)Accepted trade-off of the lean scope; set expectations; keep the new booking flow fast; revisit if the client later wants more consolidated.
Patient-data scope creep (notes/scans = health data)Written data-classification boundary; build files only when confirmed; Canadian hosting either way.
AI going off-script / costHardened prompt, max-duration, rate limits, create-only privileges, full audit.
Data residency / Law 25Everything Canadian-region; independent review before go-live; Earl signs off.
Concurrency double-bookingDB EXCLUDE constraint makes it physically impossible; covered by tests.
Scope drift across three stakeholdersSingle sign-off (Abith), phased delivery, UAT gates per phase.
18

Appendix & checklists

Go-live security checklist

Environment configuration (representative)

DATABASE_URL           # Postgres 16, Canadian region
AUTH_SECRET            # session signing
AI_SERVICE_KEY / AI_HMAC_SECRET   # signed voice-agent endpoint
STORAGE_ENDPOINT / KEYS           # CA object storage (later)
SENTRY_DSN             # CA-hosted error tracking
SMTP_*                 # invoice/quote email (later)

What already exists

A tested, deployed front-end demo of every screen (crm.nexuscai.com), a validated booking engine + Postgres schema pattern (the calendar repo), the Prisma scaffold, and the Playwright test harness. Phase 0/1 start from running code, not a blank page.