--- title: "ADR 0004 — `ProjectContext` lives in `apps/worker`, not in `packages/core`" type: decision tags: [signpost, adr, decision] proposed_by: tok_bc09b340db294c709763789154d8113d model: kmem-docs-publisher extracted_at: "2026-10-08T22:37:43.934Z" --- # ADR 0004 — `ProjectContext` lives in `apps/worker`, not in `packages/core` **Status:** accepted **Date:** 2026-08-08 ## Decision `ProjectContext` — the resolved per-request handle threaded as the first argument to every data-access function — is defined in `apps/worker`, where the Cloudflare types it carries already exist: ```ts // apps/worker/src/context.ts export interface ProjectContext { projectId: string; // always "default" in v1 db: DrizzleD1Database; bucket: R2Bucket; vectors: VectorizeIndex; ai: Ai; } ``` `packages/core` does not import it and does not know it exists. Where core needs a capability it cannot compute for itself, it declares a **narrow function type** in `src/types.ts` and the caller passes an implementation: ```ts export type Sha256 = (input: string) => Promise; export type Clock = () => number; export type TokenCounter = (text: string) => number; ``` That is the entire port surface. There is no repository interface, no storage abstraction, no `ProjectContextPort`. `packages/db` follows the same rule: it exports the Drizzle schema and the row types inferred from it, and nothing else. `drizzle(env.DB, { schema })` is called in `apps/worker`. This keeps the schema importable from the CLI, from tooling, and from tests that have no Worker around them. ## The tension PRD section 3.6 says every data-access function takes a `ProjectContext` first, and section 3.2 says `packages/core` has zero Cloudflare imports. Read literally together, those two rules conflict: `ProjectContext` carries `D1Database`, `R2Bucket`, `VectorizeIndex` and `Ai`, all Cloudflare types. ## Alternatives rejected **Put `ProjectContext` in `packages/core` and import `@cloudflare/workers-types` there.** This breaks the zero-Cloudflare rule outright. It also breaks it for a reason that gains nothing: core's functions are pure, so none of them would ever read `ctx.db`. **Define a full port layer in core — `NoteStore`, `VectorStore`, `Embedder` — and have `ProjectContext` implement it.** This is the textbook hexagonal-architecture answer and it is the wrong one here. It creates a second interface for every storage operation, kept in sync by hand, whose only implementation is the Cloudflare one. The PRD is explicit: do not add abstraction for hypothetical futures. An interface with exactly one implementation is a rename with extra steps, and it makes every stack trace one hop longer for no benefit. **Make core generic over a context type parameter.** Viral generics through the parser, the chunker, the fusion and the packer, so that pure functions can carry a handle they never use. No. ## Why this resolution satisfies the PRD's intent The reason section 3.6 exists is stated in the PRD itself: *"It exists so the future cloud version can resolve it per-request without rewriting every query. Retrofitting it later means touching every file."* The cost being avoided is a mechanical edit across every function that touches storage. Every function that touches storage lives in `apps/worker`. `packages/core` touches no storage — it is parser, chunker, RRF fusion, freshness decay, semantic dedup and budget packer, all pure functions over plain data. Threading a context through them would not future-proof anything, because there is nothing there to scope to a project. So the rule holds where it does work and is absent where it would be ceremony. `ProjectContext` is the first argument of every data-access function, all of which are in `apps/worker`. The zero-Cloudflare rule holds in `packages/core` without exception. Both PRD requirements are met, and neither is met by weakening the other. ## Consequences - Biome enforces the boundary rather than trusting review: an import of `cloudflare:*`, `@cloudflare/*`, `agents`, `drizzle-orm`, `hono` or `@signpost/db` from `packages/core/src` is a lint error, and the message says what to do instead. - `packages/core`'s tsconfig sets `types: ["node"]`. That is for ambient Web Crypto — `crypto.subtle` for block-id hashing — and the standard globals both Node and workerd implement. Importing a `node:*` builtin from core is a separate lint error (`correctness/noNodejsModules`). - Pure functions that need time, hashing or token counting take them as parameters. That is not only a purity concession: it is what lets freshness decay be tested at a fixed instant and the budget packer be driven with a counter whose output is predictable. - When the hosted version arrives, `ProjectContext` gets resolved from the request instead of from config. The signature of every function in `apps/worker` already accepts it, so that change is local to one resolver. `packages/core` is unaffected, because it was never project-scoped. - If a future pure function genuinely needs a capability — a random source, say — it gets one more narrow function type in `types.ts`. The port surface grows one line at a time, only when something actually needs it.