Architecture Overview
import { Badge } from ‘@astrojs/starlight/components’;
Architecture Overview
Section titled “Architecture Overview”End-to-end system design, multi-tenancy model, and request lifecycle.
Last reviewed: 2026-06-23
System Diagram
Section titled “System Diagram”graph TB subgraph Clients["🌐 Clients"] Browser["Browser (SPA)"] PWA["PWA (installed)"] WhatsApp["WhatsApp (patient)"] end
subgraph Railway["🚂 Railway Platform"] FE["Frontend Service<br/>React 18 + Vite + Caddy<br/>:80 → dist/*"] BE["Backend Service<br/>Node.js 22+ / Express 4<br/>:3001 → /api/* /webhooks/*"] end
subgraph Supabase_["🗄️ Supabase"] PG[("PostgreSQL<br/>+ RLS")] Auth["Auth Service<br/>ES256 JWTs"] end
subgraph Redis_["⚡ Redis (Railway)"] BQ["BullMQ Queues"] Cache["Cache + Locks"] end
Browser -->|HTTPS| FE PWA -->|HTTPS| FE FE -->|HTTPS /api/*| BE
WhatsApp -->|Messages| MetaCloud["Meta Cloud API"] MetaCloud -->|POST /webhooks/whatsapp| BE
BE --> PG BE --> BQ BE --> Cache BE -->|Claude Haiku| Anthropic["Anthropic API"] BE -->|Payment webhooks| LS["LemonSqueezy"] BE --> Sentry
FE --> Sentry BQ -.->|Delayed jobs| BE
style Clients fill:rgba(5,150,105,0.13),stroke:#059669 style Railway fill:rgba(5,150,105,0.13),stroke:#059669 style Supabase_ fill:rgba(5,150,105,0.13),stroke:#059669 style Redis_ fill:rgba(5,150,105,0.13),stroke:#059669 style BE fill:rgba(5,150,105,0.25),stroke:#059669 style FE fill:rgba(5,150,105,0.25),stroke:#059669 style PG fill:rgba(5,150,105,0.13),stroke:#059669 style BQ fill:rgba(5,150,105,0.13),stroke:#059669 style Cache fill:rgba(5,150,105,0.13),stroke:#059669Core Design Principles
Section titled “Core Design Principles”Multi-Tenancy
Section titled “Multi-Tenancy”Every piece of data is scoped to a clinic_id. Three enforcement layers:
- JWT payload —
clinicIdembedded in every token; backend reads fromreq.user.clinicId. - Application layer — Every DB query explicitly filters
.eq('clinic_id', req.user.clinicId). No exceptions. Automated regression guards (tenantScopingGuard.test.js) catch any miss. - Row-Level Security (RLS) — Supabase policies at the database level using
app.clinic_idsession variable. Safety net, not primary enforcement.
Two Supabase clients support this pattern:
| Client | Key | RLS | When to use |
|---|---|---|---|
supabase (req-scoped req.db) |
Publishable/anon | Enforced | All user-scoped reads |
supabaseAdmin |
Service role | Bypassed | Writes with code-enforced tenancy, background jobs, bootstrap |
Subscription Gating
Section titled “Subscription Gating”Middleware chain on every protected route:
Request → authenticateToken → requireActiveSubscription → [requireAdmin] → [checkDoctorLimit | checkUserLimit | requirePlan] → handlerRequest Lifecycle
Section titled “Request Lifecycle”User login:
POST /api/auth/login (email, password) → bcrypt.compare → check is_active (user + clinic) → build ES256 JWT { userId, clinicId, email, role, subscriptionPlan, subscriptionExpiresAt } → Frontend stores token in sessionStorage → Subsequent requests: Authorization: Bearer <token> → Every request: requireActiveSubscription live DB checksequenceDiagram actor User as Staff User participant FE as Frontend (React) participant BE as Backend (Express) participant DB as Supabase (PostgreSQL) participant JWT as JWT Service
User->>FE: Enter email + password FE->>BE: POST /api/auth/login BE->>DB: SELECT user WHERE email = ? BE->>BE: bcrypt.compare(password, hash) BE->>DB: SELECT clinic WHERE id = ? BE->>DB: Check user.is_active AND clinic.is_active BE->>JWT: Build ES256 JWT (userId, clinicId, role, plan) JWT-->>BE: Signed token BE-->>FE: { token, user: {...} } FE->>FE: Store in sessionStorage Note over FE,BE: All subsequent requests include Authorization: Bearer <token>WhatsApp appointment booking:
Patient sends WhatsApp → Meta Cloud API POST /webhooks/whatsapp → HMAC-SHA256 verify (timingSafeEqual) → 24h dedup check (Redis) → Route to clinic by phone_number_id → Acquire concurrency lock per (clinicId, patientPhone) → Load/create chat_session (state machine context) → Safety checks: bot_enabled? is_blocked? human_handover? → Short-circuit known button IDs (CONFIRM_BOOKING, RESET, dental recall replies) → Claude Haiku NLU with clinic context + real availability slots → JSON response: { action, collected, readyToConfirm, reply } → If readyToConfirm: verify slot → insert appointment → schedule reminders → send card → Otherwise: send clarifying text reply → Release concurrency lockJob Queue Topology
Section titled “Job Queue Topology”Three BullMQ queues, all backed by a shared Redis instance. All cron schedules in UTC (El Salvador = UTC-6, no DST).
| Queue | Workers | Purpose |
|---|---|---|
appointment-reminders |
5 | 24h/2h reminder messages, attendance verification |
daily-agenda |
2 | Morning schedule to clinic staff |
dental-messages |
3 | Payment reminders, recall campaigns |
Cron Schedule
Section titled “Cron Schedule”gantt title Job Schedule (24h UTC Cycle) dateFormat HH:mm axisFormat %H:%M section Agenda Daily Agenda (13:00 UTC) :milestone, 13:00, 0m section No-Show Auto-Mark (05:00 UTC) :milestone, 05:00, 0m section Attendance Dispatch (every 30min) :active, 00:00, 24h section Payments Reminders (14:00 UTC) :milestone, 14:00, 0m section Recall Recall (06:00 UTC) :milestone, 06:00, 0mBull Board admin dashboard at /admin/queues (basic auth via QUEUES_ADMIN_USER/QUEUES_ADMIN_PASS).
Security Boundaries
Section titled “Security Boundaries”| Layer | Mechanism |
|---|---|
| CORS | Locked to FRONTEND_URL env var (no wildcard) |
| HTTP headers | Helmet on all responses |
| Rate limiting | Per-IP on auth, demo requests, calendar feeds; per-phone on WhatsApp |
| Encryption at rest | AES-256 for WhatsApp tokens and patient DUI |
| Webhook verification | HMAC-SHA256 (Meta), signing secret (LemonSqueezy) |
| Superadmin auth | Separate X-Admin-Key header, never mixed with JWT routes |
| Auth password | bcrypt cost factor 12 |
| JWT | ES256 (P-256 asymmetric), 8-hour expiry, live DB check on every request |
| Tenant isolation | App-layer filter + RLS safety net + automated regression guards |
Tech Stack
Section titled “Tech Stack”| Technology | Version | Purpose |
|---|---|---|
| Node.js | 22+ | Backend runtime |
| Express | 4.18 | HTTP framework |
| React | 18.2 | UI library |
| Vite | 5+ | Build tool |
| Supabase JS | 2.39 | DB client |
| BullMQ | 5.1 | Job queues |
| ioredis | 5.3 | Redis client |
| @anthropic-ai/sdk | 0.100+ | Claude Haiku |
| jsonwebtoken | 9.0 | JWT |
| bcryptjs | 2.4 | Password hashing |
| TanStack Query | 5.17 | Server state |
| lucide-react | 0.323 | Icons |
| Vitest | 4.1 | Testing |
| TypeScript | 5.x | Incremental migration |