Supbuddy docs

Run multiple Supabase projects at once on one Mac, each with its own custom local domain.

Getting started

There are two ways to run Supbuddy. Use the macOS desktop app (steps below), or the command-line interface, which runs on macOS and Linux. For the CLI, install it with npx supbuddy@latest and jump to Command-line interface. The app and the CLI share the same state, so you can use either or both.

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. Requires an Apple Silicon Mac (M1/M2/M3/M4, arm64). The desktop app is macOS-only in v2, but the headless CLI runs on Linux too. See Command-line interface.

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, with no per-domain prompts and no browser warnings.

If Supbuddy detects an AI tool that ships its own JavaScript runtime (Claude Code, Cursor, Windsurf, Continue, Codex CLI, OpenCode, etc.) it will also offer to enable Bundled-runtime trust in the same first-run prompt. Those tools don't read the system Keychain (they carry their own Mozilla CA bundle), so without this setup the first OAuth/MCP connection to a *.test URL fails with unable to get local issuer certificate. Enable it once and Supbuddy keeps it in sync (including across yearly Caddy CA rotation). See the Bundled-runtime trust section under Settings → General for details.

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. One of:
    • host (default): Supabase runs on your host Docker on the stock ports. Only one host-mode Supabase project can run at a time (the standard supabase start constraint).
    • thin (lightweight, recommended): still your host Docker (no nested containers, no DinD), but Supbuddy gives each project its own port block and a unique Compose project_id, written into that project's supabase/config.toml. That's what lets several Supabase projects run at once on the shared daemon, each reached by name (api.<project>.test, studio.<project>.test). Apps bind a per-project loopback address so they keep their canonical ports too. Supbuddy owns those config.toml keys while the project is thin and restores them the moment you switch back to host. This is the lightest way to run many Supabase stacks together.
  • Active vs inactive: any project can be "active" (proxied + reachable) or inactive. Inactive projects keep their state, so flipping them on is a few seconds. Run as many active projects as you want.

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.

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 (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).

Supabase connection warning. When a project's app .env is missing the Supabase connection vars, or they've gone stale relative to the live target (e.g. after switching isolation, which republishes ports), the card shows a supabase env: not connected / supabase env: out of date pill. Click it to open Connect and push fresh values, or choose Ignore for this project.

  • Env mode chip: read-only Host or Thin label (matching the project's isolation mode). To switch 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.
  • Warnings chip: all project-level warnings (isolation drift, missing env vars, config issues, etc.) are consolidated into a single amber chip next to the enable toggle. Click it to see each warning item-by-item; it shows a spinner while Supbuddy re-checks the project.
  • Enable toggle (right edge): turn the project's proxy on/off without deleting it.
  • ⋯ actions menu (right edge): every project-level action: Edit project, Rescan, Re-check configs (re-runs the connection/env drift check for this project), Select folder, Export bundle, and Delete project.

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 6 conditional tabs. Below ~480 px, the strip collapses to a dropdown selector. (Project-level actions, like edit, rescan, re-check configs, select folder, export, and delete, are in the header's ⋯ menu, not a tab.)

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/thin switcher. A legacy project still on the old Isolated (VM) mode shows the migration wizard here instead (see Migrating a legacy Isolated (VM) project to Thin).

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.

Config editor: secret extraction. When you save a supabase/config.toml that contains a secret-bearing value inline (e.g. an SMTP password under [auth.email.smtp], an OAuth secret, or any *_key/auth_token), Supbuddy prompts before writing: it lists the detected secrets and lets you pick which gitignored env file to move them to (defaulting to the project-root .env.local). The value is written there and replaced in config.toml with an env(SUPABASE_…) reference, so secrets never land in git. Supbuddy injects those SUPABASE_-prefixed values back into the supabase start environment so the references resolve. (Saving a config with no inline secrets writes directly, with no prompt.)

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). Add-on services declared in supbuddy.addons.yml (see Add-on Compose services) appear here alongside the base stack and in get_compose_status over MCP.

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 with per-target scope (global / local), advanced options, and recent activity. See Per-project AI context sync for what global vs. local means.

Project-level actions (Edit, Rescan, Re-check configs, Select folder, Export bundle, Delete) are no longer a tab. They live in the header's ⋯ actions menu.


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.

Two ways to break that constraint, picked per project in the Supabase tab → Environment section:

Switch a project to Thin. Supbuddy assigns it a free port block (in the 55000+ range), writes those ports plus a unique Compose project_id into its supabase/config.toml, and runs supabase start on your normal host Docker, with no nested containers and nothing to pull. Several projects boot side by side this way; each is reached by name (api.acme.test, studio.acme.test, mail.acme.test). Switch back to Host and Supbuddy restores the original config.toml and stops just that project's stack.

This is the lightest, fastest option and the right default for most setups. One caveat: if your config.toml omits a port key (e.g. [inbucket] smtp_port), Supbuddy can't relocate a port that isn't declared, so that one service falls back to its stock port. That is fine for a single project, but spell those keys out if two Thin projects need the same service.

Running them all at once

Register as many projects as you want, and all of them can be "active" (proxied) at the same time. There's no limit. A Thin project's stack restarts in seconds; a Host project needs the standard supabase start cycle.

Migrating a legacy Isolated (VM) project to Thin

If you created a project in an older version of Supbuddy that used the now-retired Isolated (VM) mode, Supbuddy detects it on launch and offers a one-way, guided migration to Thin. The migration wizard appears in the Supabase tab's Environment section for any project still flagged as VM.

The migration is data-safe: Supbuddy dumps your Postgres data, starts a fresh Thin stack, restores the dump into it, and row-count-verifies the restore before tearing down the old VM container. No data loss. After migrating, the VM is gone and there's no way to switch back (but your data is intact in the Thin stack).

Over MCP, three tools handle the migration bridge:

  • list_pending_vm_migrations (read): lists all projects still on the legacy VM mode awaiting migration.
  • migrate_vm_to_thin ( { project_id } ) (write): starts the guided data-safe migration (dump, restore, verify).
  • finish_vm_migration ( { project_id } ) (write): tears down the old VM container after migration is verified. Returns an error if called before verification passes.

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 a 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 (workspaces field in root package.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 dependencyDefault port
next3000
vite5173
@remix-run/dev, @remix-run/serve3000
astro4321
nuxt, nuxt33000
@sveltejs/kit5173
@angular/core4200
@nestjs/core3000
express, fastify, koa, hono, @hono/node-server, elysia, polka, tinyhttpnone (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's warnings chip flags next.config: N origins missing; Server Action POSTs through Supbuddy mappings would 403 otherwise. Open the Apps tab (the chip's "open apps" jump) where the affected app shows the warning with a Fix button.

The Fix button opens a dialog with a paste-ready snippet and 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.

Vite allowedHosts audit

For Vite apps, Supbuddy reads your vite.config.{ts,mts,cts,js,mjs,cjs} and extracts server.allowedHosts. If a mapped host isn't covered, the warnings chip flags vite: N hosts blocked; Vite's dev server otherwise rejects proxied requests for unknown hosts with Blocked request. This host ("…") is not allowed. (403). A .your-project.local entry counts as covering every subdomain, so an existing wildcard suffix doesn't trigger a false warning.

Like the Next.js audit, the affected app's Fix button on the Apps tab opens a dialog with a paste-ready snippet and an Apply… button that previews a unified diff and writes server.allowedHosts into your vite.config (handling an existing allowedHosts array, an existing server block without it, or no server block at all; allowedHosts: true is left untouched). After write, Supbuddy rescans so the warning clears. Restart your dev server for the change to take effect; Vite does not hot-reload vite.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

ClientConfig fileTransport
Claude Code~/.claude.json (user) or <project>/.mcp.json (project)HTTP
Claude Desktop~/Library/Application Support/Claude/claude_desktop_config.jsonstdio 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.jsonHTTP

MCP tool surface

The MCP server has full read and write access:

  • Read tools (list_mappings, list_projects, get_health, get_compose_status, list_pending_vm_migrations, etc.), with env values and request bodies included.
  • get_client_capabilities and request_scope_elevation (scope discovery + user-approved grant).
  • read_env_file, tail_request_logs, watch_audit_log.
  • Write tools: create_mapping, delete_mapping (soft-delete), register_project, update_project, set_supabase_config_path, start_proxy, start_supabase, stop_supabase, restart_supabase, switch_isolation, migrate_vm_to_thin, finish_vm_migration, start_compose, stop_compose, restart_compose, scaffold_addons, seed_addons, write_env_file, copy_env_var, write_supabase_config.
  • Scripts tools (list_scripts, start_script, stop_script, restart_script, bookmark_script, tail_script_logs); see Scripts MCP tools below.
  • Extended Supabase tools: init_supabase, validate_supabase_config, list_supabase_backups, restore_supabase_backup, cancel_supabase_start, force_recreate_supabase, restart_supabase_container, get_supabase_analytics, set_supabase_analytics.
  • Bundle (export/import a project's full config): export_bundle, import_bundle, validate_bundle.
  • Connection / env-target workflow: preview_connection, get_env_targets, diff_env, apply_env, write_connection, test_connection, dismiss_connection_drift.
  • Host & network tools: bundled-runtime trust (get_trust_status, install_trust, remove_trust, detect_trust_tools, test_trust), Tailscale (get_tailscale_status, set_tailscale_key, remove_tailscale_key, test_tailscale), DNS (get_dns_status), CA (uninstall_ca), and port-forwarding (get_port_forwarding_status, set_port_forwarding, reload_port_forwarding).
  • tail_service_logs: streams a Compose/add-on service's container logs over SSE (like tail_request_logs but for container stdout/stderr).
  • watch_supabase: streams a project's live Supabase start/stop/restart progress over SSE: operation status, image-pull/service snapshots, and (for VM projects) raw log lines. Backs supbuddy supabase start --follow.
  • Multiple MCP clients can connect simultaneously. The same MCP-HTTP surface backs the headless CLI (see Command-line interface below).

Scopes: discovery & self-service elevation

Each MCP client holds a set of scopes (read, log_tail, mappings, projects, services, config, system, apply) chosen when it's added. A tool call that needs a scope the client lacks fails with scope_denied, whose payload now carries a user_message and details.remediation pointing at the fix.

  • get_client_capabilities ( { tool? } ) returns the calling client's granted_scopes and available_scopes. Pass a tool name to get { required_scope, required_feature, can_call, reason? } so an agent can pre-flight a call instead of probing by hitting scope_denied.
  • request_scope_elevation ( { scopes: [...] } ) asks the user to grant the named scopes. Supbuddy shows a blocking approval dialog; on approval the scopes are added to the client. Already-granted scopes short-circuit without a prompt.

You can also review and edit any client's scopes from the GUI: Settings → MCP → Clients lists each client's granted scopes inline and exposes a Scopes button that opens the same scope editor used when adding a client.

Registering a project via MCP

register_project takes a root_path (required), an optional label, and auto_scan (default true). It creates a host-mode project the same way the GUI's "Add project" flow does:

  • Derives a base domain as <slug>.<defaultTld> from the label (or the folder name), e.g. staffhub.test.
  • Records both the project path and rootPath so the project is visible to the proxy, scans, and file tools alike.
  • Scans the folder (unless auto_scan: false) for apps, services, scripts, and package manager.
  • Creates per-app subdomain mappings from the discovered apps (e.g. site.staffhub.test → :3400), derives the host service subdomains (api., studio., …), and reloads Caddy.

register_project creates a host-mode project. Use switch_isolation afterward to move it to thin mode.

Switching isolation over MCP

switch_isolation ( { project_id, target_mode: 'host' | 'thin', auto_start? } ) moves an existing project between host and thin mode. To-thin writes the per-project port block and project_id into supabase/config.toml and (unless auto_start: false) starts Supabase; to-host restores the original config.toml and stops that project's stack. It runs in the background and returns { started: true }; poll get_project (isolation) for the current mode.

A project can also be patched with update_project: its patch accepts name, enabled, domain, and isolation (it intentionally does not accept path/rootPath). Note that patching isolation only flips the flag; use switch_isolation to actually provision/tear down the port assignment.

Legacy VM migration over MCP

For projects still on the retired Isolated (VM) mode, three tools handle the one-way migration to Thin:

  • list_pending_vm_migrations (read): lists all projects still on the legacy VM mode, with their current vmState and migration readiness.
  • migrate_vm_to_thin ( { project_id } ) (write): starts the guided data-safe migration. It dumps Postgres data from the VM, starts a fresh Thin stack, restores the dump, and row-count-verifies before signalling completion. Returns { started: true }; poll get_project (migrationState) for progress.
  • finish_vm_migration ( { project_id } ) (write): tears down the old VM container after verification passes. Errors if called before the verify step completes.

Repointing a project's Supabase config

set_supabase_config_path ( { project_id, supabase_path } ) switches which supabase/config.toml a project uses, for monorepos that carry more than one (e.g. a repo-root config and an app-level one). supabase_path is the project-relative directory containing the supabase/ folder ("." for the repo root, e.g. "apps/getnightowls"). It persists the path, re-derives supabaseProjectId from the new config, and re-scans services. The previous stack's Docker volume is left intact (not deleted), so the switch is reversible; the response reports it under orphaned_previous_stack.

Moving a secret between env files

copy_env_var ( { source_path, source_key, target_path, target_key? } ) relocates a single variable from one env file to another (e.g. a value put in an app's .env.local that the stack actually injects from the repo-root .env.local). The value is read and written entirely inside the worker (it never crosses the MCP boundary and never appears in the audit log), so an agent can move a secret without it being printed. target_key defaults to source_key.

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.

Add-on Compose services

A project can declare extra Docker Compose services that Supbuddy discovers, merges, runs, health-checks, and tails alongside the managed stack: a Redis cache, a worker queue, a search engine, etc. Add-on services run on the host's shared Docker daemon in both host and thin isolation, with no extra setup needed.

Declaration files & merge precedence

Supbuddy looks for up to three Compose fragments in the project and merges them, later wins:

  1. docker-compose.yml: your base Compose file.
  2. docker-compose.override.yml: your own override, honored if present (standard Compose convention).
  3. supbuddy.addons.yml: Supbuddy-owned add-on fragment.

All present fragments are passed explicitly, e.g. docker compose -f docker-compose.yml -f docker-compose.override.yml -f supbuddy.addons.yml --project-name <pinned> …. The project name is pinned so the same set of containers is addressed every time. Add-on services join the Compose project's default network automatically; no extra network setup is needed for them to reach (or be reached by) the rest of the stack.

supbuddy.addons.yml format

A valid Compose fragment (a standard services: map) plus an optional Supbuddy-only x-supbuddy: extension block. A plain docker compose up ignores x-supbuddy:, so the file stays usable without Supbuddy. Today x-supbuddy supports a one-shot seed step:

services:
  redis:
    image: redis:7-alpine
    ports: ["6379:6379"]
x-supbuddy:
  seed:
    service: redis
    command: ["redis-cli", "ping"]   # explicit argv, runs once after services are healthy
    runOnce: true

The seed step runs once after the add-on services are up and healthy. It's idempotent, keyed by a signature of the seed spec, so it only re-runs if the spec changes (or you force it). It fires automatically on project start, and on demand via the seed_addons MCP tool.

MCP tools

  • scaffold_addons ( { project_id } ): scope config. Creates a starter supbuddy.addons.yml if the project doesn't have one. Never clobbers an existing file.
  • seed_addons ( { project_id, force? } ): scope services. Runs the declared x-supbuddy.seed step. Idempotent unless force: true.
  • tail_service_logs ( { project_id, service } ): scope log_tail. Streams a Compose/add-on service's container logs over SSE (like tail_request_logs, but for container stdout/stderr).
  • watch_supabase ( { project_id } ): scope log_tail. Streams a project's live Supabase start/stop/restart progress over SSE: operation (status + message), progress (image-pull/service snapshots), and log (raw lines, VM projects). The stream ends on a terminal status. Backs supbuddy supabase start --follow.

Scripts MCP tools

Scripts detected in a project (e.g. dev, build, test) are controllable over MCP:

  • list_scripts ( { project_id } ): scope read. Returns all detected scripts with their current status and bookmark state.
  • start_script ( { project_id, script } ): scope services. Starts the named script process.
  • stop_script ( { project_id, script } ): scope services. Stops the named script process.
  • restart_script ( { project_id, script } ): scope services. Stops then starts the named script process.
  • bookmark_script ( { project_id, script, bookmarked } ): scope services. Pins (bookmarked: true) or unpins a script in the Quick Access group.
  • tail_script_logs ( { project_id, script } ): scope log_tail. Streams the named script's stdout/stderr over SSE.

get_compose_status shape

get_compose_status ( { project_id } ) returns live per-service status, not just whether Compose is installed:

{
  "project_id": "…",
  "compose_installed": true,
  "running": true,
  "services": [
    { "name": "redis", "status": "running", "health": "healthy", "ports": ["6379:6379"], "image": "redis:7-alpine", "container_id": "…", "source": "addons" }
  ]
}

Each service's source is one of base | override | addons, telling you which fragment declared it.

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.md and CLAUDE.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.
  • .gitignore managed block, ignoring: .supbuddy/meta.json (volatile sync state), *.supbuddy-backup-* (rollback snapshots), and the per-editor skill files that are written locally (see scope below). The rest of .supbuddy/ is intended to be committed; AGENTS.md, CLAUDE.md, and .github/copilot-instructions.md are also kept committable since you may have hand-written content there alongside Supbuddy's managed block.

Global vs. local scope

The per-editor skill files are generic Supbuddy-owned pointers ("this is a Supbuddy project: read .supbuddy/, prefer the MCP tools"). For editors that expose a Supbuddy-owned global location, Supbuddy writes that pointer once, machine-wide instead of copying it into every project, so it isn't duplicated across all your repos. Project-specific data always stays local in .supbuddy/.

  • Claude Code → one global skill at ~/.claude/skills/supbuddy/SKILL.md. Cursor~/.cursor/skills/supbuddy/SKILL.md. The global skill self-scopes: it only acts when the working directory has a .supbuddy/ folder, and resolves the active project from that folder's meta.json.
  • All other targets (windsurf, continue, the AGENTS.md/CLAUDE.md/Copilot managed blocks, JetBrains) stay local: their "global" files are shared user files, so Supbuddy won't overwrite them.
  • Each target has a scope setting: auto (default: global for the Claude/Cursor skills, local for everything else), global, local (force per-project, useful if you commit the file for teammates), or off. A machine-global file is reference-counted across projects and removed automatically once no project uses it (on disabling sync, deleting a project, or switching that target back to local). Note: uninstalling Supbuddy (e.g. dragging it to the Trash on macOS) does not auto-remove these global files; delete them manually from ~/.claude/skills/supbuddy/ and ~/.cursor/skills/supbuddy/ if needed.
  • The always-loaded CLAUDE.md/AGENTS.md managed block stays local as a safety net so agents stay aware even if the on-demand global skill doesn't auto-activate.

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.

Command-line interface (CLI)

Everything the desktop app can do is also driveable headlessly from a terminal, with no GUI window. The CLI runs a daemon (the same worker process the GUI uses: Caddy proxy, DNS, Supabase/Compose lifecycle, MCP-HTTP) and a set of commands that attach to it over the local MCP-HTTP port. This is for SSH sessions, CI, tmux/server boxes, and scripting.

The binary is supbuddy, with a short alias sup. Run supbuddy help for the full usage list.

You can install the CLI on its own, without the desktop app:

npx supbuddy@latest        # asks to install the CLI globally (supbuddy + sup)

That command does nothing on its own except offer to put supbuddy and sup on your PATH. The CLI runs independently of the desktop app, so you can add the app later (or never). On a Mac the app installs the same two commands for you.

The daemon

supbuddy daemon --detach        # start the worker in the background
supbuddy status                 # daemon + proxy health
supbuddy stop                   # graceful shutdown

--detach backgrounds the daemon and prints its pid + ports. Foreground supbuddy daemon runs it attached (Ctrl-C shuts it down cleanly). On start the daemon writes a discovery file, daemon.json (mode 0600), into the shared state dir holding its pid, the Socket.IO port, the MCP-HTTP port, and a control token; every other command reads it to find and authenticate to the daemon, so you never pass ports or tokens by hand. Only one daemon may run per state dir; a second daemon start is refused.

The CLI and the desktop app share one state dir (~/Library/Application Support/Supbuddy/), so they manage the same projects, mappings, and settings. They must not run two workers against it at once: if you launch the desktop app while a CLI daemon is running, the app detects it and offers to stop the daemon and continue or quit. It never forks a competing worker (which would corrupt state.json).

Run on login (service)

supbuddy service install        # start-on-login (launchd on macOS, systemd-user on Linux)
supbuddy service status
supbuddy service uninstall

Commands

All app surfaces have a command. Names follow supbuddy <module> <action> [args] [--flags]. The main groups:

GroupExamples
Health / proxystatus, proxy status|start|stop|restart
Mappingsmap ls|add|get|set|enable|disable|rm|restore
Projectsproject ls|add|get|scan|set|enable|disable|rm|restore|env|refresh-context
Supabasesupabase start|stop|restart|status <proj> (add --follow to stream live progress), supabase config apply <proj> <file>
Composecompose up|down|restart|status|logs <proj> [svcs]
Scriptsscripts ls|start|stop|restart|logs|bookmark <proj> [script]
Isolationisolation switch <proj> <host|thin>, isolation pending-migrations, migrate start|finish <uuid>
Certificatesca status|install|uninstall
Env filesenv copy <src> <key> <target>, env write <path> <K=V>…
Settingssettings get, settings set --json <patch>
MCPmcp add [<agent>] (register Supbuddy into a coding agent: interactive, or --write/--print/--prompt), mcp ls, mcp revoke <id>, mcp approvals apply|cancel <id>
Host / networkconnect, trust, tailscale, dns, pf (port-forwarding)
Logslogs requests [-f], logs audit [-f], logs get <id>
Accountaccount, caps, addons scaffold|seed <proj>
Dashboardtui (alias dash)

Global flags: --json (machine-readable output), --yes (skip confirmations), --quiet, --url/--token (attach to a specific/remote daemon instead of auto-discovery), --state-dir (override the shared dir), --timeout, and -f/--follow for streaming log commands and live supabase start|stop|restart progress.

Destructive operations go through the same plan → apply gate as MCP (see Plan / apply for destructive tools); the CLI's control token is granted auto-apply, so they execute directly.

Live dashboard (TUI)

supbuddy tui                    # or: sup dash

supbuddy tui opens a full-screen terminal dashboard that attaches to the running daemon and shows live connection/proxy status, the project list (with each project's isolation, Supabase, and Compose state), the mapping count, and a tail of recent requests. Press r to refresh, q to quit. It needs a running daemon (supbuddy daemon --detach); if none is found it tells you so.

Settings reference

Open Settings via the gear icon top-right or by clicking the tray icon → Open Dashboard → gear. Five 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: host or thin for newly added projects. Default: host.
  • Auto-subdomain mapping: when on, services and apps detected during a project scan get mappings created automatically. Default: on.
  • Bundled-runtime trust: installs Supbuddy's local root CA into a place that apps with bundled JavaScript runtimes (Claude Code, Cursor, Windsurf, Continue, Codex CLI, OpenCode, …) actually read. These apps don't consult the system Keychain (they ship their own Mozilla bundle), so without this they fail OAuth/MCP/HTTPS calls to *.test with unable to get local issuer certificate. Default: prompted on first launch when one of those tools is detected.
    • macOS: writes ~/Library/LaunchAgents/com.cueplusplus.supbuddy.bundled-runtime-ca-trust.plist and calls launchctl setenv NODE_EXTRA_CA_CERTS / SSL_CERT_FILE / REQUESTS_CA_BUNDLE so GUI-launched apps inherit the right vars at process-start time.
    • Linux: writes ~/.config/environment.d/supbuddy-ca.conf (read by systemd-aware user sessions on GNOME/KDE/Sway/etc.).
    • Windows: per-user setx NODE_EXTRA_CA_CERTS / SSL_CERT_FILE / REQUESTS_CA_BUNDLE to HKCU\Environment.
    • All three env vars point at ~/Library/Application Support/Supbuddy/ca-bundle/current.crt (or the platform-equivalent), a cumulative concatenated PEM Supbuddy maintains. When Caddy rotates its root (yearly today, sometimes more), Supbuddy appends the new root automatically; long-running TLS contexts holding the old root keep working until the process restarts.
    • Test trust: runs an in-process HTTPS request against the first available *.test mapping with the same env vars set, to verify end-to-end without relaunching anything.
    • Conflict refusal: if NODE_EXTRA_CA_CERTS is already set to something else (corporate proxy, Zscaler), Supbuddy refuses to overwrite and surfaces the conflicting path. You can override with the explicit prompt that pops up on Install.
    • Quit and relaunch your AI tools after install: the env var only takes effect for newly-launched processes.

Network

  • HTTP port: default 8080.
  • HTTPS port: default 8443.
  • DNS port: default 5353.
  • Port forwarding: when on, adds a pfctl rule 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), volume sizes, image-cache controls.

MCP

  • Clients: list of connected clients. Each row has a actions menu: install, edit scopes, set-primary, rotate token, revoke.
  • 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).

AI Skills

Install Supbuddy's agent skill at the user level (machine-wide) so the agent sees Supbuddy in every repo without per-project setup. Each global-capable agent has a master on/off plus an autosync toggle (keeps the installed skill refreshed when Supbuddy updates it) and shows its install path + version.

  • Who can install at user level: only agents whose global file Supbuddy fully owns and that self-scope (act only when the working directory has a .supbuddy/): Claude Code (~/.claude/skills/supbuddy/SKILL.md) and Cursor (~/.cursor/skills/supbuddy/SKILL.md). The install is reference-counted under a synthetic __user__ ref so it persists independent of any project and is never pruned by the boot reconcile.
  • Master ↔ project: the AI Skills tab is the master (user-level). To commit a skill into a specific repo, use that project's AI Tools tab and set the target to Project (the old local scope, which writes into the repo for teammates); User there means the master install covers it.
  • Agents whose global file holds your own content (Claude CLAUDE.md, Codex AGENTS.md, Copilot, Windsurf, Continue, JetBrains) are project-level only: a machine-wide write there could clobber your config, so they're injected per-project instead.

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.
  • Projects: each project opens a submenu with Apps (click to open the mapped URL), Supabase services (status dot + open), and Scripts (your bookmarked scripts as a one-click Start <name> / Stop <name> toggle), plus Restart Supabase/Restart services and Show in Supbuddy.
  • 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.log in 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.
  • ca-bundle/current.crt: cumulative PEM containing every Caddy root that has ever been emitted. Used by Bundled-runtime trust as the target for NODE_EXTRA_CA_CERTS / SSL_CERT_FILE / REQUESTS_CA_BUNDLE. Real file (not a symlink) so Bun-bundled CLIs read it correctly.
  • ca-bundle/versioned/<sha>.crt: per-root snapshots for forensics.
  • Caddyfile: generated reverse-proxy config.
  • daemon.json: written while a headless CLI daemon is running (pid, Socket.IO + MCP-HTTP ports, control token); 0600, removed on shutdown. Used by supbuddy CLI commands to discover and authenticate to the daemon, and by the desktop app to detect a running CLI daemon at launch.
  • certs/: legacy CA from the pre-Caddy era (unused in current builds).

MCP-specific:

  • MCP client tokens (file-backed secret, mode 0600): ~/Library/Application Support/Supbuddy/secrets/mcp-<client-id>.secret
  • MCP audit log: under ~/Library/Application Support/Supbuddy/, capped at audit_cap entries (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".

"unable to get local issuer certificate" / "self signed certificate in certificate chain" from Claude Code, Cursor, MCP servers, or other AI tools

These tools ship their own bundled JavaScript runtime (Bun, Electron, pkg-bundled Node) and ignore the system Keychain. Open Settings → General → Bundled-runtime trust and click Install. Then fully quit and relaunch the AI tool; the env vars only take effect for newly-launched processes. Verify with launchctl getenv NODE_EXTRA_CA_CERTS (macOS); it should print ~/Library/Application Support/Supbuddy/ca-bundle/current.crt. If install is refused with a conflict warning, you already have NODE_EXTRA_CA_CERTS set (often a corporate proxy / Zscaler), so Supbuddy won't silently overwrite; use the override prompt or manually concatenate the two PEMs.

"Docker is not running. Please start Docker Desktop."

Compose and Supabase 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.

"Leftover host containers" / "isolation drift" warning on a project

Supbuddy flags isolation drift when a project's running containers don't match its configured isolation mode, for example a Host project with a stale thin-mode stack still running, or a Thin project with leftover host-mode containers. Switching isolation modes doesn't tear down the old layer, so those containers linger, waste resources, and can shadow the project's real stack. The warning appears in the warnings chip next to the enable toggle (click it to see each item; it shows a spinner while Supbuddy re-checks), as an entry in the issues counter, and as a notice on the Supabase tab listing the exact containers and any data volumes.

Guided cleanup. Open the Supabase tab → Clean up leftovers… to stop and remove the leftover containers. Data volumes are kept by default; deleting them is opt-in, and when the leftover copy looks newer than the active one, it requires an explicit choice and a backup (tarred to …/Supbuddy/backups/<project>-<timestamp>/). If you recently migrated a VM project, any leftover VM container from before migration can also be cleaned up from this flow.

If the leftover copy's data looks newer than the active one, the warning turns red; don't delete its volumes without first deciding which copy to keep. The Configure tab also shows a dismissible note when Supabase stacks are running on your host that Supbuddy doesn't manage at all (e.g. a plain supabase start).

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 → the client's ⋯ menu → Rotate token) 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 flags it in the warnings chip: open the Apps tab and hit Fix on the affected app for a paste-ready snippet, or Apply… to preview a unified diff and write the change to next.config directly. After applying, restart your dev server.

Vite dev server returns "Blocked request. This host is not allowed." (403)

Vite (v5+) rejects requests whose Host header isn't in server.allowedHosts, so a Vite app reached through a Supbuddy domain 403s until the host is allowed. Supbuddy detects this and flags vite: N hosts blocked in the warnings chip: open the Apps tab and hit Fix on the affected app for a paste-ready snippet, or Apply… to preview a diff and write server.allowedHosts into your vite.config directly. Restart the Vite dev server afterward; Vite does not hot-reload its config. A single .your-project.local entry covers every subdomain.

Project shows a red "PROXY ERROR" banner: domain resolves but won't load

After the proxy starts, Supbuddy runs an end-to-end reachability check: it resolves a project domain through the OS resolver and tries to connect to Caddy on the HTTPS port. If the name resolves but the connection fails, the project shows a red PROXY ERROR banner naming the likely cause (DNS, port-forwarding, or mDNS race) plus a recovery action.

The most common case: the domain resolves to 127.0.0.1 but port 443 won't connect because the elevated pfctl 443→8443 redirect drifted away (typically after a restart, so Caddy is up on 8443 with nothing forwarding 443). Click Retry; as of v2.3.6 it re-applies the port-forwarding rule (approve the sudo prompt). On older builds, toggle the proxy off→on instead. If LAN sharing is off, disregard any "LAN sharing / Bonjour" wording in the banner; the cause is the missing forward, not mDNS.

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>.

Wipe everything and start over

Quit Supbuddy, then:

# Wipe app data (state, certs, Caddyfile, logs, MCP tokens under secrets/)
rm -rf ~/Library/Application\ Support/Supbuddy

# Optional: remove the trusted CA
sudo security delete-certificate -c "Caddy Local Authority" /Library/Keychains/System.keychain

FAQ

Is Supbuddy free?

Yes. Supbuddy is free. Register as many projects and mappings as you want, with full HTTPS, full DNS, full Supabase isolation, and full read and write MCP access. There are no caps and no tiers.

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: 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.

Linux / Windows support?

The desktop app is macOS-only in v2. The headless CLI and daemon also run on Linux, where supbuddy service install registers a systemd-user start-on-login unit (macOS uses launchd). Windows is not supported. A few desktop code paths (certutil, update-ca-certificates) anticipate other platforms but are not tested there.

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.

How do I uninstall Supbuddy?

  1. Quit the app.
  2. Drag Supbuddy.app from /Applications to the Trash.
  3. Optional cleanup: see "Wipe everything and start over" above.
  4. Optional: sudo security delete-certificate -c "Caddy Local Authority" /Library/Keychains/System.keychain to 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.