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.yamlpackages: ['apps/*']). There is no packages/ directory; everything is an app.

AppPackageRoleDeploys to
apps/web@brightblur/webThe product — a SvelteKit 5 PWA.Cloudflare Workers
apps/docs@brightblur/docsThis 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). .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 $env/dynamic/private (never $env/static/private).

  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):

VarWhy
JWT_SECRETRequired; must be 32+ chars of high entropy or wrangler dev 500s on every request.
DEV_SKIP_AUTH=trueBypasses 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_KEYOptional — 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 from pnpm setup:models (run automatically by build).

The key apps/web scripts:

ScriptWhat it does
devVite+ dev server with Miniflare-emulated D1/R2. dev:auth runs the real passkey/password flow.
checksvelte-kit sync && svelte-check — the type gate.
test:unitthe Vitest suite (vp test run).
buildsetup: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:remotegenerate migration SQL / apply locally / apply to prod.
db:pushdrizzle-kit schema push for fast local iteration — not the production path; don’t confuse it with db:migrate.
test:e2ePlaywright (chromium + chromium-heavy).
verifycheck && 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. Four daily cron triggers run on-this-day notifications, passkey-challenge cleanup, session cleanup, and R2 orphan reconciliation.
  • 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 [non-blocking], test:unit), 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”
  1. 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 runs cd apps/web && pnpm db:migrate:remote by hand. Until minutes return, every merged migration needs that manual step immediately after deploy (verify with wrangler d1 migrations list brightblur --remote --env production), and PRs substitute local gates for CI (see Conventions).
  2. The TFLite e2e gate. Any change to apps/web/src/lib/image/tflite-runtime.ts or the worker bootstrap must run the heavy upload e2e against the production build:
    Terminal window
    cd apps/web && pnpm build && npx playwright test upload.test.ts --project=chromium-heavy
    The importScripts polyfill branch is unreachable from Vitest by design (an XMLHttpRequest guard keeps it off Node), so check + test:unit cannot catch regressions there. This is how the June 2026 _malloc incident reached production. The e2e web server is wrangler dev on port 4173 and needs a valid .dev.vars (both DEV_SKIP_AUTH and a strong JWT_SECRET) plus a migrated local D1.

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).