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/*']). There is no packages/ directory; everything is an app.
| App | Package | Role | Deploys to |
|---|---|---|---|
apps/web | @brightblur/web | The product — a SvelteKit 5 PWA. | Cloudflare Workers |
apps/docs | @brightblur/docs | This documentation site (Astro + Starlight). | Cloudflare Pages |
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 $env/dynamic/private (never $env/static/private).
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. |
Some older READMEs mention
.env/setup:mediapipe— both are stale. The env file is.dev.vars; ML assets come frompnpm setup:models(run automatically bybuild).
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:generate / db:migrate / db:migrate:remote | generate migration SQL / apply locally / apply to prod. |
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. Four daily cron triggers run on-this-day notifications, passkey-challenge cleanup, session cleanup, and R2 orphan reconciliation. - 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 [non-blocking],test:unit), 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”- Interim gate (Actions minutes exhausted). CI — including
migrate-prod— does not currently run, but Workers Builds still deploys code. Merging a migration therefore ships code against a stale prod schema until someone runscd apps/web && pnpm db:migrate:remoteby hand. Until minutes return, every merged migration needs that manual step immediately after deploy (verify withwrangler d1 migrations list brightblur --remote --env production), and PRs substitute local gates for CI (see Conventions). - The TFLite e2e gate. Any change to
apps/web/src/lib/image/tflite-runtime.tsor the worker bootstrap must run the heavy upload e2e against the production build:TheTerminal window cd apps/web && pnpm build && npx playwright test upload.test.ts --project=chromium-heavyimportScriptspolyfill branch is unreachable from Vitest by design (anXMLHttpRequestguard keeps it off Node), socheck+test:unitcannot catch regressions there. This is how the June 2026_mallocincident reached production. The e2e web server iswrangler devon port 4173 and needs a valid.dev.vars(bothDEV_SKIP_AUTHand a strongJWT_SECRET) plus a migrated local D1.
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).