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.
The monorepo
Section titled “The monorepo”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)..npmrcsetsengine-strict=true, so an older Node hard-failsinstall. - The project runs on Vite+ (
vp) under the hood —vp dev,vp build,vp testwrap Vite/Rolldown/Vitest. Thepnpmscripts call these. Import test utilities fromvite-plus/test, notvitest. SeeAGENTS.mdfor the Vite+ notes.
apps/web: the stack
Section titled “apps/web: the stack”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 componentshooks.server.ts per-request: CSRF, body caps, rate limits, session, DB bindingservice-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.
The request lifecycle
Section titled “The request lifecycle”- A request hits the Worker.
hooks.server.tsbinds 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 intoevent.locals.user. - For
/api/*, the route handler validates input and calls alib/server/api/function, which does the work against D1/R2 and throws typed errors on failure. - For pages, server
loadfunctions (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.
Running it locally
Section titled “Running it locally”Prerequisites: Node ≥ 22, pnpm 10.33.0, mkcert (local HTTPS for WebAuthn), and npx playwright install chromium if you’ll run e2e.
pnpm installcp apps/web/.dev.vars.example apps/web/.dev.vars # gitignoredpnpm dev # http://localhost:5173D1 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:
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 frompnpm setup:models(run automatically bybuild). Both.dev.varsand.envwork: wrangler loads.dev.varsfirst and only falls back to.envwhen it is absent, so.dev.varswins when both exist. Use.dev.varsfor the wrangler-dev e2e gate (it is the only file that gate reads);.env(viacp .env.example .env) is fine forpnpm 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, deploy, and CI
Section titled “Build, deploy, and CI”- Build output is
.svelte-kit/cloudflare/_worker.jsvia@sveltejs/adapter-cloudflare. A post-build step injects thescheduled(cron) handler andassert-scheduled.mjsverifies it’s there. - Bindings (
wrangler.toml):DB(D1,brightblur),BUCKET(R2, EU jurisdiction),ASSETS.nodejs_compatis on. Production servesbrightblur.app+www.brightblur.app; there’s astagingenv. 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_SECRETis critical (weak/missing → 500 everywhere);CRON_SECRET,VAPID_*,INSTANCE_ADMIN_ID,BREVO_API_KEYare 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): averifyjob (check, lint/knip/audit [non-blocking],test:unit), amobiletypecheck job (tsc --noEmitoverapps/mobile), abuildjob, and amigrate-prodjob (db:migrate:remoteon 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:
- CI applies migrations, Workers Builds deploys — in parallel. On push to
main, themigrate-prodjob runsdb:migrate:remoteagainst 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 NULLchanges, apply the migration by hand before merging. - The TFLite e2e gate.
tflite-runtime.ts/ worker-bootstrap changes are unreachable from Vitest and must be validated with the heavychromium-heavyupload e2e against a production build — the path that let the June 2026_mallocincident through.
The docs site (apps/docs)
Section titled “The docs site (apps/docs)”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).