rn-module-add — wire a backend/infra module into a scaffolded RN app
Contract
See references/contracts.md (vendored from dev-flow). Key facts:
- Reads
<project-root>/.workflow/meta.json#stack.framework — must be "expo-rn".
- Requires
meta.json#phase ≥ "scaffolded".
- Each module updates a specific key in
meta.json#stack:
auth module → meta.json#stack.auth
db module → meta.json#stack.db
storage module → meta.json#stack.storage (new sub-key)
realtime module → meta.json#stack.realtime (new sub-key)
push module → meta.json#stack.push (new sub-key)
payments module → meta.json#stack.payments
cms module → meta.json#stack.cms (new sub-key — content the app only reads)
- Sets
meta.json#phase = "module_added" after the first module, then leaves it.
- Always idempotent: re-running with same provider detects existing wiring and exits 0.
When this skill applies
- Phase is
scaffolded or page_generated or module_added.
- User asks to add ONE module by name. (To add multiple, run the skill multiple times.)
- Orchestrator routes here from
dev-flow.
Knowledge dependencies (read these first)
rn-fundamentals/SKILL.md — TypeScript strict, modern primitives.
rn-backend/SKILL.md — provider-agnostic client-auth architecture.
rn-backend/references/<provider>.md — provider-specific wiring (supabase.md / firebase.md / custom-rest.md / trpc.md).
rn-push-notifications/references/setup.md — for the push module.
rn-publishing-payments/references/revenuecat.md — for the payments module.
references/module-cms-sanity.md — for the cms module (Sanity Studio as the admin panel, HTTP Query API in the app).
Monorepo awareness
Before Step 1: check meta.json#stack.framework.
- If
"expo-rn" (mobile-only project): proceed normally, operate at project root.
- If
"monorepo": this skill operates inside apps/mobile/ for mobile-specific modules (push, RevenueCat, expo-image-picker), AND inside packages/api/ for modules shared with web (auth, db, storage, realtime). For shared modules, do NOT re-install the SDK if packages/api/ already has it from a prior module-add (web side) call. Run monorepo-sync-types after db and auth are wired.
- If anything else: refuse — use
module-add (web stacks) or monorepo-bootstrap (greenfield monorepo).
For the monorepo case, "install + generate" means: install npm packages into the appropriate workspace (pnpm add --filter @<slug>/mobile for mobile-specific, pnpm add --filter @<slug>/api for shared backend client), and generate code into the right sub-folder. The reference paths below all reference apps/mobile/ instead of project root.
Workflow
Step 1 — Verify preconditions
Read .workflow/meta.json. Abort if stack.framework ∉ {"expo-rn", "monorepo"} or phase < "scaffolded".
If stack.framework == "monorepo", all subsequent file paths are relative to apps/mobile/ (e.g. apps/mobile/lib/auth.ts) or packages/api/src/ (e.g. packages/api/src/auth.ts). Honor the workspace conventions.
Step 2 — Identify the module + provider
Module the user requested: one of auth | db | storage | realtime | push | payments | cms.
If the user didn't specify a provider, default to:
auth, db, storage, realtime → Supabase (the default in rn-backend/references/decision-tree.md).
push → expo-notifications + Expo push service (the default).
payments → RevenueCat (the default).
cms → Sanity (the hosted Studio is the admin panel; the app reads over the HTTP Query API with no SDK in the bundle — references/module-cms-sanity.md). Use it for content a non-developer edits and the app only reads; user-owned data stays in db.
If the user specifies a non-default provider, accept it: Firebase / custom-rest / tRPC for backend; Stripe for non-digital payments.
Step 3 — Idempotency check
Read meta.json#stack.<module-key>. If non-null:
- Same provider as requested → "Already wired with X, exiting." Phase stays.
- Different provider → REFUSE unless user passed an explicit "swap" intent. Removing one provider's code + installing another's is a multi-file change that needs explicit user confirmation. Report and stop.
Step 4 — Install deps
Use npx expo install ... -- --legacy-peer-deps for any package that touches native (auth, db SDKs, payments). Use npm install --legacy-peer-deps for JS-only packages.
Provider matrix:
| Module |
Supabase |
Firebase |
Custom REST |
tRPC |
| auth |
@supabase/supabase-js, react-native-url-polyfill, expo-secure-store |
@react-native-firebase/{app,auth}, expo-build-properties |
(uses existing lib/api.ts) + expo-secure-store |
@trpc/client, @trpc/react-query, superjson |
| db |
(same as auth) |
@react-native-firebase/firestore |
(uses lib/api.ts) |
(uses tRPC) |
| storage |
(uses @supabase/supabase-js) |
@react-native-firebase/storage |
expo-file-system + custom upload |
(uses tRPC or REST) |
| realtime |
(uses @supabase/supabase-js) |
(uses @react-native-firebase/firestore) |
(WebSocket of your choice) |
(tRPC subscriptions) |
| push |
expo-notifications, expo-device |
(same + @react-native-firebase/messaging) |
(same; server-side uses your backend) |
(same) |
| payments |
react-native-purchases (RevenueCat) |
(same) |
(same) |
(same) |
| cms |
Sanity — nothing in the app (plain fetch on the HTTP Query API); sanity, @sanity/vision, @sanity/client live in the separate cms/ package |
— |
— |
— |
Step 5 — Generate the wiring
For each (module, provider) combination, write to <project-root>/lib/:
auth/Supabase → lib/supabase.ts (client) + lib/auth.ts (sign-in/out hooks). See rn-backend/references/supabase.md.
auth/Firebase → lib/firebase.ts + lib/auth.ts. See rn-backend/references/firebase.md.
auth/custom-rest → lib/api.ts (already exists from rn-bootstrap, extend with refresh-on-401) + lib/auth.ts. See rn-backend/references/custom-rest.md.
auth/tRPC → lib/trpc.ts + lib/auth.ts. See rn-backend/references/trpc.md.
payments/RevenueCat → lib/purchases.ts + hooks/usePro.ts. See rn-publishing-payments/references/revenuecat.md.
push/expo-notifications → lib/push.ts + edits to app/_layout.tsx (handlers + permissions). See rn-push-notifications/references/patterns.md.
cms/Sanity → cms/ Studio package (config, schema types from the PRD domain, seed script, README) + lib/cms.ts (cmsFetch over the HTTP Query API) + query hooks under lib/queries/ + metro.config.js blockList for cms/. See references/module-cms-sanity.md.
ALWAYS also create store/auth.ts (Zustand) and ensure app/(app)/_layout.tsx redirects when unauthenticated — see rn-backend/references/patterns.md.
Step 6 — Update .env.example + app.json
Per provider, add the env vars to .env.example:
- Supabase:
EXPO_PUBLIC_SUPABASE_URL=, EXPO_PUBLIC_SUPABASE_ANON_KEY=
- Firebase: requires
GoogleService-Info.plist + google-services.json at root (manual download from Firebase console).
- Custom REST:
EXPO_PUBLIC_API_URL=
- tRPC:
EXPO_PUBLIC_API_URL=
- RevenueCat:
EXPO_PUBLIC_REVENUECAT_IOS_KEY=, EXPO_PUBLIC_REVENUECAT_ANDROID_KEY=
- Sanity (cms):
EXPO_PUBLIC_SANITY_PROJECT_ID=, EXPO_PUBLIC_SANITY_DATASET=production — no token: EXPO_PUBLIC_* ships in the bundle, so the catalogue dataset stays public. The Editor token for seeding lives in cms/.env only.
For Firebase or RevenueCat or push notifications, add the relevant app.json config plugin entries (see provider-specific reference).
Step 7 — Verify
Run npx tsc --noEmit. Must pass. If not, fix the wiring before reporting.
For modules with native config plugin changes (Firebase, push), the next build will require a new eas build --profile development. Print a reminder.
Step 8 — Update meta.json + commit
Update meta.json:
stack.<module>: set to the provider name (e.g. "supabase", "firebase", "custom-rest", "trpc", "revenuecat", "stripe", "expo-notifications", "sanity").
phase: if currently "scaffolded" or "page_generated", set to "module_added". Otherwise leave.
history: append { skill: "rn-module-add", ran_at: <iso>, inputs: { module, provider }, outputs: [<files>], phase_before, phase_after }.
If git repo: git add the new files + git commit -m "feat(<module>): wire <provider>".
Common anti-patterns (NEVER do)
- ❌ Wire two providers for the same module simultaneously (e.g. Supabase + Firebase auth) — pick one.
- ❌ Skip the
stack.<module> update in meta.json — future re-runs will re-install.
- ❌ Hardcode secrets in
lib/*.ts — read from process.env.EXPO_PUBLIC_*.
- ❌ Generate wiring without running
tsc --noEmit — broken project shipped to user.
- ❌ Add a module that requires a new dev build without printing the "rebuild required" reminder.
- ❌ Use
npm install without --legacy-peer-deps — fails on unsupported/older Expo SDK versions due to peer-range mismatches (see rn-bootstrap lessons-learned).
Updating meta.json (recommended pattern)
When this skill modifies state (artifact written, phase advanced, history appended), use the canonical script when available:
# Wherever dev-flow is installed (e.g. ~/.claude/skills/dev-flow/), invoke:
python3 .../dev-flow/scripts/update_meta.py <project-root> record-artifact \
--path <relative-path> --produced-by '<this-skill-name>' [--derived-from <p1> <p2> ...]
python3 .../dev-flow/scripts/update_meta.py <project-root> set-phase <new_phase>
python3 .../dev-flow/scripts/update_meta.py <project-root> append-history \
--skill '<this-skill-name>' --inputs '{...}' --outputs '{...}' --phase-after <new_phase>
The script enforces phase monotonicity, normalizes legacy kebab-case aliases (e.g. module-added → module_added), and writes the canonical sha256 + timestamp into meta.json#artifacts. Fall back to direct JSON editing only if the script is not on PATH (and warn the user).
Folder structure rules (canonical)
When wiring a module for RN/Expo, respect the canonical structure (spec: docs/superpowers/specs/2026-06-06-folder-structure-refactor.md):
auth module: client (supabase.ts / firebase.ts) → lib/. Secure-store wrapper → lib/secure-store.ts. Auth store → store/auth-store.ts. UI (SignInForm, SignUpForm) → app/(auth)/_components/.
db module: client wiring co-located with auth (lib/). Query hooks → app/<route>/_components/use<X>.ts initially, promoted to components/shared/<dominio>/hooks/ as they spread.
storage module: client in lib/storage.ts. Image-picker UI co-located with the screen.
realtime module: subscriptions wired in screens or in store/ if cross-feature.
push module: lib/push.ts + handlers in app/_layout.tsx.
payments module: client in lib/purchases.ts. Paywall UI in app/(app)/paywall/_components/. Pro gating hook → hooks/use-pro.ts.
cms module: the Studio is its own package in cms/ (own package.json, excluded from Metro and from the app's tsconfig); read client in lib/cms.ts; GROQ query hooks in lib/queries/. Never an admin screen in app/ for content — that is the Studio.
For monorepo (stack.framework="monorepo"): backend client (auth/db/storage/realtime) goes in packages/api/ and is consumed via @<slug>/api/* workspace import.
Sources
- Course: codewithbeto.dev/rnCourse — Backend Basics + Supabase + Publishing/Payments modules (paid).
- Knowledge skills consumed (see above).
1---2name: rn-module-add3description: Use to wire a backend/infra module (auth, db, storage, realtime, push, payments, cms) into a scaffolded Expo + RN app. Reads .workflow/meta.json with stack.framework="expo-rn" and the user-chosen provider for each module (Supabase, Firebase, custom REST, tRPC, RevenueCat, Sanity for cms). Installs deps, generates the wiring code (lib/auth.ts, lib/supabase.ts, etc.), updates meta.json#stack to record the choice. Always idempotent. Triggers on: "add auth", "wire up db", "set up Supabase", "set up Firebase", "add payments", "add push" (the server-side part), "add a CMS", "set up Sanity", "admin panel for content", "aggiungi un CMS", "aggiungi modulo X". Not for: building UI for the module (rn-add-screen does the login screen, etc.), client-side knowledge only (rn-backend, rn-push-notifications), scaffolding (rn-bootstrap).4---56# rn-module-add — wire a backend/infra module into a scaffolded RN app78## Contract910See `references/contracts.md` (vendored from `dev-flow`). Key facts:11- Reads `<project-root>/.workflow/meta.json#stack.framework` — must be `"expo-rn"`.12- Requires `meta.json#phase ≥ "scaffolded"`.13- Each module updates a specific key in `meta.json#stack`:14 - `auth` module → `meta.json#stack.auth`15 - `db` module → `meta.json#stack.db`16 - `storage` module → `meta.json#stack.storage` (new sub-key)17 - `realtime` module → `meta.json#stack.realtime` (new sub-key)18 - `push` module → `meta.json#stack.push` (new sub-key)19 - `payments` module → `meta.json#stack.payments`20 - `cms` module → `meta.json#stack.cms` (new sub-key — content the app only reads)21- Sets `meta.json#phase = "module_added"` after the first module, then leaves it.22- Always idempotent: re-running with same provider detects existing wiring and exits 0.2324## When this skill applies2526- Phase is `scaffolded` or `page_generated` or `module_added`.27- User asks to add ONE module by name. (To add multiple, run the skill multiple times.)28- Orchestrator routes here from `dev-flow`.2930## Knowledge dependencies (read these first)3132- `rn-fundamentals/SKILL.md` — TypeScript strict, modern primitives.33- `rn-backend/SKILL.md` — provider-agnostic client-auth architecture.34- `rn-backend/references/<provider>.md` — provider-specific wiring (supabase.md / firebase.md / custom-rest.md / trpc.md).35- `rn-push-notifications/references/setup.md` — for the push module.36- `rn-publishing-payments/references/revenuecat.md` — for the payments module.37- `references/module-cms-sanity.md` — for the cms module (Sanity Studio as the admin panel, HTTP Query API in the app).3839## Monorepo awareness4041Before Step 1: check `meta.json#stack.framework`.4243- If `"expo-rn"` (mobile-only project): proceed normally, operate at project root.44- If `"monorepo"`: this skill operates inside `apps/mobile/` for mobile-specific modules (push, RevenueCat, expo-image-picker), AND inside `packages/api/` for modules shared with web (auth, db, storage, realtime). For shared modules, do NOT re-install the SDK if `packages/api/` already has it from a prior `module-add` (web side) call. Run `monorepo-sync-types` after `db` and `auth` are wired.45- If anything else: refuse — use `module-add` (web stacks) or `monorepo-bootstrap` (greenfield monorepo).4647For the monorepo case, "install + generate" means: install npm packages into the appropriate workspace (`pnpm add --filter @<slug>/mobile` for mobile-specific, `pnpm add --filter @<slug>/api` for shared backend client), and generate code into the right sub-folder. The reference paths below all reference `apps/mobile/` instead of project root.4849## Workflow5051### Step 1 — Verify preconditions5253Read `.workflow/meta.json`. Abort if `stack.framework ∉ {"expo-rn", "monorepo"}` or `phase < "scaffolded"`.5455If `stack.framework == "monorepo"`, all subsequent file paths are relative to `apps/mobile/` (e.g. `apps/mobile/lib/auth.ts`) or `packages/api/src/` (e.g. `packages/api/src/auth.ts`). Honor the workspace conventions.5657### Step 2 — Identify the module + provider5859Module the user requested: one of `auth | db | storage | realtime | push | payments | cms`.6061If the user didn't specify a provider, default to:62- `auth`, `db`, `storage`, `realtime` → **Supabase** (the default in `rn-backend/references/decision-tree.md`).63- `push` → **expo-notifications + Expo push service** (the default).64- `payments` → **RevenueCat** (the default).65- `cms` → **Sanity** (the hosted Studio is the admin panel; the app reads over the HTTP Query API with no SDK in the bundle — `references/module-cms-sanity.md`). Use it for content a non-developer edits and the app only reads; user-owned data stays in `db`.6667If the user specifies a non-default provider, accept it: Firebase / custom-rest / tRPC for backend; Stripe for non-digital payments.6869### Step 3 — Idempotency check7071Read `meta.json#stack.<module-key>`. If non-null:72- Same provider as requested → "Already wired with X, exiting." Phase stays.73- Different provider → REFUSE unless user passed an explicit "swap" intent. Removing one provider's code + installing another's is a multi-file change that needs explicit user confirmation. Report and stop.7475### Step 4 — Install deps7677Use `npx expo install ... -- --legacy-peer-deps` for any package that touches native (auth, db SDKs, payments). Use `npm install --legacy-peer-deps` for JS-only packages.7879Provider matrix:8081| Module | Supabase | Firebase | Custom REST | tRPC |82|---|---|---|---|---|83| auth | `@supabase/supabase-js`, `react-native-url-polyfill`, `expo-secure-store` | `@react-native-firebase/{app,auth}`, `expo-build-properties` | (uses existing `lib/api.ts`) + `expo-secure-store` | `@trpc/client`, `@trpc/react-query`, `superjson` |84| db | (same as auth) | `@react-native-firebase/firestore` | (uses `lib/api.ts`) | (uses tRPC) |85| storage | (uses `@supabase/supabase-js`) | `@react-native-firebase/storage` | `expo-file-system` + custom upload | (uses tRPC or REST) |86| realtime | (uses `@supabase/supabase-js`) | (uses `@react-native-firebase/firestore`) | (WebSocket of your choice) | (tRPC subscriptions) |87| push | `expo-notifications`, `expo-device` | (same + `@react-native-firebase/messaging`) | (same; server-side uses your backend) | (same) |88| payments | `react-native-purchases` (RevenueCat) | (same) | (same) | (same) |89| cms | **Sanity** — nothing in the app (plain `fetch` on the HTTP Query API); `sanity`, `@sanity/vision`, `@sanity/client` live in the separate `cms/` package | — | — | — |9091### Step 5 — Generate the wiring9293For each (module, provider) combination, write to `<project-root>/lib/`:9495- `auth/Supabase` → `lib/supabase.ts` (client) + `lib/auth.ts` (sign-in/out hooks). See `rn-backend/references/supabase.md`.96- `auth/Firebase` → `lib/firebase.ts` + `lib/auth.ts`. See `rn-backend/references/firebase.md`.97- `auth/custom-rest` → `lib/api.ts` (already exists from rn-bootstrap, extend with refresh-on-401) + `lib/auth.ts`. See `rn-backend/references/custom-rest.md`.98- `auth/tRPC` → `lib/trpc.ts` + `lib/auth.ts`. See `rn-backend/references/trpc.md`.99- `payments/RevenueCat` → `lib/purchases.ts` + `hooks/usePro.ts`. See `rn-publishing-payments/references/revenuecat.md`.100- `push/expo-notifications` → `lib/push.ts` + edits to `app/_layout.tsx` (handlers + permissions). See `rn-push-notifications/references/patterns.md`.101- `cms/Sanity` → `cms/` Studio package (config, schema types from the PRD domain, seed script, README) + `lib/cms.ts` (`cmsFetch` over the HTTP Query API) + query hooks under `lib/queries/` + `metro.config.js` blockList for `cms/`. See `references/module-cms-sanity.md`.102103ALWAYS also create `store/auth.ts` (Zustand) and ensure `app/(app)/_layout.tsx` redirects when unauthenticated — see `rn-backend/references/patterns.md`.104105### Step 6 — Update `.env.example` + `app.json`106107Per provider, add the env vars to `.env.example`:108109- Supabase: `EXPO_PUBLIC_SUPABASE_URL=`, `EXPO_PUBLIC_SUPABASE_ANON_KEY=`110- Firebase: requires `GoogleService-Info.plist` + `google-services.json` at root (manual download from Firebase console).111- Custom REST: `EXPO_PUBLIC_API_URL=`112- tRPC: `EXPO_PUBLIC_API_URL=`113- RevenueCat: `EXPO_PUBLIC_REVENUECAT_IOS_KEY=`, `EXPO_PUBLIC_REVENUECAT_ANDROID_KEY=`114- Sanity (cms): `EXPO_PUBLIC_SANITY_PROJECT_ID=`, `EXPO_PUBLIC_SANITY_DATASET=production` — **no token**: `EXPO_PUBLIC_*` ships in the bundle, so the catalogue dataset stays public. The Editor token for seeding lives in `cms/.env` only.115116For Firebase or RevenueCat or push notifications, add the relevant `app.json` config plugin entries (see provider-specific reference).117118### Step 7 — Verify119120Run `npx tsc --noEmit`. Must pass. If not, fix the wiring before reporting.121122For modules with native config plugin changes (Firebase, push), the next build will require a new `eas build --profile development`. Print a reminder.123124### Step 8 — Update meta.json + commit125126Update `meta.json`:127- `stack.<module>`: set to the provider name (e.g. `"supabase"`, `"firebase"`, `"custom-rest"`, `"trpc"`, `"revenuecat"`, `"stripe"`, `"expo-notifications"`, `"sanity"`).128- `phase`: if currently `"scaffolded"` or `"page_generated"`, set to `"module_added"`. Otherwise leave.129- `history`: append `{ skill: "rn-module-add", ran_at: <iso>, inputs: { module, provider }, outputs: [<files>], phase_before, phase_after }`.130131If git repo: `git add` the new files + `git commit -m "feat(<module>): wire <provider>"`.132133## Common anti-patterns (NEVER do)134135- ❌ Wire two providers for the same module simultaneously (e.g. Supabase + Firebase auth) — pick one.136- ❌ Skip the `stack.<module>` update in meta.json — future re-runs will re-install.137- ❌ Hardcode secrets in `lib/*.ts` — read from `process.env.EXPO_PUBLIC_*`.138- ❌ Generate wiring without running `tsc --noEmit` — broken project shipped to user.139- ❌ Add a module that requires a new dev build without printing the "rebuild required" reminder.140- ❌ Use `npm install` without `--legacy-peer-deps` — fails on unsupported/older Expo SDK versions due to peer-range mismatches (see `rn-bootstrap` lessons-learned).141142## Updating meta.json (recommended pattern)143144When this skill modifies state (artifact written, phase advanced, history appended), use the canonical script when available:145146```bash147# Wherever dev-flow is installed (e.g. ~/.claude/skills/dev-flow/), invoke:148python3 .../dev-flow/scripts/update_meta.py <project-root> record-artifact \149 --path <relative-path> --produced-by '<this-skill-name>' [--derived-from <p1> <p2> ...]150python3 .../dev-flow/scripts/update_meta.py <project-root> set-phase <new_phase>151python3 .../dev-flow/scripts/update_meta.py <project-root> append-history \152 --skill '<this-skill-name>' --inputs '{...}' --outputs '{...}' --phase-after <new_phase>153```154155The script enforces phase monotonicity, normalizes legacy kebab-case aliases (e.g. `module-added` → `module_added`), and writes the canonical sha256 + timestamp into `meta.json#artifacts`. **Fall back to direct JSON editing only if the script is not on PATH** (and warn the user).156157## Folder structure rules (canonical)158159When wiring a module for RN/Expo, respect the canonical structure (spec: `docs/superpowers/specs/2026-06-06-folder-structure-refactor.md`):160161- **`auth` module**: client (`supabase.ts` / `firebase.ts`) → `lib/`. Secure-store wrapper → `lib/secure-store.ts`. Auth store → `store/auth-store.ts`. UI (SignInForm, SignUpForm) → `app/(auth)/_components/`.162- **`db` module**: client wiring co-located with auth (`lib/`). Query hooks → `app/<route>/_components/use<X>.ts` initially, promoted to `components/shared/<dominio>/hooks/` as they spread.163- **`storage` module**: client in `lib/storage.ts`. Image-picker UI co-located with the screen.164- **`realtime` module**: subscriptions wired in screens or in `store/` if cross-feature.165- **`push` module**: `lib/push.ts` + handlers in `app/_layout.tsx`.166- **`payments` module**: client in `lib/purchases.ts`. Paywall UI in `app/(app)/paywall/_components/`. Pro gating hook → `hooks/use-pro.ts`.167- **`cms` module**: the Studio is its own package in `cms/` (own `package.json`, excluded from Metro and from the app's `tsconfig`); read client in `lib/cms.ts`; GROQ query hooks in `lib/queries/`. Never an admin screen in `app/` for content — that is the Studio.168169For monorepo (`stack.framework="monorepo"`): backend client (auth/db/storage/realtime) goes in `packages/api/` and is consumed via `@<slug>/api/*` workspace import.170171## Sources172173- Course: codewithbeto.dev/rnCourse — Backend Basics + Supabase + Publishing/Payments modules (paid).174- Knowledge skills consumed (see above).