Skip to content

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.

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

┌────────────────────────── 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.

ChapterWhat it covers
Architecture & ToolingMonorepo layout, the stack, the request lifecycle, where code lives, local dev, build, deploy, and CI.
Data ModelThe Drizzle schema table-by-table, the entity relationships, the D1 client, and migrations.
Encryption & KeysThe trust model, epoch keys, key wrapping/rotation, the KeyRing abstractions, and recovery — from an engineer’s point of view.
Recognition PipelineOn-device detection, alignment, embedding, template pools and matching, the TFLite runtime, and the service-worker model cache.
The Publish PipelineWhat happens between “user hits publish” and “ciphertext is on the server”, end to end.
API ContractsThe HTTP surface: endpoint inventory, request/response shapes, auth, and the cross-cutting middleware.
Frontend ArchitectureSvelteKit structure, Svelte 5 runes and the house state patterns, shared components, and styling tokens.
Conventions & ProcessTDD, 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 in src/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/private for server env vars, never $env/static/private.
  • Run .svelte files 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 build from apps/web — gets an independent review, and records the substitution in a PR comment. Any merged migration needs a manual cd apps/web && pnpm db:migrate:remote after deploy.

The visual design system (colour tokens, type scale, spacing, radii) is the single source of truth in docs/design.md at the repo root. Read it before touching UI and update it in the same change when you add a token.