# Blop Cloud environment variables

> Look up the environment variables a self-hosted Blop Cloud server reads, with defaults and what each one controls.

Server variables for a self-hosted [Blop Cloud](/cloud/self-host/). Blop Cloud reads them from the process environment; the commented template is [`.env.example`](https://github.com/blop-oss/blop-app/blob/main/.env.example) in the repository root. Never commit real values. CLI and collector variables are in [Environment variables](/reference/environment/).

## Required variables

| Variable | Purpose |
|---|---|
| `POSTGRES_URL` | Postgres connection string. Add `?sslmode=require` for managed Postgres; use the pooler URL behind PgBouncer. The Compose file sets it. |
| `AUTH_SECRET` | Signs the session cookie. Changing it signs everyone out. |
| `BASE_URL` | Exact public origin, with no path, query or fragment. An `https` value marks the session cookie `Secure`. Check runners must reach `${BASE_URL}/api/ingest`. |

## Server and proxy variables

| Variable | Default | Purpose |
|---|---|---|
| `PORT` | `3000` | Listen port |
| `HOST` | `0.0.0.0` | Listen address |
| `ORIGIN` | Derived from the request | Public origin trusted for same-origin checks; set it from `BASE_URL` behind a TLS proxy |
| `PROTOCOL_HEADER`, `HOST_HEADER` | Unset | Read protocol and host from proxy headers such as `x-forwarded-proto` when `ORIGIN` is unset |
| `ADDRESS_HEADER` | Unset: socket address | Header that carries the client IP, used for rate limits and audit records |
| `XFF_DEPTH` | `1` | With `ADDRESS_HEADER=x-forwarded-for`, the number of trusted proxies; the client IP is that many entries from the end |
| `BODY_SIZE_LIMIT` | `512K` | Request body cap; accepts `K`, `M` and `G` |
| `POSTGRES_MAX_CONNECTIONS` | `5` | Connection pool size |
| `PROJECT_SECRETS_KEY` | `AUTH_SECRET` | Encrypts saved project credentials. Rotating it makes them unreadable. |
| `BLOP_PLUGINS_SAFE_MODE` | Unset | `1` starts only sign-in, permissions, health and machine endpoints (ingest, workers, webhooks) for recovery; no pages or user APIs |
| `SENTRY_DSN`, `PUBLIC_SENTRY_DSN` | Unset | Server and browser error reporting; the browser DSN is public |
| `AUTH_CODE_LIMIT_PER_EMAIL_IP` | `5` | Sign-in codes per email and IP in 15 minutes |
| `AUTH_CODE_LIMIT_PER_EMAIL` | `30` | Sign-in codes per email in 15 minutes |
| `AUTH_CODE_LIMIT_PER_IP` | `20` | Sign-in codes per IP in 15 minutes |
| `AUTH_CODE_LIMIT_PER_EMAIL_DAY` | `50` | Sign-in codes per email per day |

## Organization memory and OAuth

| Variable | Purpose |
|---|---|
| `BLOP_MEMORY_DIR` | Required for organization memory, no default or fallback: an absolute, writable, persistent directory holding one `<workspace-id>.db` per workspace. Unset returns `memory_unconfigured`. One app instance writes it. |
| `BLOP_MEMORY_ASSESSOR_URL` | Optional operator-selected assessment endpoint. HTTPS or loopback HTTP; no credentials/query/fragment in URL. Configuration opts into processing selected authorized hosted memory excerpts and query data, not laptop files or raw telemetry. No redirects, retries or provider fallback. |
| `BLOP_MEMORY_ASSESSOR_ID`, `BLOP_MEMORY_ASSESSOR_VERSION`, `BLOP_MEMORY_ASSESSOR_POLICY_VERSION` | Required nonempty version metadata when assessment is configured. Incomplete configuration fails closed, not `not_configured`. |
| `BLOP_MEMORY_ASSESSOR_TOKEN` | Optional server-only bearer credential for that endpoint; never included in receipts or browser bundles. |
| `XDG_CACHE_HOME` | Cache root for the pinned CPU embedding model (`<root>/blop/models`, else `~/.cache/blop/models`). The model downloads only on explicit indexing. |
| `BLOP_OAUTH_SIGNING_KEYS` | Private RSA JWK set (`{"keys":[…]}`) for RS256, each key with its own stable `kid`. Never expose it to clients. |
| `BLOP_OAUTH_COOKIE_SECRET` | At least 32 random characters, distinct from every other secret. Rotating it can make stored provider tokens unreadable. |

Agent connections work only when `BASE_URL` and both OAuth variables are set. The OAuth issuer is `${BASE_URL}/oauth` and the MCP resource is `${BASE_URL}/api/mcp`.

### Assessment endpoint contract

No model is installed or selected implicitly. With no configuration, reads
report `assessment.status: not_configured`. A configured endpoint is an internal
assessor, not another agent tool. It must accept a bounded JSON POST:

```json
{
  "operation": "search",
  "query": [{"text": "shipping"}],
  "scopeHash": "<trusted-scope-hash>",
  "graphRevision": 4,
  "policyRevision": 1,
  "coverageScope": "selected-memory",
  "nodes": [{
    "nodeId": "note:shipping",
    "revision": 2,
    "hash": "<sha256-of-supplied-source-body>",
    "title": "Shipping",
    "text": "Obtain owner review.",
    "complete": true
  }],
  "missingInputs": []
}
```

Return this strict response, with actual findings when present:

```json
{
  "findings": [{
    "id": "review",
    "severity": "warning",
    "summary": "Check owner review before shipping.",
    "nodeIds": ["note:shipping"]
  }],
  "coverage": {"status": "complete", "missingInputs": []}
}
```

Severity is `info|warning|high`; coverage is `complete|partial`. At most four
findings, four addressed node IDs/finding, 512 characters/summary and twenty
256-character missing-input descriptions. Unselected citation IDs and extra
fields, including approval claims, fail validation. Text and judgments remain
untrusted data; neither grants authorization.

Input covers at most sixteen selected nodes and 4,000 Unicode characters/body,
within a 60,000-byte node budget and 100,000-byte request limit. Missing excerpts,
task context, bounded pages and omitted brief/evidence text produce explicit
partial coverage codes. Body hashes bind supplied source text plus exact node
revisions; search text may already be a bounded excerpt. No evidence body is
fetched by assessment. `coverage.scope` is `selected-memory`; `workingTree` is
`not_supplied`, never an assessment of an unseen diff.

Default deadline is ten seconds; responses cap at 32,000 bytes. Receipts retain
query/input hashes, selected source revisions/hashes, graph/policy versions,
operator-supplied assessor/policy versions and assessment time. No assessment
cache is used. Graph/grant changes during processing invalidate the result.
Configured failure returns `assessment_unavailable` or `assessment_failed` with
no unassessed result. Existing envelopes, identifiers and null/not-found semantics
remain intact. Oversized final results require a smaller page/section, not
identifier truncation.

The public graph API accepts `MemoryDatabase.view(access, { assessor })` and exports
`assessMemoryRead`, `MemoryAssessor` and `createHttpMemoryAssessor` for trusted
in-process or explicitly selected local adapters. Assessed views log served reads
only after assessment succeeds. Standalone local CLI reads remain accountless and
unconfigured by default. Contract/loopback tests do not qualify model quality.

## Artifact storage variables

Any S3-compatible bucket, such as MinIO.

| Variable | Purpose |
|---|---|
| `CLOUDFLARE_R2_ENDPOINT` | S3 endpoint URL, for example `http://127.0.0.1:9000` |
| `CLOUDFLARE_R2_BUCKET` | Bucket name |
| `CLOUDFLARE_R2_ACCOUNT_ID` | Account ID; any value for MinIO |
| `CLOUDFLARE_R2_ACCESS_KEY_ID`, `CLOUDFLARE_R2_SECRET_ACCESS_KEY` | Bucket credentials |
| `ARTIFACT_RETENTION_DAYS` | Days before the cleanup worker deletes an artifact; default `30` |

## Scheduled worker variables

Every worker accepts `Authorization: Bearer $CRON_SECRET` on GET or POST. A per-worker secret is an alternative for that one worker, sent in its own header, never as the bearer token.

| Variable | Header |
|---|---|
| `CRON_SECRET` | `Authorization: Bearer` on every worker |
| `HEALER_WORKER_SECRET` | `x-healer-worker-secret` |
| `SLACK_DIGEST_WORKER_SECRET` | `x-slack-digest-worker-secret` |
| `LINEAR_SYNC_WORKER_SECRET` | `x-linear-sync-worker-secret` |
| `JIRA_SYNC_WORKER_SECRET` | `x-jira-sync-worker-secret` |

The worker schedule is in [Self-host](/cloud/self-host/#schedule-the-workers). Also call `/api/platform/health-tick` every few minutes when workspaces use platform connections; it needs `CRON_SECRET`.

## AI provider variables

| Variable | Purpose |
|---|---|
| `OPENROUTER_API_KEY`, `CHAT_AGENT_PROVIDER`, `CHAT_AGENT_MODEL`, `CHAT_AGENT_API_KEY`, `CHAT_MAX_CONCURRENT_RUNS` | In-app chat. Without a key, chat shows a setup message. |
| `CHAT_AGENT_MODELS` | Models people can pick in chat: a comma list of `slug` or `slug=Label`. The default model is always first, and any other model falls back to it. Unset offers a built-in list of free models. |
| `BLOP_AGENT_PROVIDER`, `BLOP_AGENT_MODEL`, `BLOP_AGENT_API_KEY` | Browser checks started from Blop Cloud. Each falls back to its `CHAT_AGENT_*` value, then to `openrouter`, `anthropic/claude-sonnet-4`, and `OPENROUTER_API_KEY` then `OLLAMA_API_KEY`. |
| `OLLAMA_API_KEY` | Ollama Cloud as an OpenAI-compatible provider |
| `HEALER_PROVIDER`, `OPENAI_API_KEY`, `OPENAI_HEALER_MODEL`, `KHADIM_API_KEY`, `KHADIM_HEALER_PROVIDER`, `KHADIM_HEALER_MODEL`, `OPENROUTER_HEALER_API_KEY`, `OPENROUTER_HEALER_MODEL` | Fix suggestions; configured providers are tried in order, else a template is used |

These fallbacks apply only to Blop Cloud, never to the local `blop` CLI.

## Browser variables

| Variable | Default | Purpose |
|---|---|---|
| `BLOP_BROWSER` | `camoufox` | Browser for checks started from Blop Cloud: `chromium`, `camoufox`, `firefox` or `webkit` |
| `BLOP_CONTAINERIZED` | `1` | Runs that browser in a container through the Docker socket, so Docker is required; `0`, `false` or `no` turns it off |
| `BLOP_CAMOUFOX_NETWORK` | `BLOP_PLAYWRIGHT_NETWORK`, else none | Docker network for the Camoufox container on the Nixpacks path |

## Integration variables

| Integration | Variables |
|---|---|
| GitHub | `GITHUB_APP_ID`, `GITHUB_APP_SLUG`, `GITHUB_APP_CLIENT_ID`, `GITHUB_APP_CLIENT_SECRET`, `GITHUB_APP_PRIVATE_KEY`, `GITHUB_APP_WEBHOOK_SECRET`, `GITHUB_APP_REDIRECT_URI`, `GITHUB_OAUTH_CLIENT_ID`, `GITHUB_OAUTH_CLIENT_SECRET`; `BLOP_GITHUB_API_URL` (default `api.github.com`) sets a GitHub Enterprise API base for REST calls |
| Slack | `SLACK_CLIENT_ID`, `SLACK_CLIENT_SECRET`, `SLACK_SIGNING_SECRET`, `SLACK_REDIRECT_URI` |
| Email | `RESEND_API_KEY` (blank: emails go to the server log), `EMAIL_FROM` |
| Billing | `STRIPE_SECRET_KEY`, `STRIPE_WEBHOOK_SECRET`. Billing is off without the key; `BLOP_BILLING=off` forces it off. Purchases stay closed until `BLOP_BILLING_LIVE=1`. |
| Egress | `INTEGRATION_EGRESS_ALLOWLIST` restricts public hosts; `INTEGRATION_EGRESS_ALLOW_PRIVATE` admits exact private hostnames, over HTTP too |

Integration egress refuses redirects and private, loopback and metadata addresses by default, with bounded time and size. Destination credentials are saved per project in the app, not in the environment. Register these callback URLs with the providers:

- GitHub App callback: `${BASE_URL}/api/auth/github/callback`
- GitHub App webhook: `${BASE_URL}/api/integrations/github/webhooks`
- Slack OAuth: `${BASE_URL}/api/integrations/slack/oauth-callback`

## Platform connection variables

Workspaces can register an external platform service URL. Every such URL must be HTTPS and public, with no redirects and bounded time and size.

| Variable | Purpose |
|---|---|
| `PLATFORM_URL`, `PLATFORM_SERVICE_TOKEN` | Default connection and its bearer token; URL default `http://localhost:3002` |
| `PLATFORM_EGRESS_ALLOWLIST` | Only these hosts or `.domains` are allowed |
| `PLATFORM_EGRESS_ALLOW_PRIVATE` | Hosts allowed to be private or plain HTTP, for local development |

## Same-machine collector variables

The Explore pages can read a `blop` collector on the server's own machine; a deployed server cannot reach a developer's localhost.

| Variable | Default | Purpose |
|---|---|---|
| `BLOP_COLLECTOR_URL` | `http://127.0.0.1:27811` | Collector base URL; must be loopback |
| `BLOP_COLLECTOR_WORKSPACES` | Unset: no workspace | Comma-separated workspace IDs allowed to read it |
