# Wasp-like Template — Capability Map (reusable; map once, query forever)

**Audience: AI coding agents + the `create-mod-app` scaffolder first.** Optimize reads for activation, not prose. **This is a DERIVED map, not a new donor census** — it re-views three existing sources through one lens (*"what does a Wasp/OpenSaaS-class full-stack template need, and what is missing?"*). It carries **zero new donor exploration**; every donor fact is sourced from the censuses below. Do **not** re-explore donors to answer a question this map already answers.

> **Purpose — kill the re-map.** Answer *"which modules does a Wasp-like product need · have we got it · where do we port it from · what is still missing"* in one read. Before spawning any exploration agent for a Wasp-like / full-stack-starter question, read this first.

## 0. Sources + freshness (derive, never re-census)

This map = a lens over three SoT artifacts. Its freshness = **their** freshness (no independent pins):

| Source | Side | Gives this map |
|---|---|---|
| `docs/registry.json` (+ prose `docs/specs/2026-06-12-module-registry.md`) | output | module list · layer · status · presets |
| `docs/specs/2026-06-16-donor-capability-inventory.md` | input | "who has X?" rollup + best port source (repo-level pins) |
| `docs/catalog/2026-06-16-ui-donor-census.md` | input | UI engine/token/variant convergence (path-scoped pins) |
| `docs/specs/2026-06-16-delivery-stack.md` | doctrine | the stage ladder (headless → react-sibling → ui → template → blueprint) |

**Refresh rule:** stale only if a source above is stale. Re-derive the *changed row*, not the whole map (`docs/catalog/index.md §3`).

## 1. BLUF — the three findings

1. **Backend capability is COMPLETE.** Every Wasp/OpenSaaS backend feature maps to a `shipped` module (§2). **Zero new backend modules needed.**
2. **The gap is the front-end delivery layer** — `ui-*` family (×3 packages, gated) + `10` missing `@platform-modules/<core>-react` siblings (§3; only `realtime-react`+`i18n-react` ship). All **port-from-donor**, all already dispositioned. This is the real backlog.
3. **Two from-scratch capability gaps** (everything else ports):
   - (a) **`content`** — post/page corpus + taxonomy (categories/tags) + feeds, for Blog + CMS. No module; **no clean donor** (multideal gives page-builder/editor *primitives* to harvest — `RichTextEditor`/`PublishDialog`/draft-publish/`rich-text-content` blocks — **not** a corpus). → **BUILD thin, make-with-harvest, Blog blueprint as first (non-donor) consumer** (§5A). This is the real new build.
   - (b) **typed end-to-end RPC** (Wasp's signature) — zero donor convergence + imposes a framework → **DEFER, not a module** (§5).

## 2. Wasp / OpenSaaS → platform crosswalk (backend = done)

Feature parity vs Wasp core + the OpenSaaS template. **Status from `registry.json` (SoT) — do not copy status here long-term; re-query if stale.**

| Wasp / OpenSaaS feature | Platform module | Status (registry) |
|---|---|---|
| Auth — email/pass, social/OAuth, verify, reset, 2FA | `auth` (better-auth + custom engine + otp-email) | shipped |
| Entities / ORM / migrations | `db` + `record-store` | shipped |
| Queries + Actions (async jobs/cron) | `jobs` (async + outbox; cron = host trigger feeding it — `@platform-modules/cron` killed) | shipped |
| Email sender (SendGrid…) | `mail` | shipped |
| Payments (Stripe / Lemon) | `billing` | shipped |
| AI (OpenAI) | `ai` | shipped |
| File upload (S3) | `uploads` | shipped |
| Websockets | `realtime` (+ `realtime-react`) | shipped |
| Admin / audit trail | `audit` | shipped |
| Blog/CMS metadata (sitemap · RSS · canonical · JSON-LD · hreflang) | `seo` (**metadata only — NOT content**) | shipped |
| Blog/CMS content (posts/pages · draft-publish · taxonomy · feeds) | **`content` — MISSING** (net-new; build thin, §5A) | gap |
| Analytics | partial — `audit` CF-Analytics-Engine sink + `ai/usage` residue; product-analytics provider = host config, NOT a module |
| **No Wasp equivalent (platform extra)** | `tenancy` `tax` `ledger` `search` `notifications` `i18n` `helpdesk` `marketing` `cache` | shipped |

**Typed RPC client↔server** — Wasp's killer feature — has **no row**: §5.

## 3. Front-end delivery backlog (THE gap — all port-from-donor)

**Rule: a full-stack template renders screens; screens need (a) the `ui-*` kit + (b) a per-module `@platform-modules/<core>-react` behaviour sibling** (`delivery-stack.md §2`). Only `i18n-react` + `realtime-react` ship. Everything below is missing.

**Core-status correction (load-bearing): `delivery-stack.md §3.1` lists `auth`/`billing`/`audit`/`search`/`ai` as `designed`/`pilot` — STALE. `registry.json` now says all `shipped`.** Consequence: the `-react` siblings are **unblocked on the core side**; the ONLY remaining gate per sibling is authoring the sibling itself (+ `ui-*` for styled screens). The §3.1 "zero families buildable today" verdict no longer holds for the core half.

### 3.1 `ui-*` family — 3 packages (build-GATED, not status-blocked)

Flat family on the framework/presentation axis (umbrella `ui-react` KILLED — do not re-propose; `delivery-stack.md §3`). Adopt Radix internally; do not reimplement primitives (accessibility = hard floor, already solved upstream).

| Package | Carries | Port source (donor) |
|---|---|---|
| `ui-tokens` | engine-agnostic token contract (the swap-survival layer) | net-shape; tokens abstracted from zync/ForumZone/vibeflare |
| `ui-util` | `cn` / variants / direction / focus | zync `packages/ui`, ForumZone `packages/ui` |
| `ui-primitives` | accessible React on adopted Radix, token-styled | zync (full `@radix-ui/*` + `cmdk`) · ForumZone (full set + composites + layout + admin) · vibeflare (Astro islands) |

Gate = publish path + ≥1 more `-react` sibling stable (`ui-product-brief.md`). Convergence strong (2/3 donors on Radix; ForumZone hand-rolled = the outlier).

### 3.2 `-react` siblings — full backlog (donor source + per-class priority)

Priority key: **P1** = critical path for that class · **P2** = needed · **P3** = nice-to-have/admin. Class cols: **S**aaS · **C**ommunity · co**M**merce.

| Sibling | Behaviour it ships | Port source (donor) | S | C | M |
|---|---|---|:--:|:--:|:--:|
| `auth-react` | sign-in/up/reset/verify/social screens + session hook + api-key UI | zync (oauth/2fa/api-keys) · multideal | P1 | P1 | P1 |
| `billing-react` | pricing table · checkout button · portal link · sub-status | multideal (Stripe+Connect) · trance (Sumit IL) | P1 | — | P1 |
| `tenancy-react` | org switcher · member/role admin · invite flow | zync (RBAC triad) · ForumZone | P1 | P2 | P2 |
| `search-react` | search box · results · facets | multideal (federation+facets) | P2 | P1 | P1 |
| `notifications-react` | bell · list · prefs · web-push subscribe | zync (email+webpush+telegram one seam) · multideal | P2 | P1 | P2 |
| `uploads-react` | dropzone · progress · presign flow | multideal (upload queue) · trance (R2 presign) | P2 | P2 | P1 |
| `ai-react` | chat UI · SSE/stream hook · prompt input | multideal (runner+registry) · fewtok (tokenizer/sse) · vibeflare | P2 | P2 | P3 |
| `audit-react` | audit-log viewer (admin) | zync (partitioned + redact) | P3 | P3 | P3 |
| `helpdesk-react` | ticket form + thread | multideal (polymorphic) | P3 | P3 | P3 |
| `marketing-react` | newsletter subscribe form | trance · ForumZone | P3 | P3 | P3 |
| `content-react` | post/page reader · author editor (rich-text) · taxonomy nav · feed | multideal (`RichTextEditor`/`PublishDialog` — harvest) | plugin¹ | plugin¹ | plugin¹ |
| `realtime-react` | presence/channel hooks | **SHIPPED** | ✓ | ✓ | ✓ |
| `i18n-react` | translation hooks/provider | **SHIPPED** | ✓ | ✓ | ✓ |

**Critical-path-first build order (P1 union):** `auth-react` (all classes) → then per target: SaaS = `billing-react` + `tenancy-react`; Community = `search-react` + `notifications-react`; Commerce = `billing-react` + `search-react` + `uploads-react`. `ui-*` family is the shared prerequisite for any *styled* screen (un-styled headless hooks can ship first).

¹ `content-react` is a **plugin sibling**, not a base-class sibling — its priority is *per app that adopts the Blog/CMS plugin*, not per app-class. It rides on top of any preset (§5A).

## 4. Start-small, expandable harvest (R5 capability altitude)

**Rule: each module = THIN CORE seam (build/shipped) + IMPORTABLE mature capability harvested as subpaths (opt-in) + HOST-owned app-domain (never a module).** Harvest mature donor capability as importable subpaths; never discard it to the host as "app-specific" (`CLAUDE.md §3` R5; `coding-standard.md §5A`). YAGNI bars *inventing* surface; R5 bars *discarding* mature surface. Freeze a ×1 capability's contract only when a **non-donor** consumer confirms it.

**This is the "start small, expandable" model:** ship the core seam now; the columns below are the **expansion menu already proven to exist in donors** — harvest on demand, don't re-explore to find them. Per-export surface lives in `registry.json` + the module's boundary spec — **point there, do not re-list here.** This table is the *altitude tag* only.

| Module | Core seam (start small — have) | Importable expansion (harvest from donor) | Host-owned (never module) |
|---|---|---|---|
| `auth` | session verify + access guard | api-keys (×2 zync+vibeflare) · oauth/social · 2fa · otp-email | user schema · role policy |
| `billing` | charge/refund + webhook idempotency | Connect/marketplace split (multideal) · Sumit IL adapter (trance) · invoice-on-charge config (Pattern-A) | price catalog · plan policy |
| `jobs` | enqueue + outbox | CF-Queues consumer · DO-runner · db-poll (all present) | job definitions |
| `notifications` | one send seam | email + web-push + telegram channels (zync, one seam) · dedup/preference stores | message copy |
| `uploads` | presign + object-store seam | R2 presign (trance) · upload-queue (multideal) · image variants | bucket policy |
| `search` | query seam | federation + facets (multideal) | index schema |
| `ai` | completion adapter | runner + provider registry (multideal) · tokenizer/SSE (fewtok) · usage metering (×2) | prompts · domain chains |
| `tenancy` | tenant-scope seam | RBAC triad (zync) · flat + custom-domains (ForumZone) | org entities |
| `ledger` | append-entry idempotency | vesting (multideal) · balance-after (ForumZone) · revenue-split | account chart |
| `audit` | write-on-mutation | partitioned + redact + queue-sink (zync) | what-to-audit policy |

## 5. The one true gap: typed end-to-end RPC — DEFER, do not build as a module

Wasp's signature (declare server query/action → auto-generated typed React hook). Census result: **zero donor convergence** (no tRPC / typed-client / react-query data-layer worth porting — `donor-capability-inventory.md`, rollup has no RPC row).

**Verdict: NOT a module. Defer to the generator/scaffolder phase.** Reasons:
- Typed RPC couples client to a chosen server framework → violates the no-imposed-framework north-star (`CLAUDE.md §5`).
- Zero donors → WATCH at most, never build (`CLAUDE.md §3`).
- Its natural home is the **codegen that emits typed hooks against a host's chosen transport** — the deferred generator (`delivery-stack.md §6` rejects the generator until ≥2 hand-built blueprints converge).

DO NOT build a `@platform-modules/rpc`. Re-evaluate only when (a) a non-donor consumer needs it AND (b) the generator phase opens.

## 5A. `content` module + Blog/CMS as plug-in blueprints (the real new build)

**`content` = the second from-scratch gap (§1.3a) and the only net-new MODULE this map recommends building.** Everything Blog/CMS-shaped that ISN'T `content` already ships (`seo` metadata · `uploads` media · `search` index · `auth` authoring gate · `notifications` comments-notify · `realtime` live).

### 5A.1 `content` — scope honestly (make-with-harvest, NOT a clean extract)

- **Core seam (build thin, net-new):** content *entry* model — `id · slug · type(post|page) · body · status(draft|scheduled|published) · publishedAt · author · taxonomy(category[]|tag[])` + list/get-by-slug/publish transitions + feed projection (RSS/JSON). **No donor has this corpus** (multideal = page-builder, Press.zone = reference CMS/PHP non-portable, ForumZone = forum threads; inventory "taxonomy" hits = AI-classification, not content tags).
- **Importable expansion (harvest from multideal):** rich-text/MDX body handling, `PublishDialog`/draft-publish workflow, `rich-text-content` block primitives, scheduled-publish. Harvest into `content` subpaths + `content-react`, opt-in.
- **Host-owned:** which entity types exist beyond post/page, editorial policy, custom block types.
- **Grain:** ONE thin module (R5), do NOT pre-split into content/taxonomy/media — media IS `uploads` (exists); taxonomy is a `content` field, not a module.
- **Layer:** L2 (→ `db`; composes `seo`/`search`/`uploads` at the blueprint, not as hard deps — keep the base floor, §5A.3).
- **Build order:** spec the seam from **what Blog needs**, build thin, consume in the Blog blueprint immediately. Blog/CMS are the **non-donor consumers** that freeze the ×1 contract (`CLAUDE.md §3` freeze-rule).

### 5A.2 Blog vs CMS — both are BLUEPRINTS over one `content` primitive

| | Blog | CMS (reference-CMS parity, edge-capable) |
|---|---|---|
| Form | thin blueprint | richer blueprint |
| Composes | `content` + `seo` + `uploads` + `search` + `content-react` | `content` + `seo` + `uploads` + `search` + `auth` + `tenancy` + media library + page-hierarchy admin + `content-react` |
| New module? | **no** | **no** |
| Edge-capable | yes (every module host-agnostic, `CLAUDE.md §5`) | yes — same reason |

CMS = the *same* `content` primitive + a heavier admin/media/hierarchy surface. Neither is a module. (Same disposition as `commerce` → blueprint, §6.)

### 5A.3 The plugin model — compose now, extract the auto-wire contract later

**A drop-in plugin that self-wires (owns tables, mounts routes, injects nav/UI, registers config) IS the deferred scaffolder/manifest framework** (`delivery-stack.md §6` gates it until ≥2 hand-built blueprints converge). Do **not** author the framework on one example. Get plug-and-play **now** by composition; extract the contract from Blog-on-SaaS + a 2nd blueprint.

**Owner's four plugin requirements — how each is met on the compose-now path (split: code layer vs wiring layer):**

> **Amended 2026-06-17 (§8 B1+B3):** surfaces 4→**5**; manifest split by axis (core vs react presentation). See design spec §3.1/§3.2.

| Requirement | Code layer (free TODAY) | Wiring layer (manual now → auto later) |
|---|---|---|
| **1. Installable** | `pnpm add @platform-modules/content content-react` (versioned ESM pkg) | hand-wire route + nav + migration now; `create-mod-app --plugin blog` later |
| **2. Removable** | `pnpm remove` | clean iff plugin footprint is **isolated by convention**: namespaced `content_*` tables · own forward-only migration · own route file · own nav entry · own screens → delete the unit |
| **3. Updateable** | `pnpm up` + changesets/semver (per-package, exists) | forward-only namespaced migrations → update = run new, never edits base schema |
| **4. Fits different templates** (blog in SaaS *and* commerce) | — | **dependency discipline:** plugin depends only on the **base capability floor** present in EVERY preset (`db`·`auth`·`seo`·`uploads`·`search`), never on saas/commerce-specific modules |

**Design constraint baked in NOW (so the later framework just automates it):** every plugin contribution sits on **FIVE isolated, reversible surfaces** (amended 2026-06-17 from four — B3). Split by axis:
- **Core surfaces** (`PluginCoreManifest` — framework-agnostic, consumed by any host): **schema** (namespaced `content_*` tables · own forward-only migration) · **routes** · **config**
- **React presentation surfaces** (`PluginReactManifest` — `-react` sibling axis only, consumed by React hosts): **screens** · **nav**

Non-React hosts consume the core manifest only. Hand-wire blog so each surface is a single removable unit. Build **zero** of the loader/manifest until a 2nd plugin proves the contract shape.

## 5B. Plugin catalogue — dependency map + harvest source (machine surface: `registry.json → plugins`)

**This is the plugin-grain mirror of the module registry.** A *plugin* = a bolt-on feature added on top of any base preset (≠ preset = the base dep-bundle; ≠ blueprint = a full base app). **Disposition only — the plugin SYSTEM stays deferred** (§5A.3). Query `registry.json → plugins` for the machine form; this section is the rationale.

**Three-tier dep model (verified vs preset membership 2026-06-17):** `bundles` (plugin always brings) · `requires` (hard host seam — pnpm installs any the base lacks) · `optional` (enhance-if-present). **Cross-template fit** = a plugin depends on NO base-class-specific module beyond its `requires`. Preset floor truth: `db` = ALL presets · `auth`/`mail` = core/saas/community (**NOT commerce**) · `uploads`/`search` = commerce/community (**NOT core/saas**) · `jobs`/`tenancy` = saas-only · `ledger`/`billing`/`tax` = saas+commerce · `realtime`/`notifications` = community-only.

| Plugin | Form | Bundles | Requires (hard) | Optional | Harvest source | Status |
|---|---|---|---|---|---|---|
| **subscriptions** | module | `billing`(+/subscriptions)·`billing-react`·`entitlements`·`entitlements-react` + **subscription-tiers template** | db·auth | jobs·mail | billing: zync+ForumZone; entitlements: zync+ForumZone (×2 convergent); react siblings: **BUILD** | entitlements = **designed (BUILD-approved 2026-06-17)**; billing/subscriptions = designed; react siblings missing |
| **blog** | blueprint | `content`·`content-react` | db·auth·seo | search·uploads | content: multideal editor prims + **net-new** corpus/taxonomy/feeds | content = **BUILD** |
| **cms** | blueprint | `content`·`content-react` | db·auth·seo·uploads | search·tenancy·i18n | same as blog | content = **BUILD** |
| **helpdesk** | module | `helpdesk`·`helpdesk-react` | db·auth·mail | notifications·ai | multideal (polymorphic) | helpdesk shipped; react **build** |
| **marketing** | module | `marketing`·`marketing-react` | db·mail | — | trance·ForumZone | marketing shipped; react **build** |
| **integrations** | module | `integrations` | db·auth·jobs | — | zync (slack/jira/zapier/make) | **WATCH ×1** — do not build |
| **affiliate** | module | `affiliate` | db·ledger·billing | — | multideal | **WATCH** (money-class) |
| **invoicing** | module | `invoicing` | db·billing·tax·ledger | — | zync (AR-aging) | **WATCH ×2 divergent** |

**Correction (load-bearing):** an earlier draft claimed blog's floor is "present in both saas and commerce" — **false.** `commerce` has no `auth`; `saas` has no `uploads`/`search`. Blog's only *universal* hard req is `db`; `auth` is added on commerce; `search`/`uploads` are optional. Portability = req is universal OR declared+installable on any base — NOT "pre-present in every preset."

**Not plugins:** `api-keys` + outbound `webhooks` → harvest into `auth`/`util` subpaths, not standalone plugins (§6).

## 6. Census-flagged harvest opportunities (not yet modules — disposition pending)

**Census detail lives in `donor-capability-inventory.md → New findings` — point, don't copy.** Disposition verdicts only (the lens this map adds); **none promote to a module here** — recorded so they are not re-discovered:

| Candidate | Disposition (verdict only — detail in inventory) |
|---|---|
| `auth/api-keys` | forward-need (every public API) — harvest into `auth` as subpath, not a module |
| `webhooks` outbound delivery | inbound dissolution holds; **outbound may have 2nd consumer → flag re-disposition** |
| `integrations` (slack/jira/asana/zapier/make) | ×1 → WATCH; forward-need SaaS class but do NOT build at ×1 |
| `commerce` (cart/checkout/coupons) | **blueprint, not a module** — feeds commerce blueprint |
| content / page-builder (multideal `page-organizer`) | **harvest into `content` module** (§5A.1); page-builder blocks = importable expansion, not the core seam |
| `ticketing` (trance) | **EXCLUDED** — proprietary, not generic substrate (`registry.json → excluded`) |

## 7. What is NOT a module (host / scaffolder / blueprint — don't propose as modules)

Client routing · pages · custom API endpoints · deployment · admin-dashboard UI (= blueprint, host-copied templates) · cookie consent · DB seeding · analytics-provider wiring · the typed-RPC codegen (§5) · **Blog + CMS** (= blueprints over `content`, §5A.2) · **the plugin loader/manifest/auto-wire framework** (§5A.3 — deferred until ≥2 blueprints converge). These arrive via the **scaffolder + blueprint** phase the owner deferred, not the module layer. The one thing here that IS a module: `content` (§5A.1).
