Skip to content

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 verify runs 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 on config.PORT; same app used in tests via app.request().
  • No controller layer — Hono handlers stay typed inline; services never import Hono.
  • Tenancy: every repo call takes organizationId first; single shared database.
  • Errors: throw AppError; one app.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:

FileRole
*.routes.tsHTTP: validation, auth, call service, return JSON
*.service.tsBusiness rules
*.repo.tsMongoose queries
*.model.tsSchema + 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.

Sophia AI Interview Platform — Internal Documentation