Storage engines
Pick static or Postgres, and know when to switch.
When you compile, Graft builds an index: a searchable copy of your content that your app reads from. The storage engine decides where that index lives. It does not change how you write content, how you read it, or what your schema looks like.
There are two engines:
- Static. The index is a single SQLite file,
.graft/index.db, built bygraft compile. You run no database and no services. - Postgres. The index lives in a Postgres database. You need this engine when your users write data at runtime, such as form submissions, comments, or orders, because that data cannot be rebuilt from files in Git.
Start static. Move to Postgres the day you need one of the features in What only Postgres can do.
Which engine am I on?
Look for the index export in graft.config.ts:
export const index = "static"; // or "postgres"graft init writes "static" for you. If a config has no index export at
all, Graft uses Postgres, so older projects keep working. Set it explicitly and
you never have to remember that rule.
What works on both
static or PostgresEverything you need to write, compile, and read content:
graft init,graft compile, andgraft dev- Typed reads through every framework package
- Full-text search, with ranking and highlighted snippets
- Agents over MCP with
graft mcp, including document resources and prompts - Uploading assets with
graft asset put, once storage credentials are set - Editing content in Studio with
graft studio: each save updates.graft/index.db(Studio)
What only Postgres can do
needs PostgresThese features store data that Git cannot rebuild, so they need a database:
- Runtime data: collections your users write to, declared with
authority: "db-authoritative"(Operational data) - Typed functions that write that data, served at
POST /api/fn/<name>(Functions & access) - The audit log, rate limits, and human approvals for agent actions (Audit & approvals)
- Preview branches and
graft merge(Branching & previews) - Studio's team features: accounts and sign-in for a hosted Studio,
approvals, the compile history, the media library, agent tokens, and
graft serve(Studio, Host Studio for your team)
If a static project declares one of these, Graft refuses to load the config and tells you which declaration needs Postgres:
graft.config.ts uses the static index, but declares typed function(s)
"submitContact" — those live in Postgres, not in the compiled artifact.
That error is NEEDS_DATABASE. Its fix lists the steps in
Moving to Postgres.
Working with the static index file
graft compile writes .graft/index.db. A few things to know about it:
- It is generated, so keep it out of Git.
graft initadds it to.gitignore. Rebuild it as part of your build command:graft compile && next build. - A failed compile never leaves a half-written file. Each compile builds a new file and swaps it in only when it is complete.
- It works on a read-only filesystem. Your app opens it read-only.
- It needs Node 22.16 or newer, which ships
node:sqlite. There is no native dependency to install. - Each Git checkout has its own file. That is how branches work on the
static engine: a different checkout compiles a different index. The SDK's
branchoption is accepted and has no effect here.
Your app opens the file and reads from it:
import { openStaticIndex } from "@usegraft/db";
import { createClient } from "@usegraft/sdk-core";
import { collections } from "../graft.config";
const index = await openStaticIndex(".graft/index.db");
export const graft = createClient({ index, collections });If your app reads before the first compile, you get STATIC_INDEX_NOT_FOUND.
If your Node version is too old, you get STATIC_INDEX_UNSUPPORTED.
Agents on the static engine
Agents work on both engines. graft mcp serves a static project, an agent's
write_content call checks the file and recompiles it the same way, and
search_content searches the static index.
Tools that need Postgres stay listed, so an agent can see they exist. Calling
one returns NEEDS_DATABASE and the steps to upgrade. Two tools refuse on
purpose:
| Tool | Why it refuses on the static engine |
|---|---|
delete_content | Deletes wait for a human approval, and approvals are stored in Postgres. Allowing the delete without that check would skip the approval. On the static engine, delete the file and recompile. Git history is your undo. |
list_compilations | The static file keeps only the last 50 compile runs. Returning a partial history under the same tool name could mislead an agent, so the tool reports that it needs Postgres instead. |
Moving to Postgres
Four steps, in this order.
Set
DATABASE_URLin a.envfile in your project, or in any parent folder:DATABASE_URL=postgres://…graft.config.ts export const index = "postgres";graft db migrateThis creates the tables Graft needs. The SQL ships inside
@usegraft/db, so there is nothing else to install. It is safe to run again: it only adds what is missing. Pass--dry-runto see what it would create first.graft compile
Then give your client the database instead of the file. With a framework
package, pass db where you passed index (Next.js shown):
import { createDb } from "@usegraft/db";
import { createGraft } from "@usegraft/sdk-next";
import { collections } from "@/graft.config";
export const graft = createGraft({
db: createDb(process.env.DATABASE_URL!).db,
collections,
});Without a framework, use createDbClient from @usegraft/sdk-core/db in the
same way.
Moving back to static
Set index = "static" again. The config refuses to load while it still
declares typed functions or db-authoritative collections, so remove those
first. If one came from a graft add primitive, delete it from graft/.
Switching back does not delete anything from Postgres. Rows your users wrote stay in the database.
One database per project
On the Postgres engine, each project needs its own database, or its own branch
of one. Compiling makes the index match your current schema exactly, so two
projects sharing a database would each delete the other's content. Graft stops
the compile with INDEX_OWNERSHIP before that can happen. See
How Graft works.
The static engine cannot hit this, because each project has its own file.
Next steps
- Getting started runs the static setup end to end.
- Operational data shows what to build once you move to Postgres.
- Configuration lists every environment variable.
- Deploy covers hosting either engine.