Files
suaveplan/openspec/archive/2026-05-25-api-tenant-provider/proposal.md
2026-05-27 23:29:53 +01:00

4.5 KiB

@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<TenantRecord> (stream-friendly for large tenant counts); count(): Promise<number>; lookup(tenantId): Promise<TenantRecord | null>; dispose(): Promise<void>.
  • 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)