# `@suaveplan/api-db-worker-proxy` — worker-side proxy to main-process DB adapter **Package**: `@suaveplan/api-db-worker-proxy` **Status**: PROPOSED **Type**: new-package **Stage**: 3 (Worker isolation infrastructure) ## Why Per design doc §5.2a, every capability ships two callable surfaces. `db` is special because (a) its operations return result sets that can be huge (favoring streaming over buffering), (b) its operations are transactional (transaction handles must be tied to a single connection lifetime that the worker can't naively shuttle around), and (c) tenant-scoping is a security boundary the worker MUST NOT be able to circumvent. `api-db-worker-proxy` provides the worker-side surface of `AdminDatabaseLike` / `TenantScopedDatabaseLike` whose method calls marshal across `MessagePort` to the main-process DB adapter (which lives in `api-db-adapter`, Stage 4). The proxy preserves the `PluginContext` / `RequestContext` `db`-slot type contract: a `PluginContext`'s `db` is `AdminDatabaseLike`; a `RequestContext`'s `db` is `TenantScopedDatabaseLike`. Tenant scoping is enforced on the MAIN side before the call is dispatched — worker code cannot smuggle an admin DB into a request handler. ## What changes Net-new package. Worker-side only — the main-side counterpart lives in `api-db-adapter` (Stage 4). **ADDED exports:** - `./client` — `createDbProxyClient(port, { surface: "admin" | "tenant-scoped" })`: returns a `WorkerProxyOf` or `WorkerProxyOf` typed per surface. - `./cursor` — DB cursors marshalled as `ReadableStream` via `api-worker-rpc`'s streaming primitive. Worker-side `for await (const row of cursor)` is identical to direct usage. - `./transaction` — transaction handle proxy: `client.transaction(async (tx) => { ... })` opens a transaction on the main side; the worker-side callback gets a `tx` whose methods route through a transaction-scoped sub-port. Calls outside the callback throw `DB_PROXY_TX_OUT_OF_SCOPE`. - `./error-codes` — `DB_PROXY_*` codes (`DB_PROXY_PORT_CLOSED`, `DB_PROXY_TX_OUT_OF_SCOPE`, `DB_PROXY_CURSOR_AFTER_CLOSE`, `DB_PROXY_SURFACE_MISMATCH`). - `./testing` — paired `MessageChannel` + a stub main-side server for unit tests; conformance harness `runDbProxyClientConformance` for downstream adapters. ## Out of scope - **Main-side adapter implementation** — `api-db-adapter` (Stage 4) and `api-db-sqlite-memory` (Stage 5) implement the receiving side. - **Tenant scoping enforcement** — enforced on the main side BEFORE marshalling. This package is the transport, not the policy. - **Engine-specific SQL** — `api-db-worker-proxy` is engine-agnostic; the main-side adapter resolves the engine (`postgres` / `sqlite` / `surrealdb`). - **Schema migrations** — host metadata migrations live in `api-host`; plugin migrations live in `api-db-adapter`. ## Source mapping Genesis-original. Design doc sections: §5.1 (two-context separation of `db` slot), §5.2a (worker-RPC variants), §6 (capability scoping in step 7). | Requirement | Design doc section | |---|---| | Two surfaces: `AdminDatabaseLike` vs `TenantScopedDatabaseLike` | §5.1 | | Cursors streamed via `Transferable` | §5.2a | | Worker-side proxy preserves direct-surface ergonomics | §5.2a | | Tenant scoping enforced main-side, not worker-side | §6 | ## Verification approach - `src/client/client.spec-conformance.test.ts` — `expectTypeOf` proves admin surface and tenant surface are distinct; admin surface NOT assignable to tenant-scoped. - `src/cursor/cursor.test.ts` — `for await` over large result set; consumer cancel propagates to main side. - `src/transaction/transaction.test.ts` — `tx` only valid inside callback; out-of-scope call throws `DB_PROXY_TX_OUT_OF_SCOPE`. - `tests/conformance/run-db-adapter-conformance.test.ts` — `runDbAdapterConformance("worker-proxy", makeFactory)` from `@suaveplan/api-db` passes against this proxy + a stub main-side server. **Required before Stage 4 can complete** — every Stage-4 DB adapter MUST pass conformance under BOTH the direct and worker-proxy surfaces. ## Migration impact None — additive only.