CMS Chronicle #08: Multi-Site Admin One admin, many sites
A single cms-admin instance now manages multiple sites across multiple organizations. Supabase-inspired Sites Dashboard, org switcher, site-scoped everything — media, AI config, agents, settings. Plus the settings restructuring that made it possible.
The problem
Until today, each site needed its own cms-admin instance. Want to manage two sites? Run two Next.js servers on two ports. Three sites? Three servers. It worked, but it did not scale — and it was not how platforms like Supabase, Vercel, or Fly.io work.
The goal: one admin at localhost:3010 managing N sites, with runtime switching.
Site Registry
The foundation is a registry.json file stored in the admin's own data directory — separate from any site. It defines organizations and their sites:
{
"orgs": [
{
"id": "webhouse",
"name": "WebHouse",
"sites": [
{
"id": "webhouse-site",
"name": "WebHouse Site",
"adapter": "filesystem",
"configPath": "/path/to/cms.config.ts",
"contentDir": "/path/to/content",
"uploadDir": "/path/to/uploads"
},
{
"id": "landing",
"name": "Landing Page",
"adapter": "filesystem"
}
]
}
]
}
Each site has its own config, content directory, upload directory, and preview URL. The registry supports both filesystem and GitHub storage adapters — so a site's content can live in a Git repo instead of on disk.
Backwards compatibility
The critical constraint: existing single-site setups must keep working with zero changes.
When no registry.json exists, admin reads CMS_CONFIG_PATH from the environment — exactly as before. No switcher shown, no org dropdown, no Sites Dashboard. The multi-site layer is completely opt-in.
CMS_CONFIG_PATH set + no registry → single-site mode (as before)
CMS_CONFIG_PATH set + registry exists → multi-site mode
The refactor
This was the largest refactoring since the project started. 14 library files and 6 API routes all used process.env.CMS_CONFIG_PATH to derive a project directory and find the _data/ folder:
auth.ts— user storagebrand-voice.ts— brand personacockpit.ts— AI usage trackingagents.ts— agent configurationsai-config.ts— provider keysmcp-servers.ts— MCP connectionscuration.ts— curation queuerevisions.ts— document history- Plus six more.
Every single one was refactored to use a new getActiveSitePaths() helper that resolves paths from the active site's registry entry — or falls back to the env var in single-site mode.
The result: when you switch sites in the dashboard, media library shows that site's files, AI config shows that site's API keys, agents belong to that site, and preview opens that site's frontend.
Sites Dashboard
Inspired by Supabase's Projects view. A card grid showing all sites in the active organization:
- Site name, adapter type (Local / GitHub), preview URL
- Live stats: page count (documents with URLs) and collection count
- Status badges: Active, Local/GitHub
- More menu: Copy site ID, jump to Site Settings
- Click a card to enter that site's workspace
Org switcher
A dropdown in the header bar, left of the user avatar. Same pattern as our codepromptmaker app — Building2 icon, chevron, check mark on active org. Switch orgs and the Sites Dashboard updates to show that org's sites.
Settings restructuring
Multi-site forced us to properly separate what belongs to the site versus the user:
Site Settings (in sidebar) — things that differ per site:
- General: preview URL, trash retention, curation retention, dev toggles
- AI: provider API keys, model selection
- Brand Voice: site persona and tone
- MCP: external server connections
- Schema: collection definitions
- Team: members, roles, access (RBAC)
Account Preferences (in user menu) — things that follow the user:
- General: name, email, UI zoom
- Security: password change, 2FA (authenticator app)
- Access Tokens: personal API tokens
This is not just cosmetic. When you switch sites, Site Settings change. Account Preferences stay the same. That is the whole point.
What we validated
Two sites running side by side:
- WebHouse Site — 10+ collections, localhost:3009, full production content
- Landing Page — 1 collection with block-based sections, localhost:3010
Switching between them: media library shows the right files, preview opens the right frontend, AI agents belong to the right site, settings are independent. The cookie cms-active-site persists across logout/login.
What is next
- GitHub storage adapter as the third test site (content lives in a repo)
- Team/RBAC implementation in Site Settings
- "New site" creation flow from the dashboard
- Landing page build pipeline — rendering blocks to static HTML from CMS content