# Configuration

> Environment variables for compile, serve, auth, MCP, and assets.

*Every environment variable, in one place. A static project needs none of them.*

Look up an environment variable or a `graft.config.ts` export here. Graft
reads `.env` from the current folder or any folder above it.

## Required: none

A static project needs no environment at all. `graft init` scaffolds the static
index, `graft compile` writes `.graft/index.db`, and your app reads it embedded.
Everything below is for a capability you opt into.

## Postgres

Required once `graft.config.ts` sets `export const index = "postgres"`, which is
what operational data, typed functions, and copy-on-write branches need.

| Variable | Used by | Purpose |
| --- | --- | --- |
| `DATABASE_URL` | compile, dev, migrate, branch, merge, serve, app reads | Postgres connection |

## Identity and policy

| Variable | Purpose |
| --- | --- |
| `GRAFT_DEV_TOKEN` | Static bearer for local / bootstrap actors |
| `GRAFT_DEV_SCOPES` | Comma-separated scopes for the dev token |
| `GRAFT_TRUSTED_ISSUERS` | Comma-separated OIDC issuer URLs (discovery). `graft serve` parses these into `issuers`; an embedded resolver must be passed `issuers` itself |
| `GRAFT_TRUSTED_AUDIENCE` | Comma-separated token audiences `graft serve` accepts from every trusted issuer. Unset leaves the audience unchecked, with a startup warning |
| `GRAFT_TRUSTED_ACTOR_KIND` | `agent` (default) or `human`: what `graft serve` records trusted-issuer callers as |
| `GRAFT_MCP_ALLOW_ANONYMOUS` | `1` to serve anonymous MCP callers (local dev only; refused by default) |
| `approvalPolicy` (graft.config.ts, not an env var) | `human` gates every mutation, `unattended` gates nothing |

## Serve

| Variable | Purpose |
| --- | --- |
| `PORT` / `HOST` | Bind for `graft serve` (default loopback `:3903`) |
| `GRAFT_HARDEN` | Container: create + harden the runtime role at boot, then serve under it. Default `1` in all-in-one, `0` in `GRAFT_MODE=serve` |
| `GRAFT_RUNTIME_PASSWORD` | The hardened role's password; setting it also turns hardening on. Generated per boot when unset |
| `GRAFT_MODE` | Container compose: `serve` against external db/storage |
| `GRAFT_REVALIDATE_URL` | Your app's revalidate route. `graft serve`, `graft studio`, `graft mcp`, `graft merge` and `graft migrate` POST `{ branch, gitSha, changes }` to it after each write that changes content. https only, except on loopback (`localhost`, `127.0.0.1`, `[::1]`). See [Refresh pages when content changes](/docs/next#refresh-pages-when-content-changes) |
| `GRAFT_WEBHOOK_SECRET` | Sent as `Authorization: Bearer …` to `GRAFT_REVALIDATE_URL`. Required when the URL is set. The app's route checks the same value |

## Studio

`graft serve --studio` reads these. Set `GRAFT_GITHUB_REPO` to make saves land
on GitHub instead of the content directory. See
[Host Studio for your team](/docs/studio-hosting) for the full setup.

| Variable | Purpose |
| --- | --- |
| `GRAFT_STUDIO` | `1` mounts Studio at `/studio` on `graft serve`, like `--studio` |
| `GRAFT_STUDIO_PORT` | Port for `graft studio`. Default `4983` |
| `GRAFT_STUDIO_SECRET` | Turns on sign-in. At least 32 random characters. Signs session cookies and invite links, so changing it signs everyone out |
| `GRAFT_STUDIO_URL` | Public origin people reach Studio on. GitHub sign-in and `graft studio invite` need it |
| `GRAFT_STUDIO_EDITORS` | Who may sign in with GitHub, Google or single sign-on without a Studio account or an invitation, as comma-separated logins or emails, each with an optional `=role` (default `editor`). Unset lets anyone with write access to the repository sign in with GitHub. Invite links work regardless |
| `GRAFT_STUDIO_AUTO_JOIN` | Email domains whose people join on their own when they sign in with GitHub, Google or single sign-on, as comma-separated domains, each with an optional `=role` (default `contributor`; never `admin`). Free mail providers are refused. See [Host Studio for your team](/docs/studio-hosting#2-let-people-sign-in) |
| `GRAFT_GITHUB_CLIENT_ID` / `GRAFT_GITHUB_CLIENT_SECRET` | A GitHub OAuth app, for "Sign in with GitHub". Set both or neither |
| `GRAFT_GOOGLE_CLIENT_ID` / `GRAFT_GOOGLE_CLIENT_SECRET` | A Google OAuth client of type Web application, for "Continue with Google". Set both or neither |
| `GRAFT_OIDC_ISSUER` / `GRAFT_OIDC_CLIENT_ID` / `GRAFT_OIDC_CLIENT_SECRET` | Another OpenID Connect provider, for single sign-on: Okta, Entra ID, your app's Better Auth and others. Set all three or none. See [Host Studio for your team](/docs/studio-hosting#2-let-people-sign-in) |
| `GRAFT_OIDC_NAME` | The provider's name on the single sign-on button. Optional |
| `GRAFT_GITHUB_REPO` | `owner/name`. Studio and MCP writes go to draft branches in this repository |
| `GRAFT_GITHUB_TOKEN` | A fine-grained token with Contents and Pull requests read and write |
| `GRAFT_GITHUB_APP_ID` / `GRAFT_GITHUB_APP_PRIVATE_KEY` | A GitHub App instead of a token. The key is PEM, literal `\n` accepted |
| `GRAFT_GITHUB_APP_INSTALLATION_ID` | Optional. Found from the repository when unset |
| `GRAFT_GITHUB_BRANCH` | The production branch publishing writes to. Default `main` |
| `GRAFT_GITHUB_CONTENT_PATH` | The content directory's path in the repository. Default: its path relative to the project, or `content` when the directory is outside the project |
| `GRAFT_GITHUB_API_URL` | GitHub Enterprise API base. Default `https://api.github.com` |
| `GRAFT_STUDIO_PUBLISH` | `commit` (default) or `pull-request`. Use `pull-request` when the production branch is protected |
| `GRAFT_DEPLOYED_SHA` | The commit this deployment was built from. Vercel, Render and Railway set their own, which Studio reads |

## Assets (S3-compatible)

| Variable | Purpose |
| --- | --- |
| `S3_BUCKET` | Bucket name |
| `S3_ENDPOINT` | API endpoint (R2, MinIO, Tigris, …) |
| `S3_ACCESS_KEY` / `S3_SECRET_KEY` | Credentials |
| `S3_REGION` | Optional. Defaults to `auto`, which suits R2 |
| `S3_PUBLIC_URL` | Optional stable public base; else presigned GET |
| `GRAFT_ENSURE_BUCKET` | Container: set `0` when the bucket already exists |

## Neon branching

| Variable | Purpose |
| --- | --- |
| `NEON_API_KEY` | Create / drop physical CoW branches |
| `GRAFT_NEON_PROJECT_ID` | Neon project id (required; keys may be project-scoped) |

## graft.config.ts exports

The config is TypeScript you own, loaded at runtime. Only `collections` is
required.

| Export | Default | Purpose |
| --- | --- | --- |
| `collections` | required | Record of `defineCollection` results |
| `functions` | `{}` | Record of `defineFunction` results; enables `list_functions` and `run_function` |
| `contentDir` | `"content"` | Content root, relative to the config file |
| `migrationsDir` | `"migrations"` | Migrations directory, relative to the config file |
| `index` | `"postgres"` when the export is missing. `graft init` writes `"static"`. | `"static"` for the storage engine that needs no services |
| `mdxTrust` | `"restricted"` | How much of MDX an authored body may be |
| `content` | `{ source: "git" }` | Where your content is kept, which publishes need review, and whether to copy it into git |

### content

Choose where your content is the real thing, the version everything else is
built from.

- **`"git"`** keeps it in the MDX files under `contentDir`. Studio saves files
  on your computer, or commits through GitHub when hosted. `graft compile`
  builds the index from the files.
- **`"database"`** keeps it in Postgres. Studio and agents save drafts there,
  and publishing puts a change on the live site straight away, with no
  rebuild. `graft compile` builds the index from what is published, not from
  the files. A hosted Studio needs no GitHub setup at all.

`graft.config.ts`:

```ts
export const content = { source: "database" };
```

The database source needs the Postgres engine, so it can't be combined with
`index = "static"`. To move an existing project, set the export and run
`graft content import`, which loads your files into the database. It brings
in documents that have nothing published yet, and never overwrites one that
does, so running it again is safe. Deleting a file does not delete its
document: delete it in Studio instead. [Content in the database](/docs/content-in-the-database)
covers pulling, pushing and keeping a copy in git.

| Setting | Default | What it does |
| --- | --- | --- |
| `source` | `"git"` | `"git"` or `"database"` |
| `review` | `{ deletes: true, overLines: 200 }` | For git-sourced projects hosted on GitHub: publishes that open a pull request even for an editor. `deletes` covers deletions and renames, `collections` lists collections whose every change is reviewed, and `overLines` catches large changes. `false` publishes everything directly |
| `mirror` | none | For database-sourced projects: `{ mode: "batch" \| "each", via?: "commit" \| "pull-request", everyMinutes?: number }` copies what is published into the GitHub repository in `GRAFT_GITHUB_REPO` |

`graft.config.ts`:

```ts
export const content = {
  source: "git",
  review: { collections: ["legal"], overLines: 100 },
};
```

In a database-sourced project these refuse with `CONTENT_SOURCE_MISMATCH`,
because they work on files: reverting to a commit in Studio, `graft merge`,
and content migrations.

### mdxTrust

`"restricted"` refuses `{…}` expressions, `import`, `export` and spread
attributes in authored bodies. `graft compile` checks every document and reports
all offenders at once, so the refusal happens at build time rather than
per-request.

Set `"full"` only when every author of the repository has commit access. Rendering
evaluates MDX as JavaScript on the server, so on a Studio hosted for writers,
"can author a page" would otherwise mean "can execute code on the render host".

`graft.config.ts`:

```ts
export const mdxTrust = "full";
```

`MdxBody` has its own `trust` prop with the same default, and the two have to
agree: set `mdxTrust` here and pass `trust="full"` at the render site. Compile
says so in the error when it refuses a document.

## Operator vs runtime

Keep two URLs in production:

- **Operator.** Compiles, migrates, merges, and approves from CI or your machine.
- **Runtime.** Serves the deployment on `DATABASE_URL` after `graft harden`.

On Vercel embedded deploys, run compile in the build step with the operator URL,
then serve the app with the runtime URL. See [Deploy](/docs/deploy).
