Backend architecture
Stack: Node.js 22+, TypeScript, Hono, Mongoose, Zod (contracts in @sophia/shared). API runs on ECS Fargate; async work on Lambda + SQS.
Monorepo layout
flowchart TB
subgraph apps
api["apps/api — Hono HTTP"]
fn["apps/functions — SQS handlers"]
web["apps/web — SPA"]
end
subgraph packages
shared["packages/shared — Zod + types"]
platform["packages/platform — Node runtime"]
end
api --> shared
api --> platform
fn --> shared
fn --> platform
fn --> api
web --> shared- pnpm workspaces + Turborepo — catalog pins dependency versions;
pnpm verifyruns lint, typecheck, test, build, infra typecheck. @sophia/shared— wire shapes and env schemas for API and web (web must not import platform).@sophia/platform— config, logger, Mongo,AppError, Firebase verify (Node only).@sophia/api— also exports settlement and report-generation for Lambdas without starting HTTP.
Request lifecycle
flowchart LR Client --> Routes["*.routes.ts"] Routes --> MW["validate · auth · CORS"] MW --> Service["*.service.ts"] Service --> Repo["*.repo.ts"] Repo --> Model["*.model.ts"] Model --> Mongo[(MongoDB)] Service --> Serialize["toResponse()"] Serialize --> JSON[JSON response]
- Startup: connect Mongo →
createApp()→ listen onconfig.PORT; same app used in tests viaapp.request(). - No controller layer — Hono handlers stay typed inline; services never import Hono.
- Tenancy: every repo call takes
organizationIdfirst; single shared database. - Errors: throw
AppError; oneapp.onError→ shared error envelope; 5xx logged as error, 4xx as warn. - Async: API publishes to SQS (interview completed → settlement Lambda → report queue → report Lambda); missing queue URLs no-op locally.
Layered modules
Each domain folder follows the same pattern:
| File | Role |
|---|---|
*.routes.ts | HTTP: validation, auth, call service, return JSON |
*.service.ts | Business rules |
*.repo.ts | Mongoose queries |
*.model.ts | Schema + model |
- Global middleware: observability (request ID), secure headers, CORS from
CORS_ORIGINS. - Auth variants: Firebase (
requireAuth/requireAdmin), candidate session, bot internal token. - External calls from services: Firebase, Stripe, Daily.co, S3 presigns, optional OpenAI/Gemini for parsing.
System context
flowchart TB Web["Web SPA"] --> ALB["ALB"] Cand["Candidate join"] --> ALB Bot["Interview bot"] --> ALB ALB --> API["Fargate · Hono"] API --> Mongo[(MongoDB)] API --> S3["S3 resumes"] API --> Q1["SQS interview-completed"] Q1 --> L1["Lambda settlement"] L1 --> Q2["SQS report-generate"] Q2 --> L2["Lambda report"] L2 --> OpenAI["OpenAI"] Stripe["Stripe webhook"] --> ALB
See system overview for the full product path.