# Configuration reference

> Environment variables across API, web gateway, realtime, and ai-agent: JWT, storage, plugins, encryption, cache TTLs, and internal keys.

- Repository: Paca-AI/paca
- GitHub: https://github.com/Paca-AI/paca
- Human docs: https://grok-wiki.com/public/docs/paca-ai-paca-f238b2ab3d25
- Complete Markdown: https://grok-wiki.com/public/docs/paca-ai-paca-f238b2ab3d25/llms-full.txt

## Source Files

- `deploy/.env.production.example`
- `deploy/.env.dev.example`
- `services/api/.env.example`
- `services/ai-agent/.env.example`
- `services/realtime/.env.example`
- `apps/mcp/.env.example`

---

---
title: "Configuration reference"
description: "Environment variables across API, web gateway, realtime, and ai-agent: JWT, storage, plugins, encryption, cache TTLs, and internal keys."
---

Paca reads configuration from environment variables at process startup. Docker Compose stacks pass a single deploy-level `.env` file into each service; host-side development uses per-service `.env` files copied from the `*.env.example` templates. The API validates required keys before listening; the realtime and ai-agent services fail fast on missing values.

## Configuration sources

| Layer | File | Used by |
| --- | --- | --- |
| Production / standalone deploy | `deploy/.env.production.example` → `.env` | `deploy/docker-compose.prod.yml` |
| Local dev stack | `deploy/.env.dev.example` → `deploy/.env.dev` | `deploy/docker-compose.dev.yml` |
| API (host run) | `services/api/.env.example` | `go run` / `air` on the API |
| Realtime (host run) | `services/realtime/.env.example` | `bun run` on realtime |
| AI agent (host run) | `services/ai-agent/.env.example` | Python worker on the host |
| MCP client | `apps/mcp/.env.example` | `@paca-ai/paca-mcp` via `npx` |

The install script (`scripts/install.sh`) generates a production `.env` with random secrets for `JWT_SECRET`, `AGENT_API_KEY`, and `INTERNAL_API_KEY`.

## Cross-service key relationships

Three pre-shared secrets tie services together. Mismatches cause auth failures that are easy to misdiagnose.

```mermaid
flowchart LR
  subgraph api["API"]
    AGENT_API_KEY
    ENCRYPTION_KEY
  end
  subgraph agent["AI agent"]
    PACA_API_KEY
    INTERNAL_API_KEY
    ENCRYPTION_KEY_A["ENCRYPTION_KEY"]
  end
  subgraph mcp["Built-in MCP (in agent container)"]
    PACA_API_KEY_MCP["PACA_API_KEY"]
    PACA_GATEWAY_URL
  end
  AGENT_API_KEY -->|"must equal"| PACA_API_KEY
  PACA_API_KEY -->|"injected as"| PACA_API_KEY_MCP
  ENCRYPTION_KEY -->|"must equal"| ENCRYPTION_KEY_A
```

| Pair | API / compose name | AI agent name | Purpose |
| --- | --- | --- | --- |
| Agent MCP auth | `AGENT_API_KEY` | `PACA_API_KEY` | Static `X-API-Key` accepted by the API as the built-in agent bot; injected into the hardcoded `paca` MCP server inside agent conversations |
| Encryption | `ENCRYPTION_KEY` | `ENCRYPTION_KEY` | AES-256-GCM for plugin secrets and encrypted LLM API keys in Postgres |
| Internal HTTP | — | `INTERNAL_API_KEY` | Protects ai-agent internal conversation endpoints via `X-Internal-Token` header (not exposed through the gateway) |

<AccordionGroup>
<Accordion title="Generate production secrets">

```bash
# JWT signing secret (API)
openssl rand -hex 32

# Agent ↔ API pre-shared key (set AGENT_API_KEY on API; same value as PACA_API_KEY on ai-agent)
openssl rand -hex 32

# AI agent internal endpoint protection
openssl rand -hex 32

# AES-256 encryption key — 64 lowercase hex chars (32 bytes)
openssl rand -hex 32
```

</Accordion>
</AccordionGroup>

## Deploy and compose variables

These variables live in the top-level `.env` consumed by Compose. They configure infrastructure, image pins, and ports rather than a single application binary.

### Image pins

| Variable | Default | Description |
| --- | --- | --- |
| `PACA_API_IMAGE` | `pacaai/paca-api:latest` | API container image |
| `PACA_WEB_IMAGE` | `pacaai/paca-web:latest` | Web SPA image (production) |
| `PACA_REALTIME_IMAGE` | `pacaai/paca-realtime:latest` | Realtime Socket.IO image |
| `PACA_AI_AGENT_IMAGE` | `pacaai/paca-ai-agent:latest` | AI agent worker image |

### Runtime mode and ports

| Variable | Default | Services | Description |
| --- | --- | --- | --- |
| `ENVIRONMENT` | `production` | API (`ENV`), realtime (`NODE_ENV`) | `production` enables secure cookies and structured JSON logging in realtime |
| `GATEWAY_PORT` | `80` | gateway | Host port mapped to nginx |
| `AI_AGENT_PORT` | `8082` | ai-agent | Host port for direct agent HTTP (internal debugging; not the public gateway) |
| `LOG_LEVEL` | `info` | realtime, ai-agent | Pino / Python log level |

### PostgreSQL (bundled container)

| Variable | Default | Description |
| --- | --- | --- |
| `POSTGRES_DB` | `paca` | Database name for the bundled Postgres service |
| `POSTGRES_USER` | `paca` | Database user |
| `POSTGRES_PASSWORD` | — | **Required** in production compose |
| `DATABASE_URL` | Derived from Postgres vars | Full DSN; override for external Postgres (`--scale postgres=0`) |

### Valkey / Redis

| Variable | Default | Services | Description |
| --- | --- | --- | --- |
| `REDIS_URL` | `redis://valkey:6379/0` | API, realtime | Cache, pub/sub fan-out, agent trigger streams |
| `VALKEY_URL` | Same as `REDIS_URL` in compose | ai-agent | Compose maps `REDIS_URL` → `VALKEY_URL` |

All three application services must point at the **same** Valkey instance.

### Public URL and CORS

| Variable | Dev equivalent | Used by | Description |
| --- | --- | --- | --- |
| `PUBLIC_URL` | `PUBLIC_HOST` | API, realtime CORS | Externally reachable base URL (no trailing slash). Used for plugin callbacks, GitHub webhook registration, and presigned storage URL rewriting |
| `CORS_ORIGINS` | Falls back to `PUBLIC_URL` | realtime | Comma-separated browser origins allowed for Socket.IO |
| `VITE_ALLOWED_HOST` | — | web (dev only) | Hostname (no scheme) for Vite `allowedHosts` when using tunnels or custom domains |

<ParamField body="PUBLIC_URL" type="string" required>
Full public URL where users reach Paca, e.g. `https://paca.example.com` or `http://localhost:8080`. When unset, GitHub webhook auto-registration returns `GITHUB_WEBHOOK_URL_REQUIRED`.
</ParamField>

## API service (`services/api`)

Loaded by `config.Load()` from environment (optional `.env` via `godotenv`). Missing required variables produce a single error listing all gaps.

### Server

| Variable | Required | Default | Description |
| --- | --- | --- | --- |
| `PORT` | No | `8080` | HTTP listen port |
| `ENV` | No | `development` | `development` or `production` |
| `PUBLIC_URL` | No | `""` | External base URL for webhooks and plugin callbacks |
| `COOKIE_SECURE` | No | `false` | Set `Secure` flag on auth cookies; use `true` behind TLS |

### Database and cache

| Variable | Required | Default | Description |
| --- | --- | --- | --- |
| `DATABASE_URL` | **Yes** | — | PostgreSQL connection string |
| `REDIS_URL` | **Yes** | — | Valkey/Redis URL |
| `CACHE_PROJECT_TTL` | No | `5m` | Project detail and member lists; `0` disables caching |
| `CACHE_CONFIG_TTL` | No | `10m` | Task types, statuses, custom fields, roles; `0` disables |
| `CACHE_SPRINT_TTL` | No | `2m` | Sprints and views; `0` disables |

### JWT and admin seed

| Variable | Required | Default | Description |
| --- | --- | --- | --- |
| `JWT_SECRET` | **Yes** | — | HMAC signing secret for access and refresh tokens |
| `JWT_ACCESS_TTL` | No | `15m` | Access token lifetime |
| `JWT_REFRESH_TTL` | No | `168h` | Refresh token lifetime (persistent / remember-me sessions) |
| `JWT_REFRESH_SESSION_TTL` | No | `24h` | Refresh session lifetime (ephemeral sessions) |
| `ADMIN_USERNAME` | **Yes** | — | Default admin username seeded on first startup |
| `ADMIN_PASSWORD` | **Yes** | — | Default admin password seeded on first startup |

### Object storage

| Variable | Required | Default | Description |
| --- | --- | --- | --- |
| `STORAGE_PROVIDER` | No | `minio` | `minio` or `s3` |
| `STORAGE_ENDPOINT` | No | `minio:9000` | MinIO host:port; ignored for AWS S3 |
| `STORAGE_PUBLIC_URL` | No | `""` | Public gateway path for presigned URLs, e.g. `http://localhost/storage` |
| `STORAGE_REGION` | No | `us-east-1` | AWS region (also passed to MinIO) |
| `STORAGE_BUCKET` | No | `paca` | Bucket name; created on startup if missing |
| `STORAGE_ACCESS_KEY_ID` | **Yes** | — | S3/MinIO access key |
| `STORAGE_SECRET_ACCESS_KEY` | **Yes** | — | S3/MinIO secret key |
| `STORAGE_USE_SSL` | No | `false` | `true` when endpoint is HTTPS |

For AWS S3, set `STORAGE_PROVIDER=s3`, supply real credentials, leave `STORAGE_ENDPOINT` empty, and run with `--scale minio=0`. For S3, `STORAGE_PUBLIC_URL` can be empty because presigned URLs are already public.

### Security and internal keys

| Variable | Required | Default | Description |
| --- | --- | --- | --- |
| `ENCRYPTION_KEY` | No | `""` | 64-char lowercase hex string (32 bytes, AES-256). Encrypts plugin tokens/secrets at rest. Legacy alias: `GITHUB_ENCRYPTION_KEY` |
| `AGENT_API_KEY` | No | `""` | Pre-shared `X-API-Key` for the built-in agent bot; must match `PACA_API_KEY` on ai-agent |
| `AI_AGENT_URL` | No | `http://ai-agent:8080` | Internal base URL for API → ai-agent HTTP calls (e.g. LLM model list) |

When `ENCRYPTION_KEY` is set but invalid, the API logs a warning and disables agent LLM key decryption.

### Plugin subsystem

| Variable | Required | Default | Description |
| --- | --- | --- | --- |
| `PLUGINS_STORE` | No | `local` | `local` (filesystem) or `s3` (object storage) |
| `PLUGINS_WASM_DIR` | No | `./plugins/local/backend` | WASM binaries root (`local` mode) |
| `PLUGINS_FRONTEND_DIR` | No | `./plugins/local/frontend` | Frontend bundles served at `/plugins/` |
| `PLUGINS_MCP_DIR` | No | `./plugins/local/mcp` | MCP bundles served at `/plugins-mcp/` |
| `PLUGINS_S3_PREFIX` | No | `plugins` | S3 key prefix when `PLUGINS_STORE=s3` |
| `PLUGINS_MARKETPLACE_CATALOG_URL` | No | `paca-plugins` raw JSON URL | Marketplace catalog endpoint |
| `PLUGINS_MARKETPLACE_TIMEOUT` | No | `20s` | HTTP timeout for marketplace fetches |

Production Compose hardcodes container paths (`/plugins`, `/plugins-frontend`, `/plugins-mcp`) and does not pass `PLUGINS_STORE` or cache TTL overrides — defaults from `config.Load()` apply.

## Realtime service (`services/realtime`)

| Variable | Required | Default | Description |
| --- | --- | --- | --- |
| `PORT` | No | `3001` | HTTP + Socket.IO listen port |
| `API_URL` | **Yes** | — | Internal API base URL for token verification (direct to API, not through nginx) |
| `REDIS_URL` | **Yes** | — | Valkey URL for pub/sub fan-out |
| `CORS_ORIGINS` | No | `http://localhost:3000` | Comma-separated allowed browser origins |
| `LOG_LEVEL` | No | `info` | `trace` \| `debug` \| `info` \| `warn` \| `error` \| `fatal` |
| `NODE_ENV` | No | `development` | `production` enables structured JSON logging (no pretty-print) |

## AI agent service (`services/ai-agent`)

Pydantic settings load from environment (optional `.env`). `INTERNAL_API_KEY` is required with minimum length 1.

| Variable | Required | Default | Description |
| --- | --- | --- | --- |
| `PORT` | No | `8080` | HTTP listen port inside the container |
| `LOG_LEVEL` | No | `INFO` | Python logging level |
| `VALKEY_URL` | No | `redis://valkey:6379/0` | Valkey for trigger streams and event publishing |
| `DATABASE_URL` | **Yes** | — | PostgreSQL (reads agent config and conversation state) |
| `INTERNAL_API_KEY` | **Yes** | — | Shared secret for `X-Internal-Token` on internal conversation routes |
| `API_BASE_URL` | No | `http://api:8080` | Internal API URL for service-to-service calls |
| `GATEWAY_BASE_URL` | No | `http://gateway` | Gateway URL for plugin MCP bundle resolution (`/plugins-mcp/`) |
| `PACA_API_KEY` | No | `""` | Must match `AGENT_API_KEY` on API; when set, injects built-in `paca` MCP server into agent conversations |
| `ENCRYPTION_KEY` | No | `""` | Must match API; decrypts `llm_api_key_secret` values from the database |
| `DOCKER_SOCKET` | No | `/var/run/docker.sock` | Host Docker socket for OpenHands agent-server containers |
| `AGENT_SERVER_IMAGE` | No | `ghcr.io/openhands/agent-server:latest-python` | Default agent-server image |
| `AGENT_SERVER_CONTAINER_PORT` | No | `8000` | Port the agent-server process listens on inside its container |
| `PORT_POOL_START` | No | `10000` | First host port in the pool (local dev outside Docker) |
| `PORT_POOL_SIZE` | No | `100` | Pool size (= max concurrent conversations per replica) |
| `WORKER_CONCURRENCY` | No | `10` | Concurrent Valkey trigger consumers |

When `PACA_API_KEY` is empty, the built-in `paca` MCP server is not injected. When `ENCRYPTION_KEY` is unset but the API encrypts secrets, the agent logs a warning and uses database values as-is.

## Web application and gateway

### Production web

The production `paca-web` image is a prebuilt SPA served by nginx inside the container. It has **no runtime environment variables** — API and realtime URLs are resolved relative to the gateway.

### Development web

| Variable | Required | Default | Description |
| --- | --- | --- | --- |
| `DOCKER` | No | — | Set to `true` in dev Compose so Vite proxies through the gateway |
| `VITE_ALLOWED_HOST` | No | — | Hostname allowed by Vite when accessing through a tunnel or custom domain |
| `NODE_OPTIONS` | No | — | Dev Compose sets `--max-old-space-size=2048` |

### Gateway (nginx)

The gateway container mounts `deploy/nginx/gateway.conf` and has no application env vars beyond the Compose port mapping (`GATEWAY_PORT`). It routes:

- `/api/*` → API (strips `/api` prefix)
- `/ws/*` → realtime (Socket.IO)
- `/storage/*` → MinIO (when bundled)
- `/plugins/`, `/plugins-mcp/` → plugin volume mounts
- `/*` → web SPA

`STORAGE_PUBLIC_URL` must match the gateway storage path so presigned URLs rewrite correctly for browsers.

## MCP client (`@paca-ai/paca-mcp`)

External MCP clients configure these variables. They are not part of the Docker stack unless you run MCP locally.

| Variable | Required | Default | Description |
| --- | --- | --- | --- |
| `PACA_API_KEY` | **Yes** | — | User API key or global `AGENT_API_KEY` (agent mode) |
| `PACA_API_URL` | No | `http://localhost:8080` | Paca API base URL |
| `PACA_GATEWAY_URL` | No | — | Gateway URL for loading plugin MCP bundles; falls back to `PACA_API_URL` |
| `PACA_AGENT_ID` | No | — | Agent UUID for agent single-project mode |
| `PACA_PROJECT_ID` | No | — | Project UUID; required when `PACA_AGENT_ID` is set |

<RequestExample>

```json
{
  "mcpServers": {
    "paca": {
      "command": "npx",
      "args": ["-y", "@paca-ai/paca-mcp"],
      "env": {
        "PACA_API_KEY": "your-api-key",
        "PACA_API_URL": "http://localhost:8080",
        "PACA_GATEWAY_URL": "http://localhost"
      }
    }
  }
}
```

</RequestExample>

## Production vs development defaults

| Concern | Production | Development |
| --- | --- | --- |
| Public URL var | `PUBLIC_URL` | `PUBLIC_HOST` |
| `COOKIE_SECURE` | `true` | `false` |
| `JWT_SECRET` / admin password | Strong random values required | Dev placeholders (`dev-change-in-production`, `adminpassword`) |
| `ENCRYPTION_KEY` | Generated by install script | Fixed dev hex key in `docker-compose.dev.yml` |
| Internal keys | `openssl rand -hex 32` each | `dev-agent-api-key-change-in-production`, `dev-internal-key-change-in-production` |
| Storage | MinIO sidecar or AWS S3 | MinIO on ports 9000/9001 |
| Plugin paths | Docker volumes at `/plugins*` | Bind mounts to `plugins/local/*` |
| AI agent | Optional (`--scale ai-agent=0` to skip) | Included with Docker socket mount |

## Verify configuration

<Steps>
<Step title="Check API health">

```bash
curl -s http://localhost/api/healthz
```

Expect HTTP 200 from the gateway-proxied health endpoint.

</Step>
<Step title="Confirm Valkey connectivity">

If the API or realtime fails to start, check that `REDIS_URL` / `VALKEY_URL` resolve to the same Valkey instance and that the hostname matches your compose network (`valkey` in Docker, `localhost` on the host).

</Step>
<Step title="Validate storage URLs">

Upload an attachment in the web UI. If presigned URLs fail, verify `STORAGE_PUBLIC_URL` matches your gateway storage route (e.g. `http://localhost/storage`).

</Step>
<Step title="Test agent key pairing">

With ai-agent enabled, confirm `AGENT_API_KEY` on the API equals `PACA_API_KEY` on ai-agent. A mismatch prevents the built-in MCP server from authenticating.

</Step>
<Step title="Confirm encryption key sync">

Set the same `ENCRYPTION_KEY` on API and ai-agent. Store an agent LLM key in the admin UI and start a conversation; decryption errors in ai-agent logs indicate a mismatch.

</Step>
</Steps>

## Related pages

<CardGroup cols={2}>
<Card title="Installation" href="/installation">
Prerequisites, install script, and required secrets for first deploy.
</Card>
<Card title="Deploy production" href="/deploy-production">
Production Compose topology, nginx gateway, and scaling optional services.
</Card>
<Card title="Local development" href="/local-development">
Dev Compose stack, hot-reload, and host-side run commands.
</Card>
<Card title="Configure AI agents" href="/configure-ai-agents">
Agent types, LLM config, and conversation lifecycle after keys are set.
</Card>
<Card title="Connect MCP server" href="/connect-mcp">
Wire `@paca-ai/paca-mcp` with `PACA_API_KEY` and agent mode env vars.
</Card>
<Card title="Troubleshooting" href="/troubleshooting">
Auth cookies, Valkey connectivity, storage misconfiguration, and MCP errors.
</Card>
</CardGroup>
