# Blueprint: `saas-admin`

> Audience: AI coding agents first. A blueprint is a **worked example** in `apps/consumer`, not a shipped
> product (delivery-stack §4.1). It proves the preset's modules **compose**; it does not implement a
> product. If you find yourself adding styled admin UI, route handlers, or entity schemas here — stop,
> that is the generic-platform trap. Those are host-owned.

## Preset

`registry.json → presets.saas` =
`util · db · auth · tenancy · mail · jobs · tax · ledger · billing · audit`

The preset is import-closed (see `registry.json → presets._closure`). All ten are added to
`apps/consumer/package.json` devDependencies so this blueprint also proves the **preset installs**.

## The worked flow

A **tenant-scoped admin suspends a member**:

```
auth (who is calling?)  →  tenancy (what may they do IN THIS tenant?)  →  gate  →  audit + mail + jobs
```

The composition proof lives in `apps/consumer/tests/blueprint-saas-admin.test.ts` — one flow, three
cases (permitted · cross-tenant · unauthenticated). The security property is **fail-closed
composition**: capabilities are tenant-scoped (`tenancy.resolveCapabilities` returns `[]` unless the
caller is an active member of an active tenant), so a caller acting on a tenant they don't belong to,
or with no session, is rejected at the gate **before any audit row, email, or job is produced**.

## Module roster — wired-and-exercised vs deferred

| Preset module | Role in this blueprint | Wiring file |
|---|---|---|
| `auth` | resolve caller identity (`getSession`); enforce the capability (`requirePermission`) | `wiring/auth.ts` |
| `tenancy` | scope capabilities to `(user, tenant)`; the cross-tenant isolation seam | `wiring/tenancy.ts` |
| `audit` | record the privileged action (`logAudit`) + read it back (`listAudit`) | `wiring/audit.ts` |
| `mail` | notify the suspended member — touched once past the gate | `wiring/mail.ts` |
| `jobs` | enqueue async cleanup (at-most-once via idempotency key) — touched once | `wiring/jobs.ts` |
| `billing` | stamp a proration-refund **intent** (`idempotencyKey`/`toMinorUnits`) — pure helpers | `wiring/billing.ts` |
| `tax` | **deferred** — Pattern-C, host resolves the taxed amount pre-charge; this admin flow computes no tax. In the preset for app-class completeness, not exercised here. | — |
| `ledger` | **deferred** — Pattern-B (ledger ABOVE billing). The full `settleCharge`/`refundCharge` path that injects a `LedgerSeam` is the **commerce blueprint's** headline flow; re-running it here would be the generic-platform trap. `wiring/billing.ts` re-exports `LedgerSeam` to NAME the injection point. | — (commerce) |
| `util` | transitive L0 leaf (imported as a `dependency` by auth/search); not directly wired | — |
| `db` | substrate — the `Querier` audit writes through; stood up in `wiring/audit.ts` | (in audit wiring) |

## Blueprint-owned vs host-owned boundary

- **Blueprint-owned (here):** the six per-module wiring seams + the composition test that orchestrates
  them. The wiring files instantiate each module's REAL exports with consumer-side fakes (fake
  `AuthEngine`, in-memory tenancy store, capture mail adapter, in-memory idempotency store) — just the
  instantiation seam, nothing more (adapter-minimalism, CLAUDE.md §4).
- **Host-owned (NAMED, not implemented):**
  - **Admin shell UI** = a host-copied shadcn *dashboard* block over Radix primitives (ui-primitives are
    KILLED — the consumer brings Radix; delivery-stack §3). The blueprint names it; it ships no UI.
  - **Routes / endpoints** = host web-standard `Request`/`Response` handlers that call the same
    composition `runAdminSuspend` models.
  - **The member entity + its `status` mutation** = host domain schema. The blueprint represents the
    suspension only as the audited fact, never a table.
  - **The real AuthEngine, RBAC table, mail provider, queue consumer, KV idempotency** = host adapters
    injected where the fakes sit.

## What this blueprint is NOT

A package · a published artifact (blueprints are NEVER in the publish set — only the ten modules ship) ·
styled UI / routes / entity schemas · a generator. Per delivery-stack §7, a generator is earned only
after a **second** hand-built blueprint (community) converges on the same scaffold shape — not before.
