---
name: platform-embed-module
description: >-
  Embed @platform-modules/* package into consuming project — pick install vehicle (local
  workspace link for tests, GitHub Packages for adoption), resolve peer deps, wire only seam
  module's integration axes require (adapter registration · host stores · DB migration · React provider ·
  endpoint mount), then prove swap-survival parity against host's existing hand-rolled code and leave
  rollback. Use when user wants adopt, install, integrate, wire up, drop in, vendor, or refactor a
  project onto a platform module (auth, billing, tax, search, i18n, seo, uploads, ledger, notifications,
  mail, ai, helpdesk, marketing, tenancy, realtime), or test module swaps into real app cleanly.
---

# Embed a platform module into a project

**Audience: AI coding agents first.** Optimize for activation, not prose — do not "prettify" ladders back into paragraphs.

Third verb in module lifecycle: **`platform-extract-modules`** (prior app → module) · **`platform-make-modules`** (net-new module) · **embed** (module → consumer). Installs one `@platform-modules/*` package into target app, wires *only* seam that module needs.

> **One principle: wire seam, nothing more.** Module = typed contract; embedding = satisfy that contract from host (peers, stores, provider, tables) + delete host's now-redundant hand-rolled code. **Adapter-minimalism (Gate-2): one wiring file, no Service/Repo/Controller scaffold.** Host keeps owning its DB, provider creds, routes.

Canonical law — **read, do not re-inline** (one source of truth):
- Module's **boundary spec** `docs/specs/<date>-<module>-…-boundaries.md` — seam contract you must satisfy.
- `docs/standards/coding-standard.md` **§4 seam checklist** — what export guarantees.
- `docs/specs/2026-06-13-monorepo-architecture-synthesis.md` **§6 registry** — module status / peers.
- This skill's `references/module-axes.md` — per-module integration-axis lookup. **Read for target module before wiring.**

---

## Step 0a — Load the host's own law FIRST (two rulebooks bind, not one)

**Before any gate, classify, or wire: load target (consuming) repo's own conventions and obey them.** Every project getting an embed has its *own* rules, conventions, skills, gates — they bind code you write **in that repo** exactly as platform law binds module. You are guest in host repo; follow house rules.

Do this, in order, before Step 0b:
1. **Load host's memory FIRST** — read the host project's memory index (`~/.claude/projects/-<url-encoded-host-path>/memory/MEMORY.md`, then any entry it points to that bears on your embed: the host's live DB/secret targets, deploy quirks, prior-embed state, gotchas). Memory carries hard-won facts NOT in the code — stale-DB pointers, which env the worker really reads, what bit a past agent. **Skipping it repeats a solved failure.**
2. **Read host's `CLAUDE.md`** — root one, **and** any nested `CLAUDE.md` / `AGENTS.md` in each directory you touch (cascade; nearest wins for files under it).
3. **Enumerate host's `.claude/skills/` AND `.claude/agents/`** (and `.cursor/rules`, `AGENTS.md`, lint/format/commit configs). If host has a skill covering a step you're about to do (own DB-migration, test, commit, deploy procedure), **invoke that skill — do not substitute platform default.** If host defines a subagent for a task you'd dispatch, **use the host's agent, not a generic one.**
4. **Match host's local conventions** in every file you add or edit there: import style, test layout, naming, lint/format rules, commit format. Wiring file must read like host wrote it.

**Both rulebooks apply at once — neither silently overrides other:**
- **Platform law** governs **module's seam** (typed contract, §4 checklist, adapter-minimalism) — never relax it to suit host.
- **Host law** governs **everything you write inside host repo** (style, structure, gates, skills, commit conventions).
- **On direct conflict, STOP and surface it** in embed report — never silently pick one. (Example: host's lint forbids pattern seam requires → report clash, propose host-side exception, do not quietly violate either.)

**Do NOT copy host rules into module, or platform rules into host.** Point to each; keep separate. One source of truth per repo — re-inlining drifts.

---

## Step 0b — Gate: should this module be embedded here at all?

Stop if any true ("no" here = successful outcome — saves bad graft):
- Host has **no surface** for module (don't embed `tax` into app that doesn't charge).
- Host's existing code and module's seam are **incompatible shapes** (different contract, not just different names) → WATCH, not adoption. Report mismatch; do not force-fit.
- Module is **types-only / unbuilt** for surface you need (`realtime` core types-only; Workers `/server` real). Check §6 registry.

---

## Step 1 — Classify by integration axes (the spine — steps differ per axis)

Look module up in `references/module-axes.md`. Module carries one or more axes; **do each axis that applies, skip rest.** Do not invent steps an axis doesn't call for.

| Axis | Means | Host must provide |
|---|---|---|
| **A · siblings** | peer `@platform-modules/*` | install those peers too (transitively) |
| **B · db** | exports drizzle `pgTable` schema, ships **no migration** | run module's schema as host migration; pass `Database`/`Transaction` handle |
| **C · stores** | host-injected interface (`DedupStore`, `ComplianceStore`, preference store) | real implementation backed by host's DB — **never duck-typed stub** (data-loss path) |
| **D · provider** | provider SDK is `peerDependency` behind adapter | install SDK, register adapter at bootstrap, set creds in env (never commit) |
| **E · endpoint** | web-standard `Request`→`Response` handler (auth, `ingestWebhook`, presign) | mount on host route (Astro endpoint / Hono / Workers fetch) |
| **F · react** | `-react` sibling, headless hook/provider | wrap provider in React island; do not expect context to cross island boundaries |

---

## Step 2 — Pick the install vehicle

**Default for TEST → local workspace link** (no registry round-trip, pollutes nothing, instantly reflects local module edits):
```bash
# in the target repo, add to its package.json dependencies:
#   "@platform-modules/<mod>": "file:/home/user/Projects/platform/packages/<mod>"
# then, from the target repo:
pnpm install
```
Use `file:` (or `pnpm link`, or throwaway verdaccio registry) — **not** real publish. Build module first so `dist/` exists: `pnpm --filter @platform-modules/<mod> build`.

**For real adoption → GitHub Packages.** Modules publish under `@platform-modules` scope (changesets `release.yml`). Target needs `.npmrc` line `@platform-modules:registry=https://npm.pkg.github.com` and GHP token in `NODE_AUTH_TOKEN` (read-scope `read:packages`) — **token lives in `~/.npmrc` or env, never committed.** Then `pnpm add @platform-modules/<mod>@<version>`.
> Reality check: today every module is `version: 0.0.0` and **nothing published yet**. GHP path needs `changeset version` + release run first. For testing, use local link.

After install, satisfy **Axis A** — install every peer `@platform-modules/*` module declares (they're `peerDependencies`, not bundled). ESM-only, `sideEffects:false`: tree-shaking host pulls only subpaths it imports.

---

## Step 3 — Wire the applicable axes

Per-axis ladder. **Stop at axes `module-axes.md` marks for your module.**

**Axis B — db (riskiest; live schema change):**
1. Import module's schema (`import { <mod>Schema } from '@platform-modules/<mod>'`).
2. Generate migration from it (host's drizzle-kit), **or** hand-write DDL module's `pgTable`s imply. Module ships none on purpose — table ownership is host's.
3. **Floor: never DROP/rewrite existing host column.** New tables only; if name collides with host table, alias module's table or stop. Apply forward-only.
4. Pass real `Database`/`Transaction<S>` handle (drizzle over host's Postgres) into module's functions. Money/data-loss path spec types as `Transaction` won't accept non-tx handle — seam enforcing itself; honor it.

**Axis C — stores:** implement host-injected interface against host's DB. Enumerate-faithfully (`listSuppressed` must really enumerate; duck-typed `'x' in store` is silent-`[]` data-loss path modules were hardened against). Inject real clock (`now()`), never bake one.

**Axis D — provider:** install provider SDK (its real version), call module's `register…`/`make…Adapter` maker at bootstrap, read creds from env. One provider per adapter; core stays SDK-free (bundle-check proves it).

**Axis E — endpoint:** mount handler on host route. Web-standard `Request`→`Response`, so body identical under Astro `APIRoute`, Hono, Next route handler, or Workers `fetch` — wire framework's adapter, never rewrite handler.

**Axis F — react:** render `-react` provider inside React island; read via its hooks. For page-level/cross-island state use SSR routing or shared store — React context does **not** cross Astro island boundaries.

**Then — point of exercise: delete host's now-redundant code.** Replace hand-rolled implementation with calls to module. Can't delete it → module didn't actually cover surface — surface that.

---

## Step 4 — Verify (swap-survival is the real test)

Embedding proven only when all hold:
1. **Resolves + typechecks** — `pnpm install` clean, host `tsc --noEmit` green (module ships own `.d.ts`).
2. **Host's existing suite stays green** — module is drop-in; regression in host's own tests means seam diverged.
3. **Swap-survival parity** — assert module's output **equals** host's old hand-rolled output on host's real inputs (e.g. `serializeJsonLd(x)` ≡ deleted escaper's output; `resolveVatRate(date)` ≡ prior app's lookup). Load-bearing assertion — proves true like-for-like swap, not lookalike.
4. **Axis-specific floor** — db: existing rows survive migration + module's isolation/idempotency holds on host's data; provider: real send/charge round-trips; endpoint: route returns contract's status codes.
5. **No new signals** — every install `WARN`, peer-dep notice, type hint fixed or justified benign (green = clean, not passed-with-noise).

---

## Step 5 — Rollback (embedding into a live app must be reversible)

Record every touch so graft can be undone: `package.json` dep line, `.npmrc`, migration, wiring file, deletions. **Do whole embed in throwaway git worktree of target** (`git worktree add`), never on its main. Rollback = drop worktree + revert migration (forward-only down-migration). State rollback steps in embed report.

---

## Output — the embed report

```
## embed report — @platform-modules/<mod> → <target>
**Vehicle:** local-link | GHP@<ver>     **Worktree:** <path>
**Host law:** memory loaded <y/n + key facts> · CLAUDE.md read <y/n> · host skills/agents invoked <which> · conventions matched <y/n> · conflicts <none | listed>
**Axes wired:** A:<peers> B:<tables+migration> C:<stores> D:<provider> E:<route> F:<island>
**Deleted from host:** <files/functions the module replaced>
**Verify:** typecheck <g/r> · host suite <g/r> · swap-parity <g/r, the assertion> · axis-floor <g/r>
**Rollback:** <exact steps>
**Mismatch / WATCH (if any):** <where the seam didn't cover the host surface>
```

Keep tight. Parity assertion + host-suite result are verdict; rest is provenance.

## Learned Rules

### host-constant-extraction-no-module-type | fired:1 | 2026-06-16
Wave spec added `import type { IanaTimezone }` + type annotation to host-side constant extraction (no module function called, no IANA picker in host) — coupling with zero runtime benefit; advisor blocked before dispatch → wrong.
Prevent: if wave is pure host-side constant extraction (no new module function calls), spec must NOT add module type import. Plain `export const X = 'value'` with inferred type correct. Only import a type from module when host has actual call site using that type.

### ghp-embed-ci-auth-three-touches | fired:1 | 2026-06-16
GHP embed wired only scope→registry in `.npmrc`; missing `${NODE_AUTH_TOKEN}` placeholder, CI workflow env, consumer-repo secret. CI broke on next push → wrong. Three touches required together: (1) `.npmrc` needs both lines: scope route + `//npm.pkg.github.com/:_authToken=${NODE_AUTH_TOKEN}`; (2) both deploy workflow files need `NODE_AUTH_TOKEN: ${{ secrets.GH_PKG_READ_TOKEN }}` in job-level env; (3) `GH_PKG_READ_TOKEN` secret must exist in consumer repo.
Prevent: when vehicle=GHP, treat `.npmrc` as 2-line file (not 1). Before merging, run `gh secret list --repo <consumer>` to confirm secret exists; grep both deploy YAML files for `NODE_AUTH_TOKEN`. Local `pnpm install` WARN about `${NODE_AUTH_TOKEN}` unexpanded is expected (token lives in `~/.npmrc`; not an error).