Skip to content

Architecture & Tooling

This chapter is the map. It tells you where code lives, how to run it, and how it gets to production — enough to navigate the repo confidently before you dive into any one subsystem.

A pnpm workspace (pnpm-workspace.yaml → packages: ['apps/*', 'packages/*']). Two apps ship to users; three shared libraries under packages/ are consumed by both.

Package Name Role Deploys to
apps/web @brightblur/web The product — a SvelteKit 5 PWA. Cloudflare Workers
apps/mobile @brightblur/mobile Native iOS/Android app (Expo 54 / React Native 0.81; on-device TFLite via react-native-fast-tflite, react-native-libsodium). App stores / dev builds
apps/docs @brightblur/docs This documentation site (Astro + Starlight). Cloudflare Pages
packages/crypto @brightblur/crypto Shared hybrid X25519+ML-KEM-768 envelope, key derivation, BIP39 mnemonic. (library)
packages/recognition @brightblur/recognition Shared face math: SCRFD decode, alignment, embedding-payload codec, pool matching, clustering, thresholds. (library)
packages/wire @brightblur/wire Single source of truth for the /api/photos multipart field names. (library)

The pure crypto and recognition algorithms live in packages/ so the web and native-mobile apps run byte-for-byte identical logic; each app keeps only its platform glue (browser workers/TFLite-WASM on web, native modules on mobile).

Toolchain facts that bite if you miss them:

  • Node ≥ 22, pnpm 10.33.0 (pinned in root package.json). .npmrc sets engine-strict=true, so an older Node hard-fails install.
  • The project runs on Vite+ (vp) under the hood — vp dev, vp build, vp test wrap Vite/Rolldown/Vitest. The pnpm scripts call these. Import test utilities from vite-plus/test, not vitest. See AGENTS.md for the Vite+ notes.

SvelteKit 5 · Svelte 5 (runes mode enforced globally) · Tailwind 4 · Bits UI (headless primitives) · Drizzle ORM over Cloudflare D1 · R2 for blobs · libsodium + @noble/post-quantum for crypto · TFLite (WASM) for on-device ML · valibot for validation · SimpleWebAuthn for passkeys · Vitest (unit) · Playwright (e2e).

Where things live inside apps/web/src:

routes/ SvelteKit routes
api/ HTTP API — THIN wrappers over lib/server/api
(pages) app pages: feed, upload, people, photos/[id], setup, admin…
lib/
server/ server-only code
api/ ← the real API logic + the membership/auth/validation helpers
schema.ts the Drizzle schema (one file)
d1.ts / db.ts the D1 client
crypto/ key model, hybrid encryption, KeyRing, the crypto worker facade
image/ on-device ML: detection, alignment, embedding, matching, pools, TFLite runtime
publish/ the publish pipeline (encrypt + upload)
workers/ Web Worker facades (ml, crypto) with main-thread fallback
components/ shared Svelte components
hooks.server.ts per-request: CSRF, body caps, rate limits, session, DB binding
service-worker.ts PWA cache strategy (app shell + ML model cache)

The architectural rule you will see enforced in review: logic in lib/server/api/, routes stay thin. Drizzle schema is the single schema.ts; server env vars come from event.platform.env (threaded per-request via setRequestEnv in hooks.server.ts, read through $lib/server/env) — never the $env/static/private or $env/dynamic/private modules.

  1. A request hits the Worker. hooks.server.ts binds the per-request Drizzle client (setRequestDb(platform.env.DB)), runs the CSRF same-origin check, enforces the JSON body cap, applies per-IP rate limits, and resolves the session into event.locals.user.
  2. For /api/*, the route handler validates input and calls a lib/server/api/ function, which does the work against D1/R2 and throws typed errors on failure.
  3. For pages, server load functions (and shared loaders like the publish-page-load and person-group-gate) fetch data; the page renders with Svelte 5 runes and hydrates.

See API Contracts for the middleware detail and Frontend Architecture for the page/runes side.

Prerequisites: Node ≥ 22, pnpm 10.33.0, mkcert (local HTTPS for WebAuthn), and npx playwright install chromium if you’ll run e2e.

Terminal window
pnpm install
cp apps/web/.dev.vars.example apps/web/.dev.vars # gitignored
pnpm dev # http://localhost:5173

D1 and R2 are emulated by Cloudflare’s platformProxy (Miniflare); state persists under apps/web/.wrangler/state/v3/. A fresh worktree has an empty local D1 — run migrations before anything that touches the DB:

Terminal window
cd apps/web && pnpm db:migrate

.dev.vars essentials (all local-only):

Var Why
JWT_SECRET Required; must be 32+ chars of high entropy or wrangler dev 500s on every request.
DEV_SKIP_AUTH=true Bypasses auth on localhost. Not sufficient on its own for the e2e gate (which emulates the brightblur.app host and so forces real JWT validation — you need both this and a strong JWT_SECRET).
CRON_SECRET, INSTANCE_ADMIN_ID, VAPID_*, BREVO_API_KEY Optional — crons, admin routes, push, and email degrade gracefully when absent.

Older READMEs mention setup:mediapipe — that script is stale; ML assets come from pnpm setup:models (run automatically by build). Both .dev.vars and .env work: wrangler loads .dev.vars first and only falls back to .env when it is absent, so .dev.vars wins when both exist. Use .dev.vars for the wrangler-dev e2e gate (it is the only file that gate reads); .env (via cp .env.example .env) is fine for pnpm dev.

The key apps/web scripts:

Script What it does
dev Vite+ dev server with Miniflare-emulated D1/R2. dev:auth runs the real passkey/password flow.
check svelte-kit sync && svelte-check — the type gate.
test:unit the Vitest suite (vp test run).
build setup:assets (download + SHA-256-verify ML models) → vp build (Cloudflare adapter) → inject the cron scheduled handler into _worker.js.
db:migrate / db:migrate:remote apply migrations locally / to prod. Migrations are hand-authored NNNN_slug.sql — there is no db:generate (drizzle-kit’s generator is unused; see Data Model).
db:push drizzle-kit schema push for fast local iteration — not the production path; don’t confuse it with db:migrate.
test:e2e Playwright (chromium + chromium-heavy).
verify check && test:unit — the pre-push gate.
  • Build output is .svelte-kit/cloudflare/_worker.js via @sveltejs/adapter-cloudflare. A post-build step injects the scheduled (cron) handler and assert-scheduled.mjs verifies it’s there.
  • Bindings (wrangler.toml): DB (D1, brightblur), BUCKET (R2, EU jurisdiction), ASSETS. nodejs_compat is on. Production serves brightblur.app + www.brightblur.app; there’s a staging env. Five daily cron triggers (07:00–08:00 UTC) run on-this-day notifications, passkey-challenge cleanup, session/stale-tombstone cleanup, R2 orphan reconciliation, and a data-retention sweep of the audit/log tables.
  • Secrets (via wrangler secret put): JWT_SECRET is critical (weak/missing → 500 everywhere); CRON_SECRET, VAPID_*, INSTANCE_ADMIN_ID, BREVO_API_KEY are optional and degrade gracefully.
  • Deploy: Cloudflare Workers Builds auto-deploys on push to main (non-main branch builds are disabled in the dashboard). Manual: pnpm deploy / pnpm deploy:staging.
  • CI (.github/workflows/ci.yml): a verify job (check, lint/knip/audit [non-blocking], test:unit), a mobile typecheck job (tsc --noEmit over apps/mobile), a build job, and a migrate-prod job (db:migrate:remote on push to main). E2e is not in CI. R2 lifecycle rules live out-of-band (pnpm r2:lifecycle:apply; photos/ expire after 30 days).

Two operational gotchas that have bitten production

Section titled “Two operational gotchas that have bitten production”

Both now live in full on Operational Status & Gates, so they stay in one place rather than drifting across chapters:

  1. CI applies migrations, Workers Builds deploys — in parallel. On push to main, the migrate-prod job runs db:migrate:remote against production D1 while Workers Builds deploys the worker. For additive migrations the brief window where new code sees a missing table is harmless; for destructive or required-NOT NULL changes, apply the migration by hand before merging.
  2. The TFLite e2e gate. tflite-runtime.ts / worker-bootstrap changes are unreachable from Vitest and must be validated with the heavy chromium-heavy upload e2e against a production build — the path that let the June 2026 _malloc incident through.

Astro 6 + Starlight, served at docs.brightblur.app. Content is markdown/MDX under apps/docs/src/content/docs/, grouped by user-facing area; the sidebar is a hand-maintained array in apps/docs/astro.config.mjs. This handbook lives under internals/ — a single directory, wired into the sidebar as one group, so it can be lifted into a separate internal site later without disturbing the user docs. Build with astro build; deploy with wrangler pages deploy (or pnpm docs:build / pnpm docs:deploy from the root).