# Spec Delta — api-host — foundation-fix-gap-4-db-split ## MODIFIED Requirements ### Requirement: `createHost(config)` composes every internal driver The package SHALL expose a top-level `createHost(config: HostConfig)` entry point that composes the lifecycle driver, reload driver, plugin ledger, capability registry, request pipeline, and admin API into a `Host` whose `fetch(request)` method drives the full §6 pipeline. Config SHALL be Zod-validated at the entry; failures raise `HOST.CONFIG_INVALID`. `host.start()` SHALL topo-sort `config.plugins` via `@suaveplan/api-manifest`'s `resolveInstallOrder` and install each plugin in dependency order. `host.stop(graceMs)` SHALL stop plugins in reverse-topo order with a `HOST.SHUTDOWN_TIMEOUT` deadline. The host SHALL mount the admin API at `config.adminPrefix` (default `/admin`, `null` to disable). It SHALL expose `/internal/livez` (200 when responsive) and `/internal/readyz` (200 + plugin list). `installPlugin`/`uninstallPlugin` SHALL operate at runtime; pre-start fetches SHALL return a 503 envelope. `HostAdapters` SHALL distinguish the host-private narrow SQLite ledger handle from the consumer-facing multi-engine database surface: - `HostAdapters.ledgerDb: LedgerDb` — REQUIRED. Narrow `bun:sqlite` / `better-sqlite3`-style handle (`prepare(sql)` / `run(sql)`) consumed exclusively by the host's internal `PluginLedger`. NOT exposed on `PluginContext` / `RequestContext`. - `HostAdapters.db?: AdminDatabaseLike` — OPTIONAL. The consumer-facing multi-engine façade produced by `createDbAdapter(...).admin` (`@suaveplan/api-db-adapter`). When set, the host SHALL thread the admin surface onto `PluginContext.db` AND per-request SHALL apply `createTenantScopedWrapper(admin, tenantCtx.id)` and expose the result on `RequestContext.db`. When unset, BOTH `PluginContext.db` AND `RequestContext.db` SHALL be `null`. When the per-request tenant resolver chain returns `null`, `RequestContext.db` SHALL be `null` even if `adapters.db` is configured. The two slots are type-incompatible by design and SHALL NOT be unified. #### Scenario: empty host boots; livez/readyz respond - **WHEN** `createHost({adapters, plugins: []})` + `host.start()` runs - **THEN** `host.fetch(GET /internal/livez)` returns 200 AND `host.fetch(GET /internal/readyz)` returns 200 with `plugins: []`. #### Scenario: admin disabled via `adminPrefix: null` - **WHEN** `adminPrefix: null` is configured - **THEN** a fetch to `/admin/plugins` is treated as an unknown plugin route and returns 404. #### Scenario: start twice raises HOST.ALREADY_STARTED - **WHEN** `host.start()` is called twice - **THEN** the second call rejects with `code: "HOST.ALREADY_STARTED"`. #### Scenario: stop before start raises HOST.NOT_STARTED - **WHEN** `host.stop()` is called before `host.start()` - **THEN** the call rejects with `code: "HOST.NOT_STARTED"`. #### Scenario: adapters.db is threaded to ctx.db with tenant-scoped wrapper - **WHEN** the host is booted with `adapters.db = createDbAdapter(...).admin` AND a tenant resolver that binds `{id: "acme", ...}` AND a route handler reads `ctx.db?.query("SELECT * FROM widgets", [])` - **THEN** the SQL the admin surface sees is rewritten to `SELECT * FROM widgets WHERE tenant_id = ?` AND the appended bind value is `"acme"` AND `ctx.db.tenantId === "acme"`. #### Scenario: ctx.db is null when adapters.db is absent - **WHEN** the host is booted WITHOUT `adapters.db` AND a route handler reads `ctx.db` - **THEN** the value is `null` AND the handler runs to completion. #### Scenario: ctx.db is null when no tenant resolved - **WHEN** the host is booted WITH `adapters.db` AND the tenant resolver chain returns `null` AND `defaultTenant` is unset - **THEN** `RequestContext.db === null` at the handler boundary. **Verified by:** `src/host/host.test.ts::"boots with zero plugins + serves /internal/livez 200"` AND `src/host/host.test.ts::"/internal/readyz lists empty plugin set"` AND `src/host/host.test.ts::"adminPrefix: null disables admin surface"` AND `src/host/host.test.ts::"start twice → HOST.ALREADY_STARTED"` AND `src/host/host.test.ts::"stop before start → HOST.NOT_STARTED"` AND `src/host/host.test.ts::"validates config via Zod"` AND `src/host/host.test.ts::"adapters.db threaded into RequestContext via tenant-scoped wrapper"` AND `src/host/host.test.ts::"ctx.db === null when adapters.db is undefined"` AND `src/host/host.test.ts::"ctx.db === null when tenant resolver returns null even if adapters.db is set"`