graft. docs

The model

Git owns authored content. Postgres is a derived index. One Zod layer validates everything.

Authored content is MDX in your repo. graft compile validates each file and projects it into content_index. The projection is atomic and hash-diffed: unchanged rows keep timestamps; the ChangeSet lists what moved.

Authoring and ops go through files, the CLI, and MCP. There is no mandatory admin dashboard. Graft is open source and self-hostable on your own Postgres.

Two kinds of data

Authored contentOperational data
Ownergit (files)Postgres (rows)
Examplespages, docs, productsform submissions, orders, comments
Written byMDX + compile, or MCP write_contenttyped functions only
Collection flagdefault (file-authoritative)authority: "db-authoritative"

Operational data never lives in files. Authored content is never written through the function API as rows. Mixing the two raises AUTHORITY_MISMATCH at compile time and at the function boundary.

Edit surfaces

SurfaceAuthored contentOperational data
Agent (MCP / CLI)Write files → compileCall typed mutations
You (editor / git)Same files; commit as usualSame functions (or CLI approve for destructive calls)
Frontend SDKRead (+ optional future visual-edit → file diffs)Read / policy-scoped write

One Zod layer

The schema in graft.config.ts is Zod under the hood. It is the only validation layer:

  • the compiler validates frontmatter against it
  • functions validate inputs against it
  • reads re-validate on the way out

There is no codegen step. defineCollection infers document types, so getContent("pages", "home") is typed by the same object that validated the file.

Errors teach

Every boundary error is a GraftError with a fix — the next concrete step, written so an agent can act:

{
  "error": "INDEX_OWNERSHIP",
  "message": "Refusing to project: this would remove every document in collection(s) \"products\" …",
  "fix": "Each Graft project needs its own database (or branch): point DATABASE_URL at …"
}

One database per project

Projection makes the index match your content tree. Pointing two projects at one DATABASE_URL would let each purge the other's documents. Graft refuses: if a compile would remove collections your schema does not know, it aborts with INDEX_OWNERSHIP and writes nothing.

Use --prune-unknown only when you renamed or deleted a collection on purpose.

Next steps