Self-hosted hosting for the apps your team builds with Claude Code. Each app gets its own Supabase backend (Postgres database, Auth, REST API, Edge Functions) and its static frontend, all served from a single VM. Developers ("app builders") don't use a UI to deploy: they ask Claude Code to "deploy the app to the platform", through an MCP server and a Claude Code plugin. They inspect their apps in a multi-project Supabase Studio. Admins see every app, in Studio and in an admin console.
Status: alpha. Done: app provisioning, deployment through MCP and GitHub Actions, Studio access for app builders, the admin console, Storage and Realtime, app emails and company SSO, backups and restore, the production installer with HTTPS, upgrades and the security linter. It runs locally, and installs on a VM with a real domain (docs/INSTALL.md). Known limitations and remaining work: docs/TECH_DEBT.md and Deploying.
More docs:
- docs/ARCHITECTURE.md: architecture and design decisions (tenancy, routing, Studio, MCP tools, CI deployment, security safeguards);
- docs/INSTALL.md: production installation (domain, TLS, GitHub, SMTP, backups), hardening, upgrades;
- docs/USER_JOURNEYS.md: personas, main journeys, product decisions;
- docs/TECH_DEBT.md: known debt and limitations;
- apps/control-plane/README.md: control plane, MCP tools, admin REST API, admin CLI;
- apps/admin/README.md: admin console (shadcn-admin-kit);
- plugin/README.md: Claude Code plugin.
Studio, for app builders (one Studio for all apps; each builder only sees the apps they are a member of):
Projects: alice only sees todo-app |
Table editor |
|---|---|
![]() |
![]() |
| App users (the app's own GoTrue) | A deployed app: todo-app |
![]() |
![]() |
Admin console, for admins (every app, its releases, members and audit trail; all screens in apps/admin/screenshots):
| Apps | App overview |
|---|---|
![]() |
![]() |
| Releases | Audit log |
![]() |
![]() |
flowchart TB
user(["👤 App user"])
builder(["👤 App builder"])
admin(["👤 Admin"])
claude["Claude Code<br/>+ platform plugin"]
gha["GitHub Actions<br/>(app repo)"]
builder -- "asks to build and deploy" --> claude
claude -- "git push" --> gha
subgraph vm["Platform VM"]
caddy["Caddy<br/>HTTPS, routing by host name"]
subgraph platform["Platform services"]
cp["Control plane<br/>MCP server · deploy API · admin API"]
console["Admin console"]
studio["Studio<br/>(one instance, all apps)"]
end
subgraph apps["App backends"]
front["Static frontends"]
perapp["GoTrue + PostgREST<br/>(one pair per app)"]
shared["Storage · Realtime<br/>Edge Functions<br/>(shared by all apps)"]
end
pg[("Postgres<br/>platform DB + one DB per app")]
end
user -- "<app>.domain" --> caddy
builder -- "studio.domain<br/>(own apps only)" --> caddy
admin -- "admin.domain<br/>studio.domain (all apps)" --> caddy
claude -- "MCP<br/>api.domain" --> caddy
gha -- "deploy (GitHub OIDC)<br/>api.domain" --> caddy
caddy --> platform
caddy --> apps
cp -. "provisions, deploys,<br/>configures routes" .-> apps
apps --> pg
platform --> pg
classDef persona fill:#fde68a,stroke:#b45309,color:#000
class user,builder,admin persona
- App users use the deployed apps on
<app>.<domain>: the static frontend and its Supabase APIs (/auth/v1,/rest/v1,/storage/v1,/realtime/v1,/functions/v1) share the same origin. - App builders ask Claude Code to build and deploy their apps. The plugin talks to the control plane through MCP, and deployments go through GitHub Actions (or directly from Claude Code on local installs). Builders inspect their own apps in Studio.
- Admins see every app, in Studio and in the admin console.
- The control plane provisions each app (a Postgres database, its own GoTrue and PostgREST containers, a tenant in the shared services), deploys it, and configures Caddy's routes. Details: docs/ARCHITECTURE.md §3.
apps/
control-plane/ Hono API + MCP server + admin API + provisioning (Docker, Caddy, Postgres)
admin/ admin console: shadcn-admin-kit SPA, served by the control plane on admin.<domain>
edge-main-worker/ Deno main worker routing Edge Functions to the right app
studio/ vendored supabase-studio-multi-head, patched for per-app access
packages/
shared/ zod schemas and types shared by the API and the MCP server
sql/ template database, per-app roles, linter queries
deploy-cli/ platform-deploy: checks or deploys an app repo (GitHub OIDC in CI, or a PAT)
plugin/ Claude Code plugin (build, deploy and admin-deploy skills + MCP config)
ci/ reusable GitHub Actions deploy workflow, and an example caller for app repos
deploy/ docker-compose stack ("plat"), Caddy bootstrap, install.sh, upgrade.sh
examples/todo-app/ example app: personal todo list (Vite + React + supabase-js)
examples/notes-app/ example app: notes shared between users (Vite + React + supabase-js)
- macOS or Linux, with Docker (Docker Desktop: 8 GB of RAM or more allocated)
- Node 24 and pnpm 10 (
corepack enable) - Deno 2 (Edge Functions unit tests only)
openssl,curl- Claude Code, to deploy apps through the plugin
- About 16 GB of free RAM on the host to build Studio (Next.js build, ~8 GB peak)
*.localhost resolves to 127.0.0.1 out of the box in browsers, in curl and on Linux, so no DNS setup is needed. On macOS, Node does not resolve these names (ENOTFOUND) and /etc/hosts has no wildcard: the deploy CLI (and the plugin, which ships it) resolves them itself, and the e2e tests need the same shim:
export NODE_OPTIONS="--import $PWD/packages/deploy-cli/src/localhost-dns.ts"pnpm install
deploy/install.shinstall.sh is safe to rerun. It:
- generates the stack secrets in
deploy/.env, points the platform SMTP to the local mail catcher (mailpit,SMTP_HOST=mailpit), and adds a mock OpenID Connect provider for the company SSO and a test S3 store (COMPOSE_PROFILES=mail,sso-test,s3-test); - builds the Studio image on the first run, and the control plane image (which includes the admin console) (
STUDIO_BUILD=1 deploy/install.shforces a rebuild); - starts the
platdocker compose project; - initializes the Postgres cluster (template database,
platformdatabase); - creates the first admin, and saves their personal access token (
PLATFORM_TOKEN) and password (PLATFORM_ADMIN_PASSWORD, for Studio and the admin console) indeploy/.env.local.
Both files are gitignored.
To start with sample data (two app builders, two deployed example apps, two app users; see Test users), add --seed. It is safe to rerun:
deploy/install.sh --seedTo stop or restart the stack:
docker compose -p plat -f deploy/docker-compose.yml stop
docker compose -p plat -f deploy/docker-compose.yml start| URL | What |
|---|---|
| http://api.localhost:8100/mcp | MCP server: browser sign-in from Claude Code (OAuth), or a PAT (Authorization: Bearer <PAT> or X-Platform-Token: <PAT>). Its OAuth token endpoint is announced on http://localhost:8100/oauth/token: Claude Code only sends token requests to HTTPS or loopback hosts |
| http://studio.localhost:8100 | Studio (platform login) |
| http://admin.localhost:8100 | Admin console, admins only (PLATFORM_ADMIN_EMAIL / PLATFORM_ADMIN_PASSWORD from deploy/.env.local); its REST API is under /api |
http://<slug>.localhost:8100 |
Each app: frontend at /, Supabase APIs at /auth/v1, /rest/v1, /storage/v1, /realtime/v1, /functions/v1 |
| http://localhost:8025 | Mailpit (local installs): every email sent by the platform and its apps (sign-up confirmations, password resets, invitations, notices) |
| http://localhost:8090 | Mock OpenID Connect provider (local installs): the "company IdP" for trying the company SSO; it signs in any email (see Company SSO) |
127.0.0.1:54350 |
Postgres (supabase_admin, password POSTGRES_PASSWORD in deploy/.env) |
| http://127.0.0.1:2030 | Caddy admin API |
| User | Role | Credentials |
|---|---|---|
admin@example.com |
admin, sees every app | PLATFORM_ADMIN_PASSWORD (Studio and admin console) and PLATFORM_TOKEN (MCP, admin API) in deploy/.env.local |
alice@example.com (seed) |
app builder, sees todo-app only |
alice-password (Studio, and the browser sign-in from Claude Code), or SEED_ALICE_TOKEN (a PAT) in deploy/.env.local |
bob@example.com (seed) |
app builder, sees notes-app only |
bob-password (Studio, and the browser sign-in from Claude Code), or SEED_BOB_TOKEN (a PAT) in deploy/.env.local |
demo-user@example.com (seed) |
app user of todo-app (3 todos) and notes-app (3 notes, 2 of them shared with demo-user-2) |
demo-password, on http://todo-app.localhost:8100 and http://notes-app.localhost:8100 |
demo-user-2@example.com (seed) |
app user of notes-app, with one note shared read-only and one shared for editing |
demo-password, on http://notes-app.localhost:8100 |
Seed accounts exist only after deploy/install.sh --seed. Their passwords are fixed development credentials for the local stack only.
To create other app builders for manual testing, use the admin console (Users, with a password: that is all a builder needs), or the CLI inside the control plane container:
docker exec plat-control-plane node src/cli.ts create-user carol@example.com developer # prints a PAT
docker exec plat-control-plane node src/cli.ts set-password carol@example.com --generate # prints a password
docker exec plat-control-plane node src/cli.ts add-member <slug> carol@example.com collaboratorAn app builder only sees, in Studio and through MCP, the apps they are a member of.
todo-app (examples/todo-app) and notes-app (examples/notes-app) are two different example apps. Each is a separate instance with its own database and its own user accounts.
- todo-app, as an app user: open http://todo-app.localhost:8100 and sign in as
demo-user@example.com/demo-password. You can add, check and delete todos; the "x of y done" counter comes from thesummaryEdge Function. You can also sign up with a new account (the seed turns email confirmation off for its example apps; on other apps, open the confirmation email in mailpit, http://localhost:8025): it won't seedemo-user's todos (RLS), and it won't exist innotes-app. - notes-app, sharing between users: open http://notes-app.localhost:8100 and sign in as
demo-user@example.com/demo-password. "My notes" lists three notes: "Team offsite ideas" (shared read-only withdemo-user-2), "Release checklist" (shared for editing) and "Personal reminders" (private). Open a note to edit it and to manage its sharing: share it withdemo-user-2@example.com, change the permission, or remove the share. Sharing with an email that has no account in the app is refused. Then sign out, sign in asdemo-user-2@example.com/demo-passwordand open "Shared with me": the shared notes are there, with their permission. You can edit "Release checklist" (the editor then shows you as the last editor), but "Team offsite ideas" is read-only, and you can neither delete a shared note nor reshare it. A new account sees no notes at all.node apps/control-plane/scripts/verify-notes-app.tsruns the same checks against the API. - As an app builder, in Studio: sign in to http://studio.localhost:8100 as alice (only
todo-appis listed) or bob (onlynotes-app). Users are under Auth > Users; the data is under Table editor (todosfortodo-app;notes,note_sharesandprofilesfornotes-app). The admin sees every app. - As an app builder, with Claude Code: install the plugin without
PLATFORM_TOKEN(see Deploy an app with Claude Code), sign in asalice@example.com/alice-passwordin the browser when Claude Code asks, then ask for instance "show me the logs of todo-app" or "add a priority column to the todos and redeploy". As alice,notes-appis reported as not found. The admin console's Tokens screen lists the connection ("Claude Code connection"), and revoking it makes Claude ask to sign in again. With a PAT instead:export PLATFORM_TOKEN=<SEED_ALICE_TOKEN>before starting Claude Code.
Open http://admin.localhost:8100 and sign in with PLATFORM_ADMIN_EMAIL / PLATFORM_ADMIN_PASSWORD from deploy/.env.local (app builders are refused). It lists every app with its owners, linked repository, last release and flags (deployed outside CI, schema drift, accepted lints), and lets you stop, start or delete an app, roll back a deployment, manage its members, and manage platform accounts and tokens; every action is in the audit log. Screenshots: apps/admin/screenshots. Details: apps/admin/README.md.
The platform can delegate its logins (Studio, admin console) to the company's OpenID Connect provider, and apps can let their users sign in with it. Local installs run a mock provider that signs in any email. To try it:
- In the admin console, open SSO and set: issuer
https://idp.sso.test:8443, discovery URLhttp://idp:8090/.well-known/openid-configuration, client IDplatform, client secretplatform-secret, then check Enabled and save. - Open http://studio.localhost:8100, click Sign in with company SSO, type
alice@example.com(seed) or your admin email in the mock provider's form: you are signed in to Studio as alice (an unknown email is refused, unless "Unknown users" creates developers). The admin console's login page has the same button, for admins. - For an app's users, ask Claude (or call
configure_auth) to enable company SSO on the app (sso: {provider: "company"}); the app then signs in withsupabase.auth.signInWithOAuth({ provider: "custom:company" }).
Details, security checks and limits: apps/control-plane/README.md.
-
Install the plugin (details in plugin/README.md):
claude plugin marketplace add "$PWD" claude plugin install platform@platform-localAt the first use, the
deployskill asks you to sign in: type/mcp, chooseplugin:platform:platform, then Authenticate; the browser opens on the platform's login page (an account of the test users, e.g. alice), then asks you to allow the access. To use a PAT instead (admins, scripts),export PLATFORM_TOKEN=...(fromdeploy/.env.local) before starting Claude Code. -
Copy the example app outside the repo, then ask Claude to deploy it:
cp -R examples/todo-app /tmp/todo-app && cd /tmp/todo-app && git init -q && git add -A && git commit -qm init claude "deploy the app to the platform"
On a local install (DIRECT_DEPLOY=everyone, no CI configured), the deploy skill takes the direct path: it creates the app, writes and commits .platform.json, checks the secrets the functions read, runs platform-deploy check (migrations dry run with the security linter, build), then platform-deploy deploy (migrations, Edge Functions, frontend, smoke test), and prints the app URL (for instance http://todo-app.localhost:8100) and its Studio link.
In production (
DIRECT_DEPLOY=admins,CI_ALLOWED_WORKFLOWSset), the same request takes the CI path: the skill creates or links the GitHub repository, installs the workflow that calls the platform's reusable workflow, opens a PR and watches the GitHub Actions run; direct deployment is reserved to admins (admin-deployskill, with a reason). See docs/USER_JOURNEYS.md B2 and A10, and docs/ARCHITECTURE.md §6.
Without Claude, the same deployment runs from a terminal with the deploy CLI (it reads .platform.json, which the skill writes):
PLATFORM_URL=http://api.localhost:8100 PLATFORM_TOKEN=... node packages/deploy-cli/src/main.ts deploy --dir /tmp/todo-appIn GitHub Actions, the app repo calls the reusable workflow ci/.github/workflows/deploy.yml (example caller: ci/examples/platform.yml), which runs the same CLI with a GitHub OIDC token; see packages/deploy-cli/README.md.
The stack must be running, and deploy/.env.local must exist.
pnpm typecheck # all workspace packages (Studio's upstream typecheck excluded)
pnpm --filter @platform/control-plane test # unit tests (access policies, login throttling, backup retention, admin API…) and the plugin's script tests
pnpm --filter @platform/control-plane e2e # every end-to-end suite except Studio: MCP, CI API, platform-deploy, plugin, admin API, Storage and Realtime, pooler, emails, SSO, OAuth, backups, linter, TLS
pnpm --filter @platform/control-plane e2e:studio # Studio: login and throttling, CSRF, per-app filtering, bypass attempts
pnpm e2e:admin # admin REST API, load metrics, then the admin console in Chromium (Playwright)
pnpm --filter @platform/control-plane e2e:sso # company SSO with the mock IdP (also part of e2e)
pnpm --filter @platform/control-plane e2e:lints # security linter rules and stored results (also part of e2e)
pnpm --filter @platform/control-plane e2e:tls # on-demand TLS ask endpoint (also part of e2e); HTTPS checks on a production-mode stack
node apps/control-plane/scripts/verify-notes-app.ts # seeded notes-app: sharing rules, as its app users (after --seed)
(cd apps/edge-main-worker && deno test --allow-read --allow-write)The end-to-end suites create throwaway apps and users and delete them at the end. E2E_KEEP=1 keeps them for inspection.
deploy/install.sh --production --domain <domain> --acme-email <email> --admin-email <email> installs the platform on
a Linux VM: HTTPS on api., admin., studio. and <app>.<domain> with certificates obtained on demand by Caddy
(Let's Encrypt) for the hosts the control plane approves, publishing through GitHub Actions only
(DIRECT_DEPLOY=admins), no test service. deploy/upgrade.sh upgrades it (backups first, apps recycled without
downtime, verification, rollback instructions). The whole procedure (prerequisites, DNS, firewall, GitHub
organization, SMTP, backups and the master key, first login, hardening checklist, upgrades and measured downtime) is in
docs/INSTALL.md.
Not yet done: centralized logs (Loki), and a CI run against a real GitHub organization (the CI path has only run against a simulated GitHub: the e2e suite simulates the runner's OIDC token endpoint). Production blockers are tracked as P1 in docs/TECH_DEBT.md.
MIT, except the vendored Studio in apps/studio/, which stays under its
Sustainable Use License.







