Tenant Migration Skill
Audit and execute the full migration checklist when cloning an existing Movemental tenant platform (the source, e.g. alan-hirsch) to create a new tenant deployment (the target, e.g. brad-brisco).
Context
This is a multi-tenant thought leader platform. Each tenant is:
- A separate Git repo/deployment sharing the same codebase structure
- Scoped to one
organizations row in a shared Supabase database via TENANT_ORG_ID
- Visually differentiated via
globals.css CSS variables and tenant.config.ts
The source repo (alan-hirsch) is the reference implementation. New tenants clone the repo, then must change every tenant-specific surface. This skill audits what's been done and what remains.
Execution Steps
Phase 1: Database & Org Identity (Supabase MCP)
Use the Supabase MCP (project vhaiiiykcukrlyvwlgip, public schema) to verify:
Organization row exists:
SELECT id, name, slug, description FROM organizations WHERE slug = '<target-slug>';
If missing, report that the org must be created first. Do NOT create it automatically.
TENANT_ORG_ID matches:
- Read
.env.local.example for the documented TENANT_ORG_ID
- Confirm it matches the org row's
id
Content scoped to this org:
SELECT 'books' AS type, count(*) FROM books WHERE organization_id = '<org-id>'
UNION ALL SELECT 'articles', count(*) FROM articles WHERE organization_id = '<org-id>'
UNION ALL SELECT 'courses', count(*) FROM courses WHERE organization_id = '<org-id>'
UNION ALL SELECT 'podcast_episodes', count(*) FROM podcast_episodes WHERE organization_id = '<org-id>'
UNION ALL SELECT 'videos', count(*) FROM videos WHERE organization_id = '<org-id>';
Report counts. Zero counts for enabled content types are warnings.
Storage buckets: Check if the tenant has a storage folder:
SELECT name FROM storage.buckets WHERE name LIKE '%media%';
Note: tenant images typically live under media-library/<tenant-slug>/ in Supabase Storage.
Vector store (if AI features enabled): Check if OPENAI_VECTOR_STORE_ID is documented or if the org has corpus content:
SELECT count(*) FROM book_chapters WHERE book_id IN (SELECT id FROM books WHERE organization_id = '<org-id>');
Phase 2: Tenant Config Audit
Read and audit src/lib/config/tenant.config.ts:
Name/identity fields — Must NOT reference the source tenant:
name, tagline, description, copyright
logo.text, logo.imageUrl, logo.markLightUrl, logo.markDarkUrl
about.heading, about.leadSentence, about.body, about.credentials
contact.email, contact.speakingNote
search.placeholder (should not say "Search Alan's...")
newsletter.headline, newsletter.subline, newsletter.leadMagnet
quote.text, quote.cite
Feature flags — features.* should reflect what the new tenant actually has:
- If
features.books === true, there must be books in the DB for this org
- If
features.courses === true, there must be courses
- If
features.chat === true, AI/agent infrastructure must be configured
- If
features.assessments === true, assessment records must exist
- If
features.podcasts === true, podcast episodes must exist
Chat config — All chat strings reference source tenant's name/voice:
chat.welcomeMessage, chat.disclaimer, chat.featuredSubline
chat.firstMessageHost, chat.assistantLabel
chat.floatingDocsAriaLabel
Themes/pathways — These are deeply tenant-specific:
themes[] slugs, titles, descriptions, coverImages
frameworks.items[] slugs and descriptions
pathwayCta.* content
home.router.options[] and home.router.ctas[]
Organizations/partners — organizations.items[] must be the new tenant's partners, not the source's
Pricing — pricing.plans[] may differ per tenant
Author profile (EEAT) — authorProfile.* must reflect the new author
Hero — hero.* heading, subheading, CTAs, imageUrl, backgroundImageUrl
Content type descriptions — contentTypes.* descriptions reference source tenant's domain
Home page sections — home.* section copy
Phase 3: Environment Variables
Read .env.local.example and verify:
TENANT_ORG_ID — Must be the new tenant's org UUID
DATABASE_URL — Same shared Supabase instance
NEXT_PUBLIC_SUPABASE_URL / NEXT_PUBLIC_SUPABASE_ANON_KEY — Same project
OPENAI_API_KEY / OPENAI_MODEL — If chat enabled
OPENAI_VECTOR_STORE_ID — Tenant-specific vector store for RAG
AI_LAB_AGENT_URL / AI_LAB_AGENT_API_KEY — If using external agent
STRIPE_SECRET_KEY / NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY — Tenant-specific Stripe account
SENTRY_DSN / SENTRY_PROJECT — Tenant-specific Sentry project
COURSE_PREVIEW_HMAC_SECRET — Must match studio if using course preview
Phase 4: Codebase Grep for Source Tenant Leaks
Search the entire src/ directory for hardcoded references to the source tenant:
Grep for: "Alan Hirsch", "alan-hirsch", "alanhirsch", "Alan's", "mDNA", "APEST", "Forgotten Ways", "5Q", "Reframation"
Exclude:
tenant.config.ts (that's the config file itself)
node_modules/, .next/, _docs/
- Comments in generated files
Any hits in components, pages, or services are violations that must be fixed.
Phase 5: Middleware & Auth
Read src/middleware.ts and verify:
- Protected route patterns still make sense for the new tenant
- No hardcoded paths specific to the source tenant
Phase 6: Vercel Deployment
Check for Vercel project linkage:
.vercel/project.json — Should reference the new tenant's Vercel project
- Environment variables must be set in Vercel dashboard for all required vars
Output Format
Generate a structured migration report:
# Tenant Migration Report: [source] → [target]
## Status Summary
- Database org: [PASS/FAIL/MISSING]
- Tenant config: [X/Y fields migrated]
- Env vars: [CONFIGURED/NEEDS ATTENTION]
- Source tenant leaks: [N violations found]
- Middleware/auth: [PASS/NEEDS REVIEW]
- Vercel deployment: [LINKED/NOT LINKED]
## Critical (must fix before launch)
1. ...
## Warnings (should fix)
1. ...
## Passed
1. ...
## Recommended Next Steps
1. ...
Important Rules
- Never modify the Drizzle schema for migration purposes
- Never create database rows without explicit user approval
- Use Supabase MCP for all database queries — do not guess structure
- If MCP is unavailable, report what you can from code inspection alone and note which DB checks were skipped
- Visual theming (globals.css) and image assets are out of scope — handle those separately via
/color-audit, /asset-match, or manual design work
- The tenant.config.ts + .env.local are the two files that MUST change for every migration — everything else is structural
- Run
/tenant-check after migration to verify no hardcoded strings leaked through
1---2name: tenant-migrate3description: Audit and execute multi-tenant platform migration — determines what's needed when cloning a fully-built Movemental tenant (e.g. alan-hirsch) to bootstrap a new one (e.g. brad-brisco). Inspects Supabase via MCP, audits tenant config, env vars, feature flags, storage buckets, database org row, codebase leaks, middleware, and Vercel deployment. Does NOT handle visual theming (globals.css) or image assets — those are separate concerns. Use when onboarding a new thought leader onto the platform.4---56# Tenant Migration Skill78Audit and execute the full migration checklist when cloning an existing Movemental tenant platform (the **source**, e.g. `alan-hirsch`) to create a new tenant deployment (the **target**, e.g. `brad-brisco`).910## Context1112This is a **multi-tenant thought leader platform**. Each tenant is:13- A separate Git repo/deployment sharing the same codebase structure14- Scoped to one `organizations` row in a shared Supabase database via `TENANT_ORG_ID`15- Visually differentiated via `globals.css` CSS variables and `tenant.config.ts`1617The source repo (alan-hirsch) is the reference implementation. New tenants clone the repo, then must change every tenant-specific surface. This skill audits what's been done and what remains.1819## Execution Steps2021### Phase 1: Database & Org Identity (Supabase MCP)2223Use the Supabase MCP (project `vhaiiiykcukrlyvwlgip`, `public` schema) to verify:24251. **Organization row exists**:26 ```sql27 SELECT id, name, slug, description FROM organizations WHERE slug = '<target-slug>';28 ```29 If missing, report that the org must be created first. Do NOT create it automatically.30312. **TENANT_ORG_ID matches**:32 - Read `.env.local.example` for the documented `TENANT_ORG_ID`33 - Confirm it matches the org row's `id`34353. **Content scoped to this org**:36 ```sql37 SELECT 'books' AS type, count(*) FROM books WHERE organization_id = '<org-id>'38 UNION ALL SELECT 'articles', count(*) FROM articles WHERE organization_id = '<org-id>'39 UNION ALL SELECT 'courses', count(*) FROM courses WHERE organization_id = '<org-id>'40 UNION ALL SELECT 'podcast_episodes', count(*) FROM podcast_episodes WHERE organization_id = '<org-id>'41 UNION ALL SELECT 'videos', count(*) FROM videos WHERE organization_id = '<org-id>';42 ```43 Report counts. Zero counts for enabled content types are warnings.44454. **Storage buckets**: Check if the tenant has a storage folder:46 ```sql47 SELECT name FROM storage.buckets WHERE name LIKE '%media%';48 ```49 Note: tenant images typically live under `media-library/<tenant-slug>/` in Supabase Storage.50515. **Vector store** (if AI features enabled): Check if `OPENAI_VECTOR_STORE_ID` is documented or if the org has corpus content:52 ```sql53 SELECT count(*) FROM book_chapters WHERE book_id IN (SELECT id FROM books WHERE organization_id = '<org-id>');54 ```5556### Phase 2: Tenant Config Audit5758Read and audit `src/lib/config/tenant.config.ts`:59601. **Name/identity fields** — Must NOT reference the source tenant:61 - `name`, `tagline`, `description`, `copyright`62 - `logo.text`, `logo.imageUrl`, `logo.markLightUrl`, `logo.markDarkUrl`63 - `about.heading`, `about.leadSentence`, `about.body`, `about.credentials`64 - `contact.email`, `contact.speakingNote`65 - `search.placeholder` (should not say "Search Alan's...")66 - `newsletter.headline`, `newsletter.subline`, `newsletter.leadMagnet`67 - `quote.text`, `quote.cite`68692. **Feature flags** — `features.*` should reflect what the new tenant actually has:70 - If `features.books === true`, there must be books in the DB for this org71 - If `features.courses === true`, there must be courses72 - If `features.chat === true`, AI/agent infrastructure must be configured73 - If `features.assessments === true`, assessment records must exist74 - If `features.podcasts === true`, podcast episodes must exist75763. **Chat config** — All chat strings reference source tenant's name/voice:77 - `chat.welcomeMessage`, `chat.disclaimer`, `chat.featuredSubline`78 - `chat.firstMessageHost`, `chat.assistantLabel`79 - `chat.floatingDocsAriaLabel`80814. **Themes/pathways** — These are deeply tenant-specific:82 - `themes[]` slugs, titles, descriptions, coverImages83 - `frameworks.items[]` slugs and descriptions84 - `pathwayCta.*` content85 - `home.router.options[]` and `home.router.ctas[]`86875. **Organizations/partners** — `organizations.items[]` must be the new tenant's partners, not the source's88896. **Pricing** — `pricing.plans[]` may differ per tenant90917. **Author profile (EEAT)** — `authorProfile.*` must reflect the new author92938. **Hero** — `hero.*` heading, subheading, CTAs, imageUrl, backgroundImageUrl94959. **Content type descriptions** — `contentTypes.*` descriptions reference source tenant's domain969710. **Home page sections** — `home.*` section copy9899### Phase 3: Environment Variables100101Read `.env.local.example` and verify:1021031. `TENANT_ORG_ID` — Must be the new tenant's org UUID1042. `DATABASE_URL` — Same shared Supabase instance1053. `NEXT_PUBLIC_SUPABASE_URL` / `NEXT_PUBLIC_SUPABASE_ANON_KEY` — Same project1064. `OPENAI_API_KEY` / `OPENAI_MODEL` — If chat enabled1075. `OPENAI_VECTOR_STORE_ID` — Tenant-specific vector store for RAG1086. `AI_LAB_AGENT_URL` / `AI_LAB_AGENT_API_KEY` — If using external agent1097. `STRIPE_SECRET_KEY` / `NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY` — Tenant-specific Stripe account1108. `SENTRY_DSN` / `SENTRY_PROJECT` — Tenant-specific Sentry project1119. `COURSE_PREVIEW_HMAC_SECRET` — Must match studio if using course preview112113### Phase 4: Codebase Grep for Source Tenant Leaks114115Search the entire `src/` directory for hardcoded references to the source tenant:116117```118Grep for: "Alan Hirsch", "alan-hirsch", "alanhirsch", "Alan's", "mDNA", "APEST", "Forgotten Ways", "5Q", "Reframation"119```120121Exclude:122- `tenant.config.ts` (that's the config file itself)123- `node_modules/`, `.next/`, `_docs/`124- Comments in generated files125126Any hits in components, pages, or services are violations that must be fixed.127128### Phase 5: Middleware & Auth129130Read `src/middleware.ts` and verify:131- Protected route patterns still make sense for the new tenant132- No hardcoded paths specific to the source tenant133134### Phase 6: Vercel Deployment135136Check for Vercel project linkage:137- `.vercel/project.json` — Should reference the new tenant's Vercel project138- Environment variables must be set in Vercel dashboard for all required vars139140## Output Format141142Generate a structured migration report:143144```markdown145# Tenant Migration Report: [source] → [target]146147## Status Summary148- Database org: [PASS/FAIL/MISSING]149- Tenant config: [X/Y fields migrated]150- Env vars: [CONFIGURED/NEEDS ATTENTION]151- Source tenant leaks: [N violations found]152- Middleware/auth: [PASS/NEEDS REVIEW]153- Vercel deployment: [LINKED/NOT LINKED]154155## Critical (must fix before launch)1561. ...157158## Warnings (should fix)1591. ...160161## Passed1621. ...163164## Recommended Next Steps1651. ...166```167168## Important Rules169170- **Never modify the Drizzle schema** for migration purposes171- **Never create database rows** without explicit user approval172- **Use Supabase MCP** for all database queries — do not guess structure173- **If MCP is unavailable**, report what you can from code inspection alone and note which DB checks were skipped174- **Visual theming (globals.css) and image assets are out of scope** — handle those separately via `/color-audit`, `/asset-match`, or manual design work175- **The tenant.config.ts + .env.local are the two files that MUST change** for every migration — everything else is structural176- **Run `/tenant-check` after migration** to verify no hardcoded strings leaked through