# CLAUDE.md Context for AI agents working in this repository. People should start with [CONTRIBUTING.md](CONTRIBUTING.md). ## What this is Secret shares notes and files end-to-end encrypted. The browser (or the SDK, or the CLI) encrypts before sending; the server adds a second layer (AES-256-GCM) but never sees the plaintext. The key lives in the URL fragment (`#key`), which browsers never send. One process serves the API and the web app's static build. ## Rules - **Zero-knowledge.** Plaintext, keys and passwords never reach the server. Never log note content, note ids, raw paths, keys or IP addresses: logs and metric labels hold route patterns and bounded values only. - **Two encryption layers**: XChaCha20-Poly1305 in the client, AES-256-GCM on the server. Removing either is a change to the security model. - **The protocol is specified in [PROTOCOL.md](PROTOCOL.md)**: format 1, which every client writes, and format 0 in an appendix, which instances read and accept for all of 1.x. A change to the format, the payload, a read or upload endpoint, a limit a client must respect, or an error code goes into PROTOCOL.md in the same PR, with new test vectors when the bytes change. - **Errors** answer problem details (RFC 9457) with a stable `code`: `problem()` in middlewares, `NoteError` in the notes service (turned into a `ProblemException` with its status by `routes/notes/errorStatus.ts`), and the route schemas' `defaultHook` for validation. A new code goes in PROTOCOL.md's "Errors" table. Never leak internal details. A 500 is logged with an `errorId`; refusals (4xx, 507) aren't logged. - **Docs describe the current version only**; history goes in `CHANGELOG.md`. A user-visible app change adds an entry under `[Unreleased]` in `CHANGELOG.md`. An SDK or CLI change adds a changeset (`pnpm changeset`). - **Tests**: 100% coverage of the gated `.ts` files (see [Testing](#testing)). New code comes with tests. - **Never commit** `.env` (it holds `SERVER_ENCRYPTION_KEY`) or `./data/`. - **Commits** follow Conventional Commits. Merging to `main` deploys production. ## Commands ```bash pnpm install # install dependencies pnpm dev # API on :3000 + Vite dev server on :5173 (proxies /api) pnpm lint # Biome lint + format check (pnpm lint:fix to fix) pnpm typecheck # TypeScript strict, all packages pnpm test # all tests (vitest) pnpm test:coverage # same, with the 100% coverage gate CI enforces pnpm test:e2e # Playwright end-to-end tests (builds the web app first) pnpm build # production build, all packages make check # lint + typecheck + test in Docker (docker-compose.dev.yml) ``` Schema change: edit `apps/api/src/db/schema.ts`, run `pnpm db:generate` in `apps/api`, and add the emitted SQL as a new `MIGRATIONS` entry in `apps/api/src/db/migrations.ts`. ## Environment - **Node.js 26 or later.** The code uses `Error.isError` and `node:sqlite`; older Node fails at runtime (`Error.isError is not a function`). pnpm workspaces, ES modules throughout. - In the Claude Code app's container (no Docker image), Node defaults to an older version and nvm lives in `/opt/nvm`. Before running anything: ```bash export NVM_DIR="/opt/nvm" && . "$NVM_DIR/nvm.sh" nvm install 26 && nvm use 26 pnpm install # again, so native deps build against Node 26 ``` Re-source nvm in each new shell: environment variables don't persist between commands. - The API reads its config from the environment (no dotenv): `pnpm dev` needs `SERVER_ENCRYPTION_KEY` exported. SQLite data goes in `./data/`. ## Repository map **`apps/api`**: Hono server, Node 26. - `src/notes/`: the notes' rules, with no HTTP. The infrastructure (db, storage, quota, pending deletions) never imports it. - `service.ts`: `createNotesService`: create, check, read, delete, upload sessions (`uploads.ts`); what spends a read, what burns a note. `read`/`stream` serve format 0, `readWithProofs` format 1. - `rows.ts`: the only place rows are built and read: placement, read spending, password attempts (counted before comparing, given back on a match, the fifth wrong one deletes the note), `removeNote` (row deleted and purge queued in one transaction). - `formats.ts` (what each format declares), `serverLayer.ts` (AES-GCM layer, verifier sealing), `padding.ts`, `stream.ts`, `limits.ts`, `errors.ts` (`NoteError`). - `src/routes/`: thin handlers. Every route goes through `app.openapi`, with its schema in `routes/notes/openapi-routes.ts`. Write auth (proof of work or API key) is middleware on the write routes, before validation. - `src/middleware/`: rate limits (`rateLimit.ts`, client address from `ADDRESS_HEADER`/`XFF_DEPTH` or `TRUSTED_PROXIES`), security headers (`security.ts`), problem details (`problem.ts`), request id and logs (`requestContext.ts`). - `src/site/`: serves `apps/web/build/` from memory. `load.ts` refuses to start on a bad build, or an `en.json` missing a head message (`HEAD_MESSAGES`). `shell.ts` renders `index.html` per page × language × theme (head, meta, shell data); `scripts.ts` lists inline scripts' hashes for the CSP. Without a build, the API serves itself alone. - `src/db/`: SQLite (`node:sqlite`) + Drizzle. Migrations run at boot (`PRAGMA user_version`); a database newer than the build refuses to start. - `src/storage/`: `StorageBackend` (local, S3), always async, reached through the Hono context, never imported by routes. - Jobs: `cleanup.ts` (expired notes), `pendingDeletions.ts` (purge outbox), `reconcile.ts` (daily orphan sweep, only on storage this database claimed). - Assembly: `config.ts` (`parseConfig`, `readSecret` for `NAME_FILE`), `app.ts` (`createApp`), `instance.ts` (`startInstance`), `index.ts` (bootstrap only). Also `backup.ts`, `metrics.ts`, `logger.ts`. **`apps/web`**: SvelteKit 3 static SPA (Svelte 5, Tailwind CSS 4). - adapter-static and `ssr = false`, configured in `vite.config.ts` (SvelteKit 3 refuses a `svelte.config.js`). - `src/app.html` carries `SHELL_HTML_TAG` and `SHELL_HEAD_MARKER`, which the API fills per request. The page starts from the shell data (`lib/shell.ts`, from `hooks.client.ts`). Pages only set `