# `@suaveplan/api-tenant-provider` — abstract tenant enumeration contract **Package**: `@suaveplan/api-tenant-provider` **Status**: PROPOSED **Type**: new-package **Stage**: 1 (Contracts) — see umbrella `openspec/changes/api-plugin-foundation/` ## Why `perTenant: true` cron / queued jobs (design §5.4) need a way to list every tenant the host knows about so the scheduler can fan one job invocation out per tenant, each with a tenant-scoped context. The same enumeration is needed by background reconciliation, per-tenant cache warmers, and admin tooling. Different deployments source tenant lists from different places: an in-memory list (tests + simple setups), a database row, a directory service, a remote API. This package owns the abstract contract — concrete sources plug behind it (`api-tenant-provider-memory` in Stage 4; production adapters land alongside the host metadata DB or a plugin's own DB). Per design §5.4: "`tenant.provider` ships in v1 (in-memory reference impl + adapter slot) so retrofitting tenant enumeration into a stable contract doesn't have to happen later." `tenant.provider` is intentionally distinct from `tenant.resolver` (which extracts `tenantId` from a request — that contract lives in `api-types` / consumed by `api-tenant-resolver-*` adapters). ## What changes Net-new package. Contract + pagination shape + conformance harness. - ADDED: `./provider` — abstract `TenantProvider`: `list(opts?): AsyncIterable` (stream-friendly for large tenant counts); `count(): Promise`; `lookup(tenantId): Promise`; `dispose(): Promise`. - ADDED: `./tenant-record` — `TenantRecord`: `{ id: TenantId, displayName?: string, status: "active" | "suspended" | "archived", createdAt: string }`. Zod schema. Only `active` tenants are enumerated by default; `list({ includeSuspended: true })` surfaces those too. - ADDED: `./pagination` — `ListOpts`: `{ pageSize?: number, includeSuspended?: boolean, cursor?: string }`. The `AsyncIterable` MAY be page-driven internally; callers iterate without knowing. - ADDED: `./capability-id` — adds `"tenant.provider"` to the `CapabilityId` union. - ADDED: `./error-codes` — frozen `TENANT_PROVIDER_CODES`: `TENANT_NOT_FOUND`, `SOURCE_UNAVAILABLE`, `INVALID_CURSOR`. - ADDED: `./testing` — `runTenantProviderConformance(factory)` reusable harness. ## CapabilityId additions This package contributes `"tenant.provider"` to the `CapabilityId` union. Per-tenant jobs declared in manifest implicitly require this capability (the host resolves it automatically when `perTenant: true` is set — plugins don't need to list it manually, but it shows up in the registry). ## Out of scope - Concrete sources — `api-tenant-provider-memory` (Stage 4); DB-backed sources are deployment-specific. - Tenant CREATE / UPDATE / DELETE — that's an admin concern, not a foundation contract. - Tenant resolution from a Request — that's `tenant.resolver`, contract in `@suaveplan/api-types`. ## Source mapping Genesis-original. Source of design intent: `docs/superpowers/specs/2026-05-13-api-plugin-foundation-design.md`. | Requirement | Design doc section | |---|---| | `perTenant: true` fan-out reading from `tenant.provider` | §5.4 | | Provider ships in v1 with in-memory reference impl | §5.4 | | Status filter (`active` default) | Implicit from §5.4 (fan-out should not include suspended tenants by default) | ### Requirements with no source line | Requirement | Justification | |---|---| | `AsyncIterable` over `Array` | Future-proofs against large tenant counts; an array forces full-set load, an iterable streams. Standard genesis pattern. | | `runTenantProviderConformance` harness | Stage-5 deliverable (§13.4). | ## Verification approach - `src/provider/provider.spec-conformance.test.ts` — interface shape; iterator semantics. - `src/tenant-record/tenant-record.test.ts` — Zod accept/reject; `TenantId` brand preserved. - `src/provider/status-filter.test.ts` — default excludes `suspended`/`archived`; `includeSuspended: true` surfaces them. - `src/pagination/pagination.test.ts` — `pageSize` honored; `cursor` round-trips. - `src/capability-id/capability-id.spec-conformance.test.ts` — `"tenant.provider"` in `CapabilityId`. - `tests/dep-graph.test.ts` — deps: `@suaveplan/api-types`, `@suaveplan/error`, `@suaveplan/types`, `zod`. ## Migration impact None — additive only. ## Impact ### Affected dependents - (none) — [x] no action needed (reason: net-new; in-memory ref impl lands Stage 4)