Configuration
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 |
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 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 |
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 |
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 undercontentDir. Studio saves files on your computer, or commits through GitHub when hosted.graft compilebuilds 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 compilebuilds the index from what is published, not from the files. A hosted Studio needs no GitHub setup at all.
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
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 |
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".
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_URLaftergraft 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.