Configuration

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

Reference7 min read

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.

VariableUsed byPurpose
DATABASE_URLcompile, dev, migrate, branch, merge, serve, app readsPostgres connection

Identity and policy

VariablePurpose
GRAFT_DEV_TOKENStatic bearer for local / bootstrap actors
GRAFT_DEV_SCOPESComma-separated scopes for the dev token
GRAFT_TRUSTED_ISSUERSComma-separated OIDC issuer URLs (discovery). graft serve parses these into issuers; an embedded resolver must be passed issuers itself
GRAFT_TRUSTED_AUDIENCEComma-separated token audiences graft serve accepts from every trusted issuer. Unset leaves the audience unchecked, with a startup warning
GRAFT_TRUSTED_ACTOR_KINDagent (default) or human: what graft serve records trusted-issuer callers as
GRAFT_MCP_ALLOW_ANONYMOUS1 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

VariablePurpose
PORT / HOSTBind for graft serve (default loopback :3903)
GRAFT_HARDENContainer: create + harden the runtime role at boot, then serve under it. Default 1 in all-in-one, 0 in GRAFT_MODE=serve
GRAFT_RUNTIME_PASSWORDThe hardened role's password; setting it also turns hardening on. Generated per boot when unset
GRAFT_MODEContainer compose: serve against external db/storage
GRAFT_REVALIDATE_URLYour 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_SECRETSent 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.

VariablePurpose
GRAFT_STUDIO1 mounts Studio at /studio on graft serve, like --studio
GRAFT_STUDIO_PORTPort for graft studio. Default 4983
GRAFT_STUDIO_SECRETTurns on sign-in. At least 32 random characters. Signs session cookies and invite links, so changing it signs everyone out
GRAFT_STUDIO_URLPublic origin people reach Studio on. GitHub sign-in and graft studio invite need it
GRAFT_STUDIO_EDITORSWho 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_JOINEmail 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_SECRETA GitHub OAuth app, for "Sign in with GitHub". Set both or neither
GRAFT_GOOGLE_CLIENT_ID / GRAFT_GOOGLE_CLIENT_SECRETA 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_SECRETAnother 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_NAMEThe provider's name on the single sign-on button. Optional
GRAFT_GITHUB_REPOowner/name. Studio and MCP writes go to draft branches in this repository
GRAFT_GITHUB_TOKENA fine-grained token with Contents and Pull requests read and write
GRAFT_GITHUB_APP_ID / GRAFT_GITHUB_APP_PRIVATE_KEYA GitHub App instead of a token. The key is PEM, literal \n accepted
GRAFT_GITHUB_APP_INSTALLATION_IDOptional. Found from the repository when unset
GRAFT_GITHUB_BRANCHThe production branch publishing writes to. Default main
GRAFT_GITHUB_CONTENT_PATHThe 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_URLGitHub Enterprise API base. Default https://api.github.com
GRAFT_STUDIO_PUBLISHcommit (default) or pull-request. Use pull-request when the production branch is protected
GRAFT_DEPLOYED_SHAThe commit this deployment was built from. Vercel, Render and Railway set their own, which Studio reads

Assets (S3-compatible)

VariablePurpose
S3_BUCKETBucket name
S3_ENDPOINTAPI endpoint (R2, MinIO, Tigris, …)
S3_ACCESS_KEY / S3_SECRET_KEYCredentials
S3_REGIONOptional. Defaults to auto, which suits R2
S3_PUBLIC_URLOptional stable public base; else presigned GET
GRAFT_ENSURE_BUCKETContainer: set 0 when the bucket already exists

Neon branching

VariablePurpose
NEON_API_KEYCreate / drop physical CoW branches
GRAFT_NEON_PROJECT_IDNeon 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.

ExportDefaultPurpose
collectionsrequiredRecord 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
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.

SettingDefaultWhat 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
mirrornoneFor 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
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
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.