How Graft works

Four ideas that explain the rest of the docs.

Get started3 min read

After Getting started you have a project that compiles and a page that reads content. This page explains what happened, so the rest of the docs make sense. It introduces four ideas, one at a time:

  1. Your files are the source of truth.
  2. Content and user data are kept apart.
  3. One schema checks everything.
  4. Errors tell you what to do next.

Your files are the source of truth

You write content as files in Git. Your app does not read those files directly, because searching and filtering hundreds of files on every request would be slow. Instead, graft compile reads them, checks each one, and builds an index: a copy of your content organized for fast queries.

The index is always a copy. If it is lost, deleted, or out of date, run graft compile and it is rebuilt from your files. This is why Graft calls the index derived: everything in it comes from Git.

Compiling only changes what changed. Graft compares each file with what the index already holds and updates just the documents that differ, so unchanged documents keep their timestamps. The list of documents a compile added, changed, or removed is called a ChangeSet. Framework packages use it to refresh only the cached pages that changed.

Content and user data are kept apart

Some data cannot live in files. When a visitor submits a form, places an order, or leaves a comment, that data is created at runtime, by your users, and there is no file in Git to rebuild it from. Graft calls this operational data.

The two kinds are stored and written differently:

ContentOperational data
Examplespages, docs, productsform submissions, orders, comments
Source of truthfiles in Gitrows in Postgres
Who writes ityou and agents, by editing filesyour users, through typed functions
How you declare ita normal collectiona collection with authority: "db-authoritative"

Operational data needs the Postgres engine, because that is where its rows live. If a collection is declared one way and written the other, Graft stops with AUTHORITY_MISMATCH and tells you which collection is wrong. Operational data covers it in full.

One schema checks everything

You describe each collection once, in graft.config.ts. That one description is used everywhere data crosses a boundary:

  • graft compile checks every file's frontmatter against it.
  • Typed functions check their inputs against the input schema each one declares. That can reuse a collection's fields (input: submissions.fields) or describe something else entirely, so a collection's rules apply to a function only when its input uses them.
  • Reads check documents on the way out.

The schema is built on Zod, a TypeScript validation library, so you get its types for free. There is no code generation step: getContent("pages", "home") is typed by the same object that checked the file. Schema shows how to write one.

Errors tell you what to do next

Every Graft error has a code, a message saying what happened, and a fix naming the next step. The fix is written so that both you and an agent can act on it without searching the docs:

{
  "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 …"
}

The Error reference lists every code.

One database per project

On the Postgres engine, compiling makes the index match your current schema exactly. That includes removing collections your schema no longer declares. If two projects pointed at the same database, each compile would delete the other project's content.

Graft prevents this. If a compile would remove collections your schema does not know about, it stops with INDEX_OWNERSHIP and writes nothing. Give each project its own database, or its own branch of one.

If you renamed or deleted a collection on purpose, run the compile with --prune-unknown to allow the removal.

Next steps