Supbuddy docs
Run multiple Supabase projects at once on one Mac, each with its own custom local domain.
Getting started
1. Install
Download the latest .dmg from the download page. Drag Supbuddy.app into /Applications and launch it. Supbuddy is signed and notarized; macOS will not show a Gatekeeper warning. Apple Silicon (M1/M2/M3/M4) and Intel Macs are both supported. Windows and Linux are not supported in v2.
2. Trust the local Certificate Authority
On first launch, open the app and click Install Certificate when prompted. Supbuddy generates a local CA (Caddy's internal PKI) at ~/Library/Application Support/Supbuddy/caddy-data/caddy/pki/authorities/local/root.crt and installs it into your system Keychain via sudo security add-trusted-cert. macOS will ask for your password once. After this, every Supbuddy domain gets the green padlock automatically — no per-domain prompts, no browser warnings.
If you skip the prompt, you can re-trigger it any time from the Settings → Network tab.
3. Add your first project
Click Add project in the Configure tab and pick a project root folder (the one with package.json and/or supabase/config.toml). Supbuddy scans it and creates auto-mapped subdomains based on what it finds:
- Supabase Kong →
api.<project>.test - Supabase Studio →
studio.<project>.test - Supabase Inbucket / Mailpit →
mail.<project>.test - Each detected app (Next.js, Vite, etc.) →
<app-name>.<project>.test
The default TLD is .test. You can change it project-wide in Settings → General → Default TLD.
4. Start the proxy
Toggle the project on. Supbuddy starts Caddy on port 8443 (HTTPS) and starts its built-in DNS server on port 5353. If you want real ports 80/443 instead of 8080/8443, enable port forwarding in Settings → Network — Supbuddy adds a pfctl redirect rule (asks for sudo once).
Core concepts
Four things to understand:
- Project — a folder you registered. Holds detected apps (Next.js, Vite, etc.), detected services (Supabase stack, Docker Compose services), and a list of mappings.
- Mapping — a domain → port pair (e.g.
api.acme.test → 54321). Auto-generated mappings are tied to a detected service or app; you can also create manual ones. - Isolation mode — per-project. Either:
host(default): Supabase runs on your host Docker, sharing ports with the host. Only one host-mode Supabase project can run at a time (the standardsupabase startconstraint).vm(isolated): Supabase runs inside a Docker-in-Docker container exclusive to this project. This is what lets you run multiple Supabase projects simultaneously — each gets its own Postgres, its own Studio, its own port universe inside its DinD.
- Active vs inactive — Free tier keeps any 2 projects "active" (proxied + reachable) at once. Pro removes that cap. Inactive projects keep their state (DinD container exists, just stopped), so flipping them on is a few seconds.
Project cards (Configure tab)
Each registered project appears as a card in the Configure tab. Cards have a single-row header that's always visible and a tab-based body that expands on click.
Header
Reading left to right:
- Expand chevron + project name — click to expand/collapse the card.
- Status indicator — a single colored dot next to the project name aggregating the realtime state of every subsystem (VM, Supabase services, Compose, scripts, AI sync, port conflicts, next.config warnings). Red = error, amber = warning, green = at least one service running, muted gray = idle, animated cyan spinner = transitioning. Hover for a tooltip that lists each subsystem's state.
- Tech badges — e.g.
TurboRepo,Supabase(shown when detected). - Warning pills — amber
next.config: N origins missingand redport conflict: N(click to fix). Hidden when none. - Env mode chip — read-only
HostorIsolatedlabel. To switch isolation modes, open the Supabase tab and use the Environment section at the top. - Issues counter — red for errors, amber for warnings. Click to open the issues popover (see below). Hidden when there are no issues.
- Enable toggle (right edge) — turn the project's proxy on/off without deleting it.
Edit, Rescan, Select folder, Export bundle, and Delete are not in the header — they live in the Settings tab.
Issues popover
Clicking the issues counter opens a popover listing all current errors and warnings. Each issue shows a severity icon, title, optional detail, and a → open {tab} link. Clicking the link jumps to the relevant tab and closes the popover.
Body tabs (when expanded)
The body renders a flat tab strip with 7 conditional tabs. Below ~480 px, the strip collapses to a dropdown selector.
Apps (default tab)
Per-app rows are domain-first: domain → :port (with hover-revealed copy/open URL buttons), then app name + tech badge, then a flex spacer pushes hover-revealed edit / delete / access (LAN / Tailscale state) actions and the per-mapping toggle to the right edge. A Map CTA appears on hover for unmapped apps. Manual mappings scoped to this project (not auto-generated) are listed below under their own subheader.
Supabase (shown when Supabase is detected)
Environment section (top): host/isolated switcher. When isolated, shows the DinD VM lifecycle controls (Start VM / Stop VM) and a More menu containing Adjust resources and Reset VM.
Action bar: Start, Stop, Restart buttons; a first-class Connect button (cyan, opens the connection panel for .env generation / merge); and a More menu with Config editor and Details.
Service rows (read-only): status dot, service name, URL. No inline actions — lifecycle is driven by the action bar.
Compose (shown when Compose services are detected)
Action bar: Start, Stop, Restart. Service rows are read-only (status dot, name, URL).
Other (shown when non-Supabase, non-Compose services are detected)
Read-only service rows: status dot, name, URL.
Scripts (shown when scripts are detected)
Bookmarked scripts appear in a Quick Access group at the top; remaining scripts appear under Other Scripts. Per-script row: status dot, name, uptime, bookmark star, Start/Stop/Restart buttons. A search input appears when there are more than 5 scripts.
AI Tools
Wraps the project-context-sync panel — sync mode selector (Auto / Manual / Off), detected targets list, advanced options, and recent activity.
Settings
Explicit action buttons: Edit (rename/reconfigure), Rescan (re-detect apps and services), Select folder (move the project root), Export bundle (download a zip of the project's Supbuddy state), Delete (destructive, moves project to Trash).
Multiple Supabase projects (the main use case)
The reason Supbuddy exists. Stock Supabase CLI binds to fixed ports (54321 Kong, 54322 Postgres, 54323 Studio, 54324 Inbucket). Two projects on the same machine collide; you must supabase stop one before supabase start-ing the other.
How to run them all at once
- For each project: Add project, then open the project card → Supabase tab → Environment section → switch to Isolated.
- Supbuddy creates a per-project DinD container named
supbuddy-<project-id>, copies yoursupabase/folder into it, and runssupabase startinside. - Each project gets its own subdomain set under its project domain, e.g.
api.acme.test,studio.acme.test,db.acme.test:<port>for Postgres. - Postgres is exposed via Caddy's L4 (TCP) routing, so the host port is stable across restarts (one of the recurring pains with raw
docker run -p).
Free vs Pro for parallel projects
- Free: register unlimited projects, but only 2 can be "active" (proxied) at the same time. Toggle which 2.
- Pro: all projects can be active simultaneously. No limit.
The DinD containers themselves persist regardless of tier — toggling a project off just stops its container, toggling on resumes it.
Custom domains & TLDs
Every mapping resolves through Supbuddy's built-in DNS server on port 5353. By default the TLD is .test (an IETF-reserved TLD safe for local use). You can change the default in Settings → General → Default TLD to local, dev, or anything else; existing mappings are migrated to the new TLD on save.
For host resolution, Supbuddy does not use /etc/hosts for wildcards — it runs an actual DNS resolver. macOS's default resolver only queries port 53; Supbuddy installs a per-project resolver file under /etc/resolver/<project-domain> (e.g. /etc/resolver/myapp.local) pointing at 127.0.0.1:5353. macOS picks the longest-suffix-matching file, so per-project entries route reliably without colliding with reserved namespaces like .local (which Bonjour/mDNS owns). You'll be prompted for sudo the first time this changes.
LAN sharing
When LAN sharing is enabled (Settings → Network), Supbuddy binds Caddy to 0.0.0.0 instead of 127.0.0.1 and runs an mDNS responder so other machines on your local network can reach your dev servers via <hostname>.local. Useful for testing on your phone or another laptop without setting up Tailscale.
.local TLD + LAN sharing: macOS reserves the .local namespace for Bonjour/mDNS (RFC 6762), and macOS's TCP stack short-circuits self-connections to your own LAN IP via the loopback path without consulting pf — so the obvious "redirect lo0 → my LAN IP" trick can't fix it. Supbuddy's mDNS responder works around this by ignoring queries that originate from this machine, letting the OS resolver fall through to /etc/resolver/<project-domain> (which routes to 127.0.0.1 where Caddy listens). Other LAN devices still get answered with the LAN IP and reach you normally. The net result: .local works correctly both on this machine and on other LAN devices, with no manual configuration. If you previously worked around this by switching to .test, you can switch back.
If studio.<project>.local (or similar) doesn't load: open the Configure tab — a red banner will tell you whether it's a DNS, port-forwarding, or mDNS-race issue, with the specific recovery action.
Tailscale
If you have Tailscale installed and a Tailscale API key configured in Settings, Supbuddy can push split-DNS routes to your tailnet so any device on your tailnet resolves your Supbuddy domains. Optional, off by default.
Monorepo support
Supbuddy auto-detects these monorepo layouts when scanning a project root:
- Turborepo (presence of
turbo.json) - pnpm workspaces (
pnpm-workspace.yaml) - npm/yarn workspaces (
workspacesfield in rootpackage.json) - Common folder layouts:
apps/*,packages/*,services/*,sites/*
Each detected app gets its own subdomain. Supabase is searched for in the project root and these subdirectories: apps/*, packages/*, services/*, sites/*, db/, db/*, database/, database/*, packages/backend, packages/db, packages/database.
Detected app frameworks
Port detection looks for the framework dependency in package.json and combines that with: explicit -p/--port in the dev script, PORT= env in the dev script, or a config file read. If none of those resolve, the framework default is used:
| Framework dependency | Default port |
|---|---|
next | 3000 |
vite | 5173 |
@remix-run/dev, @remix-run/serve | 3000 |
astro | 4321 |
nuxt, nuxt3 | 3000 |
@sveltejs/kit | 5173 |
@angular/core | 4200 |
@nestjs/core | 3000 |
express, fastify, koa, hono, @hono/node-server, elysia, polka, tinyhttp | none — must be explicit in dev script |
Server Actions allowedOrigins audit
For Next.js apps, Supbuddy reads your next.config.{ts,mts,js,mjs,cjs} and extracts the hosts in experimental.serverActions.allowedOrigins. If a mapped subdomain is missing from that list, the project shows an amber warning with a paste-ready snippet — Server Action POSTs through Supbuddy mappings would 403 otherwise.
The dialog also has an Apply… button: click it to see a unified diff of the change Supbuddy will make to your next.config, then Confirm & write to apply it. Supbuddy handles the four common config shapes (existing allowedOrigins array, existing serverActions block without it, existing experimental block without serverActions, or no experimental at all). After write, Supbuddy rescans the project so the warning disappears immediately. Restart your dev server for the change to take effect — Next.js does not hot-reload next.config.
MCP setup (AI agents)
Supbuddy ships a built-in MCP server on http://127.0.0.1:9877/mcp with static Bearer-token auth. Five clients have one-click install; any other MCP-compatible tool can be configured manually with the same URL + token.
Open Settings → MCP → Add client, pick the client kind, and Supbuddy generates a token, edits the client's config file, and backs up the original (<file>.supbuddy-backup next to it).
Auto-install paths
| Client | Config file | Transport |
|---|---|---|
| Claude Code | ~/.claude.json (user) or <project>/.mcp.json (project) | HTTP |
| Claude Desktop | ~/Library/Application Support/Claude/claude_desktop_config.json | stdio shim via npx -y @supbuddy/mcp@latest |
| Cursor | ~/.cursor/mcp.json (user) or <project>/.cursor/mcp.json (project) | HTTP |
| Codex CLI | ~/.codex/config.toml (adds an [mcp_servers.supbuddy] block) | HTTP |
| Windsurf | ~/.codeium/windsurf/mcp_config.json | HTTP |
Tier gating for MCP tools
Free tier:
- Read tools (
list_mappings,list_projects,get_health, etc.) — full access. read_env_file— keys only, values redacted.tail_request_logs,watch_audit_log— request bodies stripped.- 1 connected MCP client at a time. A second client connecting will be rejected with
multi_client_on_free. - All write/destructive tools are blocked (
tier_locked).
Pro tier:
- Full read access (env values included, request bodies included).
- Write tools enabled:
create_mapping,delete_mapping(soft-delete),register_project,update_project,start_supabase,stop_supabase,restart_supabase,start_compose,stop_compose,restart_compose,read_env_file,write_env_file,write_supabase_config. - Multiple MCP clients can connect simultaneously.
Plan / apply for destructive tools
Tools that delete or mutate state (delete_mapping, delete_project, write_env_file, etc.) return a plan with a preview. The MCP client (or you, in the Activity panel) explicitly calls apply with the plan_id to execute. Plans expire after 5 minutes if not applied. Soft-deletes go to the Trash and are recoverable for 7 days.
Per-project AI context sync
Each project has a Context sync — AI tools panel, accessible via the AI Tools tab in the project card, that writes a project-scoped briefing to disk so AI agents working in that repo see your live mappings, services, and isolation state without having to ask. Files written:
.supbuddy/—README.md,mappings.md,services.md,project.md,mcp.md,do-not.md,docs.md. The full live snapshot, regenerated on each sync.AGENTS.mdandCLAUDE.md— a small managed block prepended (or updated in place) telling the agent which project this is and pointing it at.supbuddy/.- Editor skill files when detected:
.cursor/rules/supbuddy.mdc,.claude/skills/supbuddy/SKILL.md,.codeium/windsurf/rules/supbuddy.md,.continue/rules/supbuddy.md,.github/copilot-instructions.md,.idea/supbuddy.md. .gitignoremanaged block, ignoring:.supbuddy/meta.json(volatile sync state),*.supbuddy-backup-*(rollback snapshots), and the fully-owned per-editor skill files (.cursor/rules/supbuddy.mdc,.claude/skills/supbuddy/SKILL.md,.codeium/windsurf/rules/supbuddy.md,.continue/rules/supbuddy.md,.idea/supbuddy.md). The rest of.supbuddy/is intended to be committed;AGENTS.md,CLAUDE.md, and.github/copilot-instructions.mdare also kept committable since you may have hand-written content there alongside Supbuddy's managed block.
Sync modes per project:
- Auto — Supbuddy regenerates the files whenever mappings, services, or project state change.
- Manual only — files are only written when you click Sync now (or use the tray's Sync AI context for all projects).
- Off — nothing is written.
The collapsed header shows an at-a-glance status pill: mode (auto / manual / off), a colored dot for the last sync result, and a relative timestamp. Disabled targets (e.g. an editor whose folder isn't present) appear greyed out in the Detected targets list inside the panel.
Settings reference
Open Settings via the gear icon top-right or by clicking the tray icon → Open Dashboard → gear. Four tabs.
General
- Theme — dark or light.
- Auto-start at login — registers Supbuddy as a macOS login item. Default: on.
- Default TLD — applied to new auto-generated mappings. Existing mappings are renamed to the new TLD on save. Default:
test. - Default isolation —
hostorvmfor newly added projects. Default:host. - Auto-subdomain mapping — when on, services and apps detected during a project scan get mappings created automatically. Default: on.
- Default VM config — CPU / memory / disk for new isolated DinDs.
Network
- HTTP port — default 8080.
- HTTPS port — default 8443.
- DNS port — default 5353.
- Port forwarding — when on, adds a
pfctlrule mapping 80→HTTP port and 443→HTTPS port. Asks for sudo once. - LAN sharing — binds Caddy to
0.0.0.0+ starts mDNS responder. - Tailscale — paste a tailnet API key to enable split-DNS push.
- Install / Uninstall CA — installs Caddy's root cert into your Keychain. Uninstall is not yet implemented (manual remove via Keychain Access).
Storage
Trash retention (per-kind), DinD volume sizes, image-cache controls.
MCP
- Clients — list of connected clients with rotate / revoke / set-primary actions.
- Activity — audit log with Apply/Cancel/Undo on plan rows.
- Trash — soft-deleted mappings and projects, restorable for 7 days.
- Settings: server
enabled,port(default 9877),audit_cap(default 5000),trash_ttl_days(default 7).
Tray menu
The macOS menu bar tray icon opens a menu with:
- Status: … — current proxy state (running / idle).
- DNS Active (:5353) — shown when proxy is running.
- LAN Sharing (<ip>) — shown when LAN sharing is on.
- Tailscale (<ip>) — shown when Tailscale is connected.
- Start Proxy / Stop Proxy — opens the dashboard.
- Open Dashboard.
- Sync AI context for all projects — runs the project-context sync engine for every registered project (writes
.supbuddy/,CLAUDE.md,AGENTS.md, etc.). - Show Logs — reveals
main.login Finder. - Check for Updates... — manual update check (only enabled in packaged builds).
- Quit.
File locations
All under ~/Library/Application Support/Supbuddy/ on macOS:
main.log+main.log.1— app logs (rotates at 2 MB).state.json— persistent state (projects, mappings, settings, MCP clients, license).caddy-data/— Caddy's data dir (PKI, autosaves, certs).caddy-data/caddy/pki/authorities/local/root.crt— the local CA cert installed in your Keychain.Caddyfile— generated reverse-proxy config.certs/— legacy CA from the pre-Caddy era (unused in current builds).
MCP-specific:
- MCP client tokens (encrypted via Electron's
safeStorage):~/.config/Supbuddy/mcp/<client-id>.bin - MCP audit log: under
~/Library/Application Support/Supbuddy/, capped ataudit_capentries (default 5000).
Troubleshooting
Browser shows "Not secure" or certificate warning
The Caddy CA is not trusted. Open Settings → Network → Install Certificate. macOS will prompt for your password. After install, fully restart your browser (Cmd+Q, not just close window). Verify: Keychain Access → System keychain → search for "Caddy Local Authority".
"Docker is not running. Please start Docker Desktop."
Compose and isolated-mode features need Docker. Open Docker Desktop and wait until the whale icon stops animating.
"Docker Compose is not installed"
Compose v2 ships inside Docker Desktop. If you removed Docker Desktop and are using a standalone Docker daemon (e.g. Colima, Rancher), install compose: brew install docker-compose.
Project is in isolated mode but services show stopped after restart
On boot, Supbuddy reconciles each project's isolation state with the actual DinD containers on Docker. If your DinD container was removed (e.g. docker system prune -a), the project is automatically demoted to host mode. Re-toggle to isolated to recreate the container; your supabase/ folder is the source of truth so data persists if Postgres' volume survived.
Indicator shows "running" but the VM isn't
In v2.2.x and earlier, a Docker event for an unrelated host-mode Supabase container could falsely match a project that's in isolated mode but whose VM is stopped. Update to the latest version. Workaround: stop the host Supabase, then re-toggle the project off/on.
MCP client says "Invalid OAuth error" or "JSON Parse error: Unexpected EOF"
The MCP client is trying OAuth discovery and getting an empty 404. Either the token was lost (regenerate it in Settings → MCP → Rotate) or you're on a build older than the OAuth-probe fix. Update to the latest version; the server now answers OAuth discovery paths with a structured 404 instead of an empty body, and 401 responses include WWW-Authenticate: Bearer so the client doesn't fall back to OAuth.
MCP token disappeared after app restart
Fixed in recent builds. If you're on an older version, regenerate the token. Root cause was that addMcpClient didn't trigger state persistence; the client was held in memory only.
Server Actions return 403 in a Next.js app behind Supbuddy
Next.js's CSRF guard rejects POSTs whose Origin isn't in experimental.serverActions.allowedOrigins. Supbuddy detects this and shows an amber warning on the project — click it for a paste-ready snippet, or hit Apply… in the dialog to preview a unified diff and write the change to next.config directly. After applying, restart your dev server.
Port already in use (8080, 8443, 5353, 9877)
Default ports: HTTP 8080, HTTPS 8443, DNS 5353, MCP 9877. Change them in Settings → Network / Settings → MCP. Find what's holding a port: lsof -i :<port>.
Supabase fails to start in isolated mode
Open the Activity tab and watch the Supabase log stream during start. The most common cause is a config.toml port outside the range Docker is willing to bind on the host (e.g. ports below 1024 without root). Supbuddy reads the [db], [api], [studio], [inbucket] ports from your config.toml — keep them in the 30000+ range to avoid conflicts.
"Cannot start Supabase: isolation is enabled but the VM is not running"
Isolated projects keep supabase start inside their DinD container. If the VM (DinD) isn't running, Supbuddy refuses to fall back to host-mode supabase start — host mode binds raw ports 54321/54322 directly, which collides with Caddy's L4 DB-port pinning that fronts your other isolated projects on 54322–54399. Start the isolated environment first (project header → toggle on) and Supabase will start inside it. The Supabase ▶ button is also disabled for isolated projects whose VM is stopped.
Wipe everything and start over
Quit Supbuddy, then:
# Stop and remove all DinD containers
docker ps -a --filter "name=supbuddy-" -q | xargs -r docker rm -f
# Wipe app data (state, certs, Caddyfile, logs)
rm -rf ~/Library/Application\ Support/Supbuddy
# Wipe MCP tokens
rm -rf ~/.config/Supbuddy/mcp
# Optional: remove the trusted CA
sudo security delete-certificate -c "Caddy Local Authority" /Library/Keychains/System.keychain
FAQ
Is Supbuddy free?
Yes — the free tier lets you register unlimited projects with 5 mappings and 2 active projects at a time, full HTTPS, full DNS, full Supabase isolation, and read-only MCP access (1 client at a time). Pro removes the caps and adds MCP write access. See pricing.
Does Supbuddy send my data anywhere?
No. Caddy, the DNS server, and the MCP server all run locally on your Mac. The only outbound traffic is: license validation (when activating a Pro license), Tailscale split-DNS push (only if you enabled it), auto-update checks (GitHub Releases), and Google Analytics on the marketing site (not the desktop app). The desktop app does not send telemetry.
Can I work offline?
Yes. The app works fully offline once the CA is trusted and projects are registered. License validation runs offline using a cached signed token.
Linux / Windows support?
Not in v2. The app is macOS-only. Some code paths (hosts file, certutil, update-ca-certificates) anticipate Windows and Linux but they are not tested or supported.
How many seats does a Pro license cover?
Each Pro license includes exactly 1 seat by default — Pro Monthly, Pro Lifetime, and Gold all start at one seat. A seat is bound to one Mac at a time; you can move it by deactivating the device on the Account page and activating on another Mac.
Need more than one seat? Click Add extra seat on the Account page when all current seats are in use. Pricing for extra seats: $3/mo per seat (subscription) or $29 one-time (lifetime).
Can I use my own TLD?
Yes. Set any TLD in Settings → General → Default TLD. Supbuddy installs /etc/resolver/<project-domain> files that tell macOS to query our DNS server for that project's domain. Avoid TLDs that actually resolve on the public internet (.com, .net, etc.) — your browser will hit the real site for cached entries.
What happens if I delete a project?
The project moves to the Trash (visible in Settings → MCP → Trash) for 7 days, then is permanently deleted by the sweep timer. Restoring brings back the project record and all its mappings. Note: the underlying DinD container is also stopped on delete; restoring the project does not auto-recreate the container — toggle isolated mode again to do that.
How do I uninstall Supbuddy?
- Quit the app.
- Drag Supbuddy.app from
/Applicationsto the Trash. - Optional cleanup — see "Wipe everything and start over" above.
- Optional:
sudo security delete-certificate -c "Caddy Local Authority" /Library/Keychains/System.keychainto remove the trusted CA.
Where do I report a bug?
Email support with your version (visible at the bottom of the Settings popover) and the relevant lines from ~/Library/Application Support/Supbuddy/main.log.