Developer Handbook
This handbook is the engineering counterpart to the rest of this site. Everything else here is written for the people who use BrightBlur; this section is written for the people who build it. It is deliberately kept together in one place (apps/docs/src/content/docs/internals/) so it can be lifted out into a separate internal site later without untangling it from the user-facing docs.
If you are reading this on your first day: start here, then read Architecture, then pick the chapter closest to whatever you have been asked to touch. You do not need to read all of it before writing code — but you should read Conventions before opening your first pull request.
What BrightBlur is
Section titled “What BrightBlur is”BrightBlur is a privacy-first photo-sharing PWA. The core idea: when you share a photo, the faces in it are permanently pixelated in the base image and each face is encrypted separately to the people authorised to see it. The server only ever stores ciphertext — it never sees an unblurred face, a private key, or any biometric data. Viewers fetch the encrypted face slices they hold keys for, decrypt them on-device, and composite them back over the pixelated photo in real time.
Two capabilities make this more than a toy:
- On-device face detection and recognition. Faces are detected, aligned, and embedded entirely in the browser (TFLite + WASM in a Web Worker). The app learns the people you tag so it can suggest matches on future uploads — without a single pixel leaving the device for inference.
- Per-group end-to-end encryption with key rotation. Every “group” (a sharing circle or a person) has an epoch keypair. Content is encrypted to the group’s current epoch public key; members hold wrapped private keys. Removing someone rotates the epoch, so they keep nothing created after they left.
The product goals, in priority order: privacy by construction (the server is untrusted), it must actually work offline-ish and feel instant (PWA, optimistic UI, on-device ML), and it must be approachable (people-first sharing, automatic recognition, batch upload).
The system in one screen
Section titled “The system in one screen”┌────────────────────────── browser (untrusted server below) ──────────────────────────┐│ ││ UPLOAD VIEW ││ ────── ──── ││ decode image fetch encrypted photo + the ││ → detect faces (SCRFD, Web Worker) face slices you hold keys for ││ → pixelate faces (destructive 5×5 mosaic) → decrypt locally (KeyRing) ││ → align + embed (ArcFace/Umeyama, w600k) → composite faces over the ││ → match to people (template pools, raw cosine) pixelated base, watermark ││ → encrypt: ││ • base image → circle epoch keys ││ • face slices → person-group epoch keys ││ • embeddings → person-group epoch keys ││ → POST ciphertext + wrapped keys │└────────────────────────────────────────────────────────────────────────────────────────┘ │ only ciphertext + wrapped keys cross this line │┌──────────────────────────────────────── Cloudflare ──────────────────────────────────────┐│ Worker (SvelteKit) · D1 (SQLite, Drizzle) · R2 (blob storage) ││ stores: photos, circles, face_slices, person_embeddings, epoch/wrapped keys, sessions… ││ enforces: membership, auth, rate limits — never decrypts anything │└────────────────────────────────────────────────────────────────────────────────────────────┘The single most important invariant in the whole codebase: the line between the browser and the server is a trust boundary. Plaintext images, face pixels, embeddings, and private keys live only on the left of that line. If you find yourself sending any of those to the server, stop — you have a bug.
How this handbook is organised
Section titled “How this handbook is organised”| Chapter | What it covers |
|---|---|
| Architecture & Tooling | Monorepo layout, the stack, the request lifecycle, where code lives, local dev, build, deploy, and CI. |
| Data Model | The Drizzle schema table-by-table, the entity relationships, the D1 client, and migrations. |
| Encryption & Keys | The trust model, epoch keys, key wrapping/rotation, the KeyRing abstractions, and recovery — from an engineer’s point of view. |
| Recognition Pipeline | On-device detection, alignment, embedding, template pools and matching, the TFLite runtime, and the service-worker model cache. |
| The Publish Pipeline | What happens between “user hits publish” and “ciphertext is on the server”, end to end. |
| API Contracts | The HTTP surface: endpoint inventory, request/response shapes, auth, and the cross-cutting middleware. |
| Frontend Architecture | SvelteKit structure, Svelte 5 runes and the house state patterns, shared components, and styling tokens. |
| Conventions & Process | TDD, testing, commit style, British English, the design system, and the current merge-gate policy. Read this first. |
The five-minute conventions you cannot skip
Section titled “The five-minute conventions you cannot skip”These are expanded in Conventions, but if you read nothing else:
- Server logic lives in
src/lib/server/api/; the files insrc/routes/api/are thin wrappers. Put real logic in the API module, not the route. - Red–green TDD, two commits. Commit a failing test first (red), then the implementation that makes it pass (green), so a reviewer can validate the bug from the red commit.
- British English everywhere — code comments, docs, UI copy — with correct diacritics. Conventional-commit messages. Named imports, never namespace imports.
$env/dynamic/privatefor server env vars, never$env/static/private.- Run
.sveltefiles through the Svelte MCP autofixer before finalising them. - Merge gate (interim): GitHub Actions minutes are currently exhausted, so CI does not run. Until they return, every PR runs the CI commands locally on the head commit —
pnpm check,pnpm test:unit,pnpm buildfromapps/web— gets an independent review, and records the substitution in a PR comment. Any merged migration needs a manualcd apps/web && pnpm db:migrate:remoteafter deploy.
The visual design system (colour tokens, type scale, spacing, radii) is the single source of truth in
docs/design.mdat the repo root. Read it before touching UI and update it in the same change when you add a token.