# Blueprint: `community`

> 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 feed UI, route handlers, follower-graph tables, or a posts
> schema here — stop, that is the generic-platform trap. Those are host-owned.

## Preset

`registry.json → presets.community` =
`util · db · mail · auth · realtime · notifications · search · uploads · audit`

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

## The worked flow

A **member publishes a post with an image attachment**:

```
auth (may this caller post?)  →  uploads (is this attachment really an image?)  →  gate
    →  search-index  +  audit-log  +  notifications fan-out (email channel composes mail)  +  realtime envelope
```

The composition proof lives in `apps/consumer/tests/blueprint-community.test.ts` — one flow, three
cases (valid publish · forged attachment · unauthorized caller). The security property is **fail-closed
composition across TWO boundaries** (one more than saas-admin's single gate):

1. **auth capability gate** — `requirePermission('post:create')`; a reader without the capability is
   rejected here, *before* the attachment is even inspected.
2. **upload trust boundary** — `uploads/magic-bytes` sniffs the real content-type and
   `uploads/size-limit` caps the size; a forged attachment (a script-bearing SVG posing as `image/png`)
   is rejected here.

A failure at **either** gate produces **zero** downstream side effects — nothing indexed, audited, or
announced. That ordering (both gates before any side effect) is exactly what the two negative cases
assert.

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

| Preset module | Role in this blueprint | Wiring file |
|---|---|---|
| `auth` | resolve caller identity (`getSession`); enforce the publish capability (`requirePermission`) | `wiring/auth.ts` |
| `uploads` | **trust boundary** — `magic-bytes` sniff + `size-limit` cap (subpath-only module; no barrel) | `wiring/uploads.ts` |
| `search` | index the published post; make it findable through `searchEntities` (one 'post' provider) | `wiring/search.ts` |
| `notifications` | fan out to followers across channels (preferences + per-channel dedup) | `wiring/notifications.ts` |
| `mail` | the 'email' delivery **channel** that `notifications` composes ABOVE it — not wired standalone | `wiring/mail.ts` |
| `audit` | record the published fact (`logAudit`) + read it back (`listAudit`), single-scope (`tenant_id` null) | `wiring/audit.ts` |
| `realtime` | **type-only** — build the typed `RealtimeEvent` envelope the host publishes; `/server` delivery (built, Workers-locked) is host-wired, out of this recipe's scope. Like saas-admin's `ledger` seam: NAMED, not exercised at runtime. | `wiring/realtime.ts` |
| `util` | transitive L0 leaf (`search` imports `util/fts` to sanitize the tsquery); 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 seven 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 post index, capture mail adapter, in-memory preference/dedup stores) — just the
  instantiation seam, nothing more (adapter-minimalism, CLAUDE.md §4).
- **Host-owned (NAMED, not implemented):**
  - **Feed / composer UI** = a host-copied shadcn block over Radix primitives (ui-primitives are KILLED —
    the consumer brings Radix; delivery-stack §3). The blueprint ships no UI.
  - **Routes / endpoints** = host web-standard `Request`/`Response` handlers that call the same
    composition `runMemberPublish` models.
  - **The post entity, the follower graph, the FTS columns/index** = host domain schema. The blueprint
    represents a post only as an audited + indexed + announced fact, never a table.
  - **Realtime delivery** = the host's Durable Object / Queue consumer behind `@platform-modules/realtime/server`
    (intentionally unbuilt). The blueprint builds the wire envelope; the host delivers it.
  - **The real AuthEngine, R2/S3 storage, FTS provider, mail provider, push/SMS channels, KV dedup** =
    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 modules ship) ·
styled UI / routes / entity schemas · a generator.

## Convergence note (delivery-stack §7 — generator readiness)

This is the **second** hand-built blueprint. It converges with `saas-admin` on the scaffold shape:
identical 4-artifact layout (`recipe.md` + per-module `wiring/*.ts` + one composition test +
`package.json` rows), structurally identical `auth`/`mail`/`audit` fakes, and the same fail-closed
"gate(s)-before-side-effects" test spine. The two **divergences** are meaningful and must inform any
generator: (1) capability source — tenancy-scoped (saas) vs session-direct (community); (2) gate count —
one (saas) vs two (community, because `uploads` adds a trust boundary). A generator is earned only after
this convergence is confirmed against the remaining blueprints (commerce, marketplace), per §7 — not yet.
