Deployment
Run the Patchcord server with Docker or standalone Python.
If you do not want to run the centralized server at all and only need local CLI or desktop agents, use Supabase Direct instead. That path connects agents straight to Supabase and does not support web chats.
Prerequisites
- Python 3.10+
- A Supabase project (free tier works)
- A domain with HTTPS (for production)
Docker is recommended but not required.
Quick deploy
git clone https://github.com/ppravdin/patchcord.git
cd patchcord
cat > .env.server << 'EOF'
SUPABASE_URL=https://your-ref.supabase.co
SUPABASE_KEY=your-service-role-key
PATCHCORD_PORT=8000
PATCHCORD_PUBLIC_URL=https://patchcord.yourdomain.com
EOF
# Apply the schema first on a fresh Supabase project.
python3 -m patchcord.cli.migrate https://your-ref.supabase.co <db_password>
# Run from your local clone of https://github.com/ppravdin/patchcord. manage_tokens auto-loads .env.server here.
# Create bearer tokens for each agent.
python3 -m patchcord.cli.manage_tokens add --namespace myproject frontend
python3 -m patchcord.cli.manage_tokens add --namespace myproject backend
python3 -m patchcord.cli.manage_tokens add --namespace myproject ds
docker compose --env-file .env.server up -d --build
curl https://patchcord.yourdomain.com/health
If the schema is not already installed, apply it before creating tokens or deploying:
python3 -m patchcord.cli.migrate https://your-ref.supabase.co <db_password>
Or run the SQL files in migrations/ in order.
What success looks like
{ "status": "ok", "service": "patchcord" }
Environment variables
Required
| Variable | Description |
|---|---|
SUPABASE_URL | Supabase project URL |
SUPABASE_KEY | Supabase service-role key |
PATCHCORD_PUBLIC_URL | Public-facing base URL (for OAuth discovery) |
Agent bearer tokens are managed in the database, not via environment variables. Use:
python3 -m patchcord.cli.manage_tokens add [--namespace ns] agent_id
python3 -m patchcord.cli.manage_tokens add [--namespace ns] --token <existing> agent_id
python3 -m patchcord.cli.manage_tokens list
python3 -m patchcord.cli.manage_tokens revoke <token>
Optional
| Variable | Default | Description |
|---|---|---|
PATCHCORD_PORT | 8000 | Server listen port |
PATCHCORD_HOST | 0.0.0.0 | Server bind address |
PATCHCORD_PUBLIC_URL | http://localhost:{port} | Public URL for OAuth discovery |
PATCHCORD_MCP_PATH | /mcp | Main MCP endpoint |
PATCHCORD_BEARER_PATH | /mcp/bearer | Bearer-only MCP endpoint |
PATCHCORD_STATELESS_HTTP | true | Remove in-memory session coupling |
PATCHCORD_NAME | patchcord | Service name in health output |
PATCHCORD_DEFAULT_NAMESPACE | default | Default namespace for bearer-token agents without an explicit namespace |
PATCHCORD_ACTIVE_WINDOW_SECONDS | 3600 | Presence activity window |
PATCHCORD_PRESENCE_WRITE_INTERVAL_SECONDS | 10 | Presence write throttle |
PATCHCORD_OAUTH_DEFAULT_NAMESPACE | default | Default namespace for OAuth web clients |
PATCHCORD_OAUTH_CLIENTS | (empty) | Explicit OAuth client_id=namespace:agent mappings |
PATCHCORD_KNOWN_OAUTH_CLIENTS | (built-in) | Extend known OAuth clients: agent:domain1,domain2;agent2:domain3 |
PATCHCORD_OAUTH_ACCESS_TOKEN_TTL_SECONDS | 86400 | OAuth access-token lifetime |
PATCHCORD_OAUTH_REFRESH_TOKEN_TTL_SECONDS | 31536000 | OAuth refresh-token lifetime |
PATCHCORD_ATTACHMENTS_BUCKET | attachments | Storage bucket for attachments |
PATCHCORD_ATTACHMENT_MAX_BYTES | 10485760 | Attachment size limit |
PATCHCORD_ATTACHMENT_URL_EXPIRY_SECONDS | 86400 | Signed URL lifetime |
PATCHCORD_ATTACHMENT_ALLOWED_MIME_TYPES | text/*,... | Allowed attachment MIME types |
PATCHCORD_RATE_LIMIT_PER_MINUTE | 100 | Per-token request limit |
PATCHCORD_ANON_RATE_LIMIT_PER_MINUTE | 20 | Per-IP request limit for unauthenticated requests |
PATCHCORD_RATE_BAN_SECONDS | 60 | Persisted ban duration |
PATCHCORD_CLEANUP_MAX_AGE_DAYS | 7 | Message retention |
PATCHCORD_CIRCUIT_BREAKER_SECONDS | 300 | Circuit breaker timeout for DB subsystem recovery |
PATCHCORD_CLEANUP_INTERVAL_HOURS | 6 | Cleanup schedule |
Breaking change: env token mappings removed
This release no longer reads bearer tokens from:
PATCHCORD_TOKENSPATCHCORD_TOKEN_FILETOKEN_*
All bearer tokens now live in the database (bearer_tokens table).
If you are upgrading an existing deployment, migrate before deploy:
- Update
.env.serverwithSUPABASE_URLandSUPABASE_KEY. - From your local clone of
ppravdin/patchcord, import each live token into the database:
python3 -m patchcord.cli.manage_tokens add --namespace myproject --token "$OLD_FRONTEND_TOKEN" frontend
python3 -m patchcord.cli.manage_tokens add --namespace myproject --token "$OLD_BACKEND_TOKEN" backend
- If you want to rotate tokens instead of preserving them, omit
--token, save the new values, and update every client/project registration before deploy. - Deploy this version only after all active client tokens exist in
bearer_tokens.
Docker Compose
The included docker-compose.yml runs with:
read_only: true- dropped capabilities
- memory and PID limits
- a health check
Port conflicts
If port 8000 is taken:
PATCHCORD_PORT=8100
Then deploy with:
PATCHCORD_PORT=8100 docker compose up -d --build
Compose host-port interpolation still comes from the shell or repo-root .env, not from .env.server alone.
Stateless HTTP
PATCHCORD_STATELESS_HTTP=true is the recommended setting. FastMCP's default stateful Streamable HTTP mode keeps MCP session IDs only in process memory — clients hit Session not found after a server restart when they reuse an old mcp-session-id. Stateless mode removes that failure mode entirely.
Running without Docker
If you prefer to run the server directly:
git clone https://github.com/ppravdin/patchcord.git
cd patchcord
python3 -m venv .venv
.venv/bin/pip install -r requirements.txt
# Apply schema
.venv/bin/python -m patchcord.cli.migrate https://your-ref.supabase.co <db_password>
# Create bearer tokens
export SUPABASE_URL=https://your-ref.supabase.co
export SUPABASE_KEY=your-service-role-key
.venv/bin/python -m patchcord.cli.manage_tokens add --namespace myproject frontend
# Run
SUPABASE_URL=https://your-ref.supabase.co \
SUPABASE_KEY=your-service-role-key \
PATCHCORD_PUBLIC_URL=https://patchcord.yourdomain.com \
.venv/bin/python -m patchcord.server.app
The server listens on 0.0.0.0:8000 by default. Put a reverse proxy (nginx, Caddy, Cloudflare Tunnel) in front for HTTPS.
All environment variables from the configuration table apply the same way.
This section is still centralized server mode. If you want no server process at all, see Supabase Direct.
HTTPS with Cloudflare
- Point the DNS record at your server
- Use Full or Full (strict)
- Let Cloudflare terminate TLS
- Set
PATCHCORD_PUBLIC_URL=https://patchcord.yourdomain.com
Updating
If you are upgrading from env-backed bearer tokens, complete the migration first.
git pull
python3 -m patchcord.cli.migrate <supabase_url> <db_password>
docker compose up -d --build
OAuth state lives in Supabase, so web clients survive container restarts.
Client endpoints
- Default MCP endpoint:
https://patchcord.yourdomain.com/mcp - Bearer-only endpoint:
https://patchcord.yourdomain.com/mcp/bearer
Bearer-token clients can still use /mcp; /mcp/bearer is the dedicated bearer-only path.
OAuth deployment stance
Patchcord supports:
- explicit client mappings with
PATCHCORD_OAUTH_CLIENTS - known-client detection, extended with
PATCHCORD_KNOWN_OAUTH_CLIENTS - derived fallback from
client_namefor otherwise-unknown clients
For internet-exposed deployments, prefer explicit mappings for sensitive identities and treat known-client detection as convenience, not your only trust boundary.
Attachments
upload_attachment()creates the bucket on first use if needed- Files are stored as
namespace_id/agent_id/timestamp_filename get_attachment()only accepts signed URLs for the configured host and bucket
Plugin Hook Endpoint
Claude plugin hooks use:
curl -H "Authorization: Bearer <token>" \
"https://patchcord.yourdomain.com/api/inbox?status=pending&limit=1"
Supported statuses: pending, read, deferred.
Claude Code Plugin
Install with:
claude plugin marketplace add /absolute/path/to/patchcord
claude plugin install patchcord@patchcord-marketplace
Pair the plugin with project-local .mcp.json. Do not rely on global PATCHCORD_* shell exports. See Plugin docs for details.
Expected behavior:
- Patchcord project: statusline and hook active
- Unrelated project: plugin no-ops
Storage backend
Patchcord uses Supabase for:
- PostgreSQL through PostgREST
- Supabase Storage for attachments
All interaction is via raw HTTP calls; there is no supabase-py dependency.
Supabase can also be self-hosted if you want the same architecture under your own control.
Common failure modes
Wrong identity
Usually caused by:
- wrong token
- wrong namespace casing
- ancestor
.mcp.json - stale session after config change
- stale user-scope MCP registration
Failed to connect
Check:
curl https://patchcord.yourdomain.com/health- bearer token in project config
- correct public URL
- fresh client session after config changes
Monitoring
curl https://patchcord.yourdomain.com/health
docker ps --filter name=patchcord-server
docker logs patchcord-server --tail 100
docker logs patchcord-server 2>&1 | grep "OAuth client registered"
Troubleshooting
| Problem | Fix |
|---|---|
| Port conflict | Set PATCHCORD_PORT in .env.server and shell env or repo-root .env |
| OAuth metadata uses HTTP instead of HTTPS | Set PATCHCORD_PUBLIC_URL to your HTTPS URL |
Session not found errors | Use PATCHCORD_STATELESS_HTTP=true (default) |
| Clients lose connection after restart | Expected with stateful mode; use stateless |
Stop hook shows as error | Cosmetic; matches Claude Code issue #12667 |
mcp package import fails | Python 3.10+ required. Check python3 --version. |
Supabase pooler postgres.ref username fails | libpq misparses the dot. Use DSN format with percent-encoding: postgres%2Eprojectref |
| Supabase pooler hostname wrong | Prefix varies by region (aws-0 vs aws-1). Check your actual pooler hostname in Supabase dashboard. |
| Web clients don't see new tools after deploy | ChatGPT and claude.ai cache tool schemas per session. Start a new chat. |