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 content | Operational data | |
|---|---|---|
| Owner | git (files) | Postgres (rows) |
| Examples | pages, docs, products | form submissions, orders, comments |
| Written by | MDX + compile, or MCP write_content | typed functions only |
| Collection flag | default (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
| Surface | Authored content | Operational data |
|---|---|---|
| Agent (MCP / CLI) | Write files → compile | Call typed mutations |
| You (editor / git) | Same files; commit as usual | Same functions (or CLI approve for destructive calls) |
| Frontend SDK | Read (+ 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
- Schema — define collections and fields
- Getting started — run the loop
- Branching & previews — overlay and Neon backends