Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Deployment and Operations Guide

This guide covers deploying and operating Alloy — from single-developer SQLite to team PostgreSQL with integrations.


SQLite vs PostgreSQL: Which Backend?

CriteriaSQLitePostgreSQL
Best forSolo developer, small team, evaluationTeams, multi-tenant, production
SetupZero — single file, auto-createdRequires running PostgreSQL server
Multi-tenancySingle-tenant onlyFull multi-tenant with RLS
ConcurrencyLimited (single writer)High concurrency
BackupCopy the .db filepg_dump / replication
Migration pathExport via API, re-import into PGN/A

Start with SQLite if you’re evaluating Alloy or running it for personal use. Use PostgreSQL when you need multi-tenant isolation, team access, or production reliability.

Migrating from SQLite to PostgreSQL

There is no built-in migration tool. The recommended approach:

  1. Stand up a PostgreSQL instance and start Alloy pointed at it (migrations run automatically)
  2. Export your data from the SQLite instance using the API (list projects, tickets, etc.)
  3. Import into PostgreSQL via the API
  4. Switch your ALLOY_DATABASE_URL to the PostgreSQL connection string

SQLite Deployment

The simplest deployment — a single binary with a single database file.

# Start with defaults (creates ./alloy.db automatically)
cargo run -p alloy-cli -- serve

# Or with a prebuilt binary
alloy serve

Data location: By default, alloy.db is created in the current working directory. Override with:

ALLOY_DATABASE_URL=sqlite:///var/data/alloy.db alloy serve

Backup: Just copy the .db file while the server is stopped (or use SQLite’s .backup command for online backup):

cp alloy.db alloy.db.backup

PostgreSQL Deployment

The repository includes a docker-compose.yml that runs PostgreSQL 16 and MinIO (S3-compatible storage for attachments):

docker compose up -d

This starts:

  • PostgreSQL on port 5432 (user: postgres, password: postgres, database: alloy_dev)
  • MinIO on port 9000 (console on 9001, user: minioadmin, password: minioadmin)
  • minio-init creates the alloy-attachments bucket automatically

Then start Alloy pointed at PostgreSQL:

ALLOY_DATABASE_URL=postgres://postgres:postgres@localhost:5432/alloy_dev alloy serve

Connection string format

postgres://USER:PASSWORD@HOST:PORT/DATABASE

Multi-tenancy and RLS

PostgreSQL mode enables Row-Level Security (RLS) for full multi-tenant isolation:

  • Each request sets app.tenant_id via SET LOCAL in the transaction
  • RLS policies ensure tenants can only see their own data
  • This is transparent to the application — queries return only tenant-scoped rows
  • No data leaks between organizations, even if a bug skips application-level filtering

TLS / HTTPS

Alloy supports automatic TLS certificate provisioning via Let’s Encrypt using the ACME protocol. When TLS is enabled, the server listens over HTTPS with no reverse proxy required.

Enabling automatic TLS

Pass --tls-domain to alloy serve with the public domain name:

alloy serve --tls-domain api.example.com --tls-contact admin@example.com

This will:

  1. Automatically request a TLS certificate from Let’s Encrypt for api.example.com
  2. Cache the certificate on disk (default: ./acme_cache)
  3. Serve HTTPS on the configured port (default 3000)

CLI flags

FlagDescription
--tls-domainDomain to provision a certificate for. Omit for plain HTTP.
--tls-contactEmail address for Let’s Encrypt notifications (recommended).
--tls-stagingUse the Let’s Encrypt staging environment (for testing — avoids rate limits).
--tls-cache-dirDirectory to cache certificates. Default: ./acme_cache.

Environment variables (TLS)

VariableDefaultDescription
ALLOY_TLS_DOMAINDomain for automatic TLS (alternative to --tls-domain).
ALLOY_TLS_CONTACTContact email for Let’s Encrypt (alternative to --tls-contact).
ALLOY_TLS_STAGINGfalseUse staging environment (alternative to --tls-staging).
ALLOY_TLS_CACHE_DIR./acme_cacheCertificate cache directory (alternative to --tls-cache-dir).

Example: production HTTPS

alloy serve \
  --tls-domain api.example.com \
  --tls-contact ops@example.com \
  --port 443

Once running, verify with curl:

curl -s https://api.example.com/health

Expected response:

{"status":"ok"}

Example: staging / testing

Use --tls-staging to test certificate provisioning without hitting production rate limits:

alloy serve \
  --tls-domain staging.example.com \
  --tls-contact ops@example.com \
  --tls-staging

Docker with TLS

Mount a volume for certificate persistence across container restarts:

docker run -d \
  --name alloy \
  -p 443:443 \
  -v alloy-data:/data \
  -v alloy-certs:/certs \
  -e ALLOY_DATABASE_URL=sqlite:///data/alloy.db \
  alloy \
  serve --tls-domain api.example.com --tls-contact ops@example.com --tls-cache-dir /certs --port 443

Notes

  • DNS must resolve first: The domain must point to your server before ACME validation can succeed.
  • Port 443: Let’s Encrypt HTTP-01 challenge may require port 80 or 443 to be reachable. If running behind a firewall, ensure the challenge port is open.
  • Certificate renewal: Certificates are renewed automatically before expiration. Keep the cache directory persistent.
  • Without TLS flags: The server runs plain HTTP exactly as before — no TLS overhead.

Docker Deployment

Building the image

docker build -t alloy .

The multi-stage Dockerfile produces a minimal Debian-based image with just the alloy binary.

Running with SQLite

docker run -d \
  --name alloy \
  -p 3000:3000 \
  -v alloy-data:/data \
  alloy

Data is stored at /data/alloy.db inside the container. The volume mount persists it across container restarts.

Running with PostgreSQL

docker run -d \
  --name alloy \
  -p 3000:3000 \
  -e ALLOY_DATABASE_URL=postgres://user:pass@db-host:5432/alloy \
  -e ALLOY_JWT_PRIVATE_KEY_FILE=/secrets/private.pem \
  -e ALLOY_JWT_PUBLIC_KEY_FILE=/secrets/public.pem \
  -v /path/to/secrets:/secrets:ro \
  alloy

Volume mounts

MountPurpose
/dataSQLite database file (default ALLOY_DATABASE_URL=sqlite:///data/alloy.db)
/secretsJWT key files (if using file-based keys)

Environment Variables Reference

Core

VariableDefaultDescription
ALLOY_DATABASE_URLsqlite://alloy.dbDatabase connection string. Use sqlite://path for SQLite or postgres://... for PostgreSQL.
ALLOY_AUTO_MIGRATEtrueRun database migrations automatically on startup. Set to false to skip.
PORT3000TCP port the HTTP server listens on.
ALLOY_REGISTRATIONopenRegistration mode: open (anyone can register) or invite (invite-only).

Authentication (JWT)

VariableDefaultDescription
ALLOY_JWT_PRIVATE_KEY(required)Ed25519/RSA private key (PEM string) for signing JWTs.
ALLOY_JWT_PRIVATE_KEY_FILEPath to private key file (alternative to inline).
ALLOY_JWT_PUBLIC_KEY(required)Corresponding public key (PEM string) for verifying JWTs.
ALLOY_JWT_PUBLIC_KEY_FILEPath to public key file (alternative to inline).
ALLOY_JWT_ISSUERalloyJWT iss claim value.
ALLOY_JWT_AUDIENCEalloy-apiJWT aud claim value.
ALLOY_JWT_TTL_SECONDS3600JWT token lifetime in seconds.

S3 / Object Storage (Attachments)

VariableDefaultDescription
ALLOY_S3_ENDPOINTS3-compatible endpoint URL (e.g., http://localhost:9000 for MinIO).
ALLOY_S3_BUCKETalloy-attachmentsBucket name for file attachments.
ALLOY_S3_REGIONus-east-1S3 region.
ALLOY_S3_ACCESS_KEY_IDS3 access key.
ALLOY_S3_SECRET_ACCESS_KEYS3 secret key.

Security

VariableDefaultDescription
ALLOY_HTTPSfalseSet to true to mark cookies as Secure (requires TLS termination).
ALLOY_CORS_ORIGINS(permissive)Comma-separated list of allowed CORS origins.

Rate Limiting

VariableDefaultDescription
ALLOY_RATE_LIMIT_GLOBALGlobal requests per minute limit.
ALLOY_RATE_LIMIT_AUTHAuthenticated endpoint requests per minute.
ALLOY_RATE_LIMIT_LOGINLogin endpoint requests per minute.

Slack Integration

VariableDefaultDescription
ALLOY_SLACK_SIGNING_SECRETSlack app signing secret for verifying webhook requests.
ALLOY_SLACK_BOT_TOKENSlack bot OAuth token (starts with xoxb-).
ALLOY_SLACK_NOTIFICATION_CHANNELgeneralDefault Slack channel for notifications.
ALLOY_SLACK_DEFAULT_USER_IDFallback Slack user ID when user mapping is unavailable.

GitHub Integration

VariableDefaultDescription
ALLOY_GITHUB_WEBHOOK_SECRETSecret for verifying GitHub webhook payloads (HMAC-SHA256).

SCIM Provisioning

VariableDefaultDescription
ALLOY_SCIM_BEARER_TOKENBearer token for authenticating SCIM provisioning requests.
ALLOY_SCIM_ORG_IDOrganization ID to provision SCIM users/groups into.

MCP Server

VariableDefaultDescription
ALLOY_API_URL(required)Base URL of the Alloy API (e.g., http://localhost:3000).
ALLOY_API_TOKEN(required)API key for authenticating MCP requests (must start with alloy_live_ or alloy_test_).

TUI

VariableDefaultDescription
ALLOY_BASE_URLhttp://localhost:3000Alloy API URL for the TUI client.

Auto-Migration Behavior

On startup, Alloy checks ALLOY_AUTO_MIGRATE (defaults to true):

  • If true: Runs all pending migrations from the embedded migration set. Migrations are compiled into the binary — no external SQL files needed at runtime.
  • If false: Skips migrations entirely. Use this in environments where you run migrations as a separate step.

Migrations are idempotent — re-running an already-applied migration is a no-op. The migration runner detects the database backend (SQLite or PostgreSQL) and applies the correct dialect automatically.


MCP Server

The MCP server (alloy-mcp) lets AI assistants like Claude interact with Alloy. For complete setup instructions, configuration examples, and troubleshooting, see the dedicated MCP Guide.

For a full reference of available tools, parameters, and response formats, see the MCP Tools Reference.


Integrations Setup

Slack

  1. Create a Slack App at api.slack.com/apps
  2. Enable Event Subscriptions and set the request URL to https://your-alloy-host/api/v1/integrations/slack/events
  3. Enable Slash Commands (e.g., /alloy) with the request URL https://your-alloy-host/api/v1/integrations/slack/commands
  4. Under OAuth & Permissions, add bot scopes: chat:write, commands
  5. Install the app to your workspace and copy the Bot User OAuth Token
  6. Set environment variables:
    ALLOY_SLACK_SIGNING_SECRET=your_signing_secret
    ALLOY_SLACK_BOT_TOKEN=xoxb-your-bot-token
    ALLOY_SLACK_NOTIFICATION_CHANNEL=engineering  # optional
    

GitHub

  1. Create a GitHub App or use repository webhooks
  2. Set the webhook URL to https://your-alloy-host/api/v1/integrations/github/webhook
  3. Select events: push, pull_request, issues, etc.
  4. Generate a webhook secret and set:
    ALLOY_GITHUB_WEBHOOK_SECRET=your_webhook_secret
    

Okta SSO (OIDC)

SSO is configured per-organization via the API (not environment variables). To set up OIDC:

  1. In Okta, create a new Web Application with:
    • Sign-in redirect URI: https://your-alloy-host/api/v1/auth/sso/callback
    • Sign-out redirect URI: https://your-alloy-host
  2. Note the Client ID, Client Secret, and Issuer URL (e.g., https://your-org.okta.com/oauth2/default)
  3. Register the identity provider via the Alloy API for your organization:
    curl -X POST https://your-alloy-host/api/v1/orgs/{org_id}/identity-providers \
      -H "Authorization: Bearer $TOKEN" \
      -H "Content-Type: application/json" \
      -d '{
        "provider_type": "oidc",
        "provider_name": "okta",
        "issuer_url": "https://your-org.okta.com/oauth2/default",
        "client_id": "your-client-id",
        "client_secret": "your-client-secret"
      }'
    
  4. Users can then sign in via GET /api/v1/auth/sso/login?org={org_slug}

Troubleshooting

Port conflicts

Symptom: error binding to 0.0.0.0:3000: Address already in use

Solution: Either stop the other process using port 3000, or set a different port:

PORT=3001 alloy serve

Migration failures

Symptom: migration error or refinery errors on startup

Solutions:

  • Ensure the database is accessible (correct ALLOY_DATABASE_URL)
  • For PostgreSQL, verify the user has CREATE TABLE / ALTER TABLE permissions
  • If a migration was partially applied, check the refinery_schema_history table and fix manually
  • Set ALLOY_AUTO_MIGRATE=false to skip auto-migration and run migrations separately

Authentication errors

Symptom: 401 Unauthorized on API requests

Solutions:

  • Verify JWT keys are set: both ALLOY_JWT_PRIVATE_KEY (or _FILE) and ALLOY_JWT_PUBLIC_KEY (or _FILE) are required
  • For API key auth, ensure the key starts with alloy_live_ or alloy_test_
  • Check ALLOY_JWT_ISSUER and ALLOY_JWT_AUDIENCE match between token issuer and verifier
  • If using SSO, confirm the identity provider’s issuer_url is correct and reachable

Database connection issues (PostgreSQL)

Symptom: error connecting to database or connection timeouts

Solutions:

  • Verify PostgreSQL is running: pg_isready -h localhost -p 5432
  • Check the connection string format: postgres://user:password@host:port/database
  • Ensure the database exists: createdb alloy_dev
  • For Docker Compose: docker compose up -d postgres and wait for the health check

CORS errors

Symptom: Browser console shows CORS errors when calling the API

Solution: Set allowed origins:

ALLOY_CORS_ORIGINS=http://localhost:5173,https://your-app.com alloy serve

Slack integration not working

Symptom: Slash commands or events not reaching Alloy

Solutions:

  • Verify ALLOY_SLACK_SIGNING_SECRET matches your Slack app’s signing secret
  • Ensure webhook URLs are publicly reachable (use ngrok for local development)
  • Check that bot scopes include chat:write and commands

MCP server connection issues

Symptom: Claude Desktop can’t connect to the MCP server

Solution: See the troubleshooting section in the MCP Guide for detailed solutions covering connection errors, authentication issues, and configuration problems.