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

VariableDescription
SUPABASE_URLSupabase project URL
SUPABASE_KEYSupabase service-role key
PATCHCORD_PUBLIC_URLPublic-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

VariableDefaultDescription
PATCHCORD_PORT8000Server listen port
PATCHCORD_HOST0.0.0.0Server bind address
PATCHCORD_PUBLIC_URLhttp://localhost:{port}Public URL for OAuth discovery
PATCHCORD_MCP_PATH/mcpMain MCP endpoint
PATCHCORD_BEARER_PATH/mcp/bearerBearer-only MCP endpoint
PATCHCORD_STATELESS_HTTPtrueRemove in-memory session coupling
PATCHCORD_NAMEpatchcordService name in health output
PATCHCORD_DEFAULT_NAMESPACEdefaultDefault namespace for bearer-token agents without an explicit namespace
PATCHCORD_ACTIVE_WINDOW_SECONDS3600Presence activity window
PATCHCORD_PRESENCE_WRITE_INTERVAL_SECONDS10Presence write throttle
PATCHCORD_OAUTH_DEFAULT_NAMESPACEdefaultDefault 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_SECONDS86400OAuth access-token lifetime
PATCHCORD_OAUTH_REFRESH_TOKEN_TTL_SECONDS31536000OAuth refresh-token lifetime
PATCHCORD_ATTACHMENTS_BUCKETattachmentsStorage bucket for attachments
PATCHCORD_ATTACHMENT_MAX_BYTES10485760Attachment size limit
PATCHCORD_ATTACHMENT_URL_EXPIRY_SECONDS86400Signed URL lifetime
PATCHCORD_ATTACHMENT_ALLOWED_MIME_TYPEStext/*,...Allowed attachment MIME types
PATCHCORD_RATE_LIMIT_PER_MINUTE100Per-token request limit
PATCHCORD_ANON_RATE_LIMIT_PER_MINUTE20Per-IP request limit for unauthenticated requests
PATCHCORD_RATE_BAN_SECONDS60Persisted ban duration
PATCHCORD_CLEANUP_MAX_AGE_DAYS7Message retention
PATCHCORD_CIRCUIT_BREAKER_SECONDS300Circuit breaker timeout for DB subsystem recovery
PATCHCORD_CLEANUP_INTERVAL_HOURS6Cleanup schedule

Breaking change: env token mappings removed

This release no longer reads bearer tokens from:

  • PATCHCORD_TOKENS
  • PATCHCORD_TOKEN_FILE
  • TOKEN_*

All bearer tokens now live in the database (bearer_tokens table).

If you are upgrading an existing deployment, migrate before deploy:

  1. Update .env.server with SUPABASE_URL and SUPABASE_KEY.
  2. 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
  1. If you want to rotate tokens instead of preserving them, omit --token, save the new values, and update every client/project registration before deploy.
  2. 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

  1. Point the DNS record at your server
  2. Use Full or Full (strict)
  3. Let Cloudflare terminate TLS
  4. 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_name for 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

ProblemFix
Port conflictSet PATCHCORD_PORT in .env.server and shell env or repo-root .env
OAuth metadata uses HTTP instead of HTTPSSet PATCHCORD_PUBLIC_URL to your HTTPS URL
Session not found errorsUse PATCHCORD_STATELESS_HTTP=true (default)
Clients lose connection after restartExpected with stateful mode; use stateless
Stop hook shows as errorCosmetic; matches Claude Code issue #12667
mcp package import failsPython 3.10+ required. Check python3 --version.
Supabase pooler postgres.ref username failslibpq misparses the dot. Use DSN format with percent-encoding: postgres%2Eprojectref
Supabase pooler hostname wrongPrefix varies by region (aws-0 vs aws-1). Check your actual pooler hostname in Supabase dashboard.
Web clients don't see new tools after deployChatGPT and claude.ai cache tool schemas per session. Start a new chat.