Storage engines

Pick static or Postgres, and know when to switch.

Get started5 min read

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 by graft 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:

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 Postgres

Everything you need to write, compile, and read content:

  • graft init, graft compile, and graft 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 Postgres

These 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 init adds 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 branch option is accepted and has no effect here.

Your app opens the file and reads from it:

lib/graft.tsstatic engine
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:

ToolWhy it refuses on the static engine
delete_contentDeletes 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_compilationsThe 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.

  1. Point at a database

    Set DATABASE_URL in a .env file in your project, or in any parent folder:

    DATABASE_URL=postgres://…
  2. Switch the index

    graft.config.ts
    export const index = "postgres";
  3. Create Graft's tables

    graft db migrate

    This 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-run to see what it would create first.

  4. Compile again

    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):

lib/graft.tsPostgres engine
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