# Configuration

> Every GRAFT_* variable the self-hosted server reads, its default, and when Graft refuses to start without it.

Source: https://docs.getgraft.ai/graft/self-hosting/configuration

Graft reads its configuration from the environment, validates all of it at boot, and refuses to start on the first problem with a sentence naming the variable. Nothing is read lazily: a wrong value fails in the boot log, not on the first vendor call. Every variable is `GRAFT_*`. The source of truth is `packages/env/src/schema.ts` in the repository, with `.env.example` beside the compose file as the annotated starting point.

Under Docker Compose, everything in `.env` reaches the `graft` container, so any variable below can be set there. A few are fixed by the compose file itself and win over the same name in `.env`.

## Two rules to know

**Some settings only make sense complete.** A group that is half set is refused at boot, because a partial group is always a typo or a half-finished deploy and would otherwise present as a feature that silently never works. The groups are the capability token key pair, the Docker sandbox pair, the admin pair, the Langfuse pair, the model provider settings, and the four gateway provider settings.

**A production self-host must have a model.** With `NODE_ENV=production` and the default `GRAFT_BACKINGS=open`, which is what the published image runs as, Graft refuses to start unless `GRAFT_MODEL_BACKEND=provider`, `GRAFT_MODEL_PROVIDER` and `GRAFT_MODEL_API_KEY` are set. `acquire` is the product, and a server that boots with no model would answer every `acquire` with `acquire_unconfigured` while looking installed. The refusal reads:

```text
The self-hosted form needs a model to author with (ADR 0014): set GRAFT_MODEL_BACKEND=provider with GRAFT_MODEL_PROVIDER (anthropic or openai) and GRAFT_MODEL_API_KEY.
```

Secret-shaped values are also refused while they still hold a secret store's `PLACEHOLDER` string.

## Read by the compose file

These shape the containers and are not read by the server itself.

| Variable | Default | What it does |
| --- | --- | --- |
| `GRAFT_PUBLIC_URL` | `http://localhost:3000` | The origin the browser sees. The compose file passes it to the server as both `GRAFT_AUTH_URL` and `GRAFT_CONSOLE_URL`. Behind a domain, the https origin your reverse proxy serves. |
| `GRAFT_PORT` | `3000` | The host port the console, API, proxy and MCP endpoint are published on. |
| `GRAFT_POSTGRES_PORT` | `5432` | The host port Postgres is published on, for a server run from source. The `graft` container never uses it. |
| `GRAFT_DOCKER_GID` | `0` | The Docker socket's group inside the container. `0` under Docker Desktop; on Linux, the output of `stat -c %g /var/run/docker.sock`. |
| `GRAFT_IMAGE_TAG` | `latest` | Which published tag of `ghcr.io/getmodern-ai/graft` and `ghcr.io/getmodern-ai/graft-sandbox` to run. |
| `POSTGRES_PASSWORD` | `password` | The Postgres password, used in the connection string the compose file builds. |

## Set by the compose file

The compose file fixes these for the `graft` service. They are listed so you know what the server runs with; change the `.env` value they derive from rather than these.

| Variable | Value under compose |
| --- | --- |
| `GRAFT_DATABASE_URL` | `postgresql://postgres:<POSTGRES_PASSWORD>@postgres:5432/graft` |
| `GRAFT_AUTH_URL` | `GRAFT_PUBLIC_URL` |
| `GRAFT_CONSOLE_URL` | `GRAFT_PUBLIC_URL` |
| `GRAFT_PROXY_PUBLIC_URL` | `http://proxy:3000/api/proxy`, resolved by sandboxes on the internal network and never by the host |
| `GRAFT_SANDBOX_IMAGE` | `ghcr.io/getmodern-ai/graft-sandbox:<GRAFT_IMAGE_TAG>` |
| `GRAFT_SANDBOX_NETWORK` | `<project>_sandbox` |
| `GRAFT_TOOLBOX_VOLUME` | `<project>_toolboxes` |
| `GRAFT_ADMIN_EMAIL`, `GRAFT_ADMIN_PASSWORD` | From `.env`, with development defaults you must change |

The image itself sets `NODE_ENV=production`, `PORT=3000`, `GRAFT_CONSOLE_DIR=/app/apps/web/dist` and `GRAFT_TOOLBOX_ROOT=/var/lib/graft/toolboxes`.

## The server's variables

### Required

Graft does not boot without these.

| Variable | Rule |
| --- | --- |
| `GRAFT_DATABASE_URL` | A `postgres://` or `postgresql://` connection string. |
| `GRAFT_AUTH_SECRET` | At least 32 characters. Signs every session cookie and token. |
| `GRAFT_AUTH_URL` | An absolute http(s) URL: the server's public origin. |
| `GRAFT_CONSOLE_URL` | An absolute http(s) URL: where the console answers, and the base of every handoff URL. |
| `GRAFT_HANDOFF_SECRET` | At least 32 characters. Signs handoff URLs; rotating it only invalidates links not yet opened. |
| `GRAFT_KEYRING_SECRET` | At least 32 characters. Required with `GRAFT_BACKINGS=open`, the default: the local keyring that encrypts credentials derives its key from it. |

`docker compose run --rm --no-deps graft node dist/keys.mjs` mints the three secrets above plus the capability token key pair below.

### The proxy and the capability token

| Variable | Default | Rule |
| --- | --- | --- |
| `GRAFT_CAPABILITY_TOKEN_PRIVATE_KEY` | unset | A PKCS#8 Ed25519 PEM; a literal `\n` in place of line breaks is accepted. Pair with the public key. |
| `GRAFT_CAPABILITY_TOKEN_PUBLIC_KEY` | unset | The matching SPKI PEM, also published at `/api/proxy/.well-known/jwks.json`. Pair with the private key. |
| `GRAFT_PROXY_PUBLIC_URL` | `http://localhost:<PORT>/api/proxy` | What a sandbox is handed as the base of every vendor call. |
| `GRAFT_PROXY_FOLLOW_REDIRECTS` | `false` | The break glass: follow a vendor redirect that stays inside the connection's host set, at most three hops. Leave it off unless a vendor insists. |

Without the key pair the server boots, nothing mints a token, and the proxy answers `503 proxy_unconfigured`. A half pair is refused.

### Backings, sandbox and toolbox

| Variable | Default | Rule |
| --- | --- | --- |
| `GRAFT_BACKINGS` | `open` | `open` for the backings in the repository (Docker sandbox, local keyring, filesystem toolbox), or `cloud` for the hosted form's private package. |
| `GRAFT_SANDBOX_BACKEND` | `docker` | `docker`, or `fake` for an in-process directory used in tests. `fake` is refused in production. |
| `GRAFT_SANDBOX_IMAGE` | unset | The prebuilt image sandboxes are created from. Pair with the network. |
| `GRAFT_SANDBOX_NETWORK` | unset | The internal Docker network sandboxes join. Pair with the image. Without the pair, the server boots with no sandbox and every run refuses, saying so. |
| `GRAFT_TOOLBOX_ROOT` | `./.graft/toolboxes` | Where toolboxes live as files, one directory per person. The image sets `/var/lib/graft/toolboxes`. |
| `GRAFT_TOOLBOX_VOLUME` | unset | The named Docker volume `GRAFT_TOOLBOX_ROOT` is mounted from, when the server itself is a container. A volume name, not a path. |
| `GRAFT_CONSOLE_DIR` | `../web/dist` | Where the console's build is served from. A directory with no build is not a boot failure; the API serves and console paths answer a JSON 404. |
| `GRAFT_CORS_ORIGIN` | unset | Comma-separated origins, for a console served from another origin. Under compose the console is same-origin and this stays unset. |
| `GRAFT_DEV_SEED` | unset | A development seed file of connections. Refused in production. |

The Docker backing also honours `DOCKER_HOST` as the Docker CLI does, `unix:///path` or `tcp://host:port`, for the sibling-daemon arrangement.

### The package policy

| Variable | Default | Rule |
| --- | --- | --- |
| `GRAFT_PACKAGE_MIN_AGE_DAYS` | `90` | A package off the allowlist must be at least this old, counted from its first publish. A whole number; zero is allowed. |
| `GRAFT_PACKAGE_MIN_WEEKLY_DOWNLOADS` | `1000` | A package off the allowlist must have had at least this many downloads last week. |
| `GRAFT_PACKAGE_ALLOWLIST` | unset | Extra names admitted without the registry checks, comma-separated: exact names or `@scope/*`. A bare `*` is refused. |

See [Packages and SDKs](https://docs.getgraft.ai/graft/toolbox/packages-and-sdks.md) for the policy these feed.

### Approvals, the sweep and acquire

| Variable | Default | Rule |
| --- | --- | --- |
| `GRAFT_APPROVAL_WAIT_SECONDS` | `25` | How long a tool call waits for a person to answer a handoff before returning `awaiting_approval`. 0 to 300. |
| `GRAFT_PENDING_ACTION_TTL_HOURS` | `24` | How long a pending action stays answerable in the console. 1 to 168. |
| `GRAFT_SWEEP_INTERVAL_SECONDS` | `300` | How often the working-set sweep applies each agent's cap and idle window. At least 1. |
| `GRAFT_ACQUIRE_MAX_ATTEMPTS` | `4` | How many drafts one `acquire` job may make before giving up. 1 to 20. |
| `GRAFT_ACQUIRE_TOKEN_CEILING` | `400000` | The most tokens one `acquire` job may spend across every model turn. At least 1000. |
| `GRAFT_ACQUIRE_CONCURRENCY` | `2` | How many `acquire` jobs run at once. 1 to 32. |

### The model

| Variable | Default | Rule |
| --- | --- | --- |
| `GRAFT_MODEL_BACKEND` | unset | `provider` for a real provider. `scripted` is a canned answer sheet for development and is refused in production. Unset, `acquire` refuses `acquire_unconfigured`. A production self-host requires `provider`. |
| `GRAFT_MODEL_PROVIDER` | unset | `anthropic` or `openai`. Read only under `provider`, and required there. |
| `GRAFT_MODEL_API_KEY` | unset | The provider's key. Read only under `provider`, and required there. |
| `GRAFT_MODEL_AUTHORING` | per provider | The model that writes the tool. Anthropic `claude-fable-5-1`, OpenAI `gpt-5.6-sol`. |
| `GRAFT_MODEL_TRIAGE` | per provider | The cheaper model that reads documentation first and never writes code. Anthropic `claude-haiku-4-5-20251001`, OpenAI `gpt-5.4-mini`. |
| `GRAFT_MODEL_BASE_URL` | unset | An absolute http(s) URL pointing the `openai` provider at an OpenAI-compatible gateway. |
| `GRAFT_MODEL_SCRIPT` | unset | The scripted backend's JSON file. Pair with `GRAFT_MODEL_BACKEND=scripted`. |

Any of the five provider settings set while `GRAFT_MODEL_BACKEND` is not `provider` is refused, since nothing would read it. A wrong model id is not caught at boot; it fails the first job with the provider's own message.

### Langfuse

| Variable | Default | Rule |
| --- | --- | --- |
| `GRAFT_LANGFUSE_PUBLIC_KEY` | unset | Pair with the secret key. With the pair, every model call `acquire` makes is traced under the job, the person and the attempt. |
| `GRAFT_LANGFUSE_SECRET_KEY` | unset | Pair with the public key. |
| `GRAFT_LANGFUSE_BASE_URL` | the SDK's, the EU cloud | An absolute http(s) URL naming the region. |
| `GRAFT_PIPEDREAM_PROJECT_ID` | unset | A Pipedream Connect project id, `proj_…`. With the three below, the Pipedream provider is on: a vendor Pipedream covers (Gmail) connects with one click and every call for it relays through Pipedream's proxy, which holds the token; Graft stores only the account id. The four are all-or-nothing. |
| `GRAFT_PIPEDREAM_ENVIRONMENT` | unset | `development` or `production`: which of the project's two account stores the server addresses. |
| `GRAFT_PIPEDREAM_CLIENT_ID` | unset | The project's OAuth client id, from Pipedream's console. |
| `GRAFT_PIPEDREAM_CLIENT_SECRET` | unset | The project's OAuth client secret. A value still holding a secret store's `PLACEHOLDER` is refused at boot. |

### Your API gateway

| Variable | Default | Rule |
| --- | --- | --- |
| `GRAFT_GATEWAY_HOSTS` | unset | The vendor hosts your API gateway covers, comma-separated: exact hostnames or `*.suffix` patterns. One of four, all or nothing. |
| `GRAFT_GATEWAY_UPSTREAM_URL` | unset | Where the gateway answers: an absolute URL with an optional path, no query. `https` in production. One of four. |
| `GRAFT_GATEWAY_HEADER_NAME` | unset | The header the gateway authenticates your deployment with. One of four. |
| `GRAFT_GATEWAY_HEADER_VALUE` | unset | Its value, a secret. One of four. |
| `GRAFT_GATEWAY_HEADER_PREFIX` | unset | A prefix for the caller's headers, for a gateway that forwards only prefixed ones. Refused without the four. |

With the four set, a vendor whose hosts they cover connects with no person step and every call for it relays through the gateway. See [Route vendors through your API gateway](https://docs.getgraft.ai/graft/self-hosting/route-vendors-through-your-api-gateway.md).

### First start

| Variable | Default | Rule |
| --- | --- | --- |
| `GRAFT_ADMIN_EMAIL` | unset | The account opened on first start, into an empty database only. Pair with the password. |
| `GRAFT_ADMIN_PASSWORD` | unset | At least 8 characters. Pair with the email. |
| `GRAFT_MIGRATE_ON_START` | `true` | Apply the committed migrations before listening. `false` only for a development database moved with `db:push`. |
| `NODE_ENV` | `development` | `development`, `production` or `test`. The image sets `production`. |
| `PORT` | `3000` | What the server listens on inside the container. |

The admin pair is read on every start and acts once: a database that already holds a person is never touched, so changing `GRAFT_ADMIN_PASSWORD` later resets nothing, and the boot log says `admin bootstrap skipped`.
