Migrations

Breadcrumb owns two tables in your database. You keep them up to date one of two ways: let Breadcrumb apply the schema directly, or generate SQL migration files that you review, commit, and apply with your own tooling. Either way the schema changes are additive-only and idempotent, so they’re always safe to run.

What migrations create

Breadcrumb creates three prefixed tables, so they don’t collide with your own schema:

  • breadcrumb_spans — one row per span. This holds everything in the dashboard: trace and parent IDs, name, function ID and kind, environment, user and session, model and provider, token counts (input, output, cached, cache-write, reasoning), cost, status and error, the input/output/metadata payloads, and start/end timestamps. Traces and sessions are derived from these rows at query time.
  • breadcrumb_meta — a small key/value table used to coordinate retention sweeps across instances.
  • breadcrumb_mcp_keys — the keys that let a coding agent read your traces over MCP. Only a SHA-256 hash of each key is stored, never the key itself.

The column list lives in one place in the library and drives every adapter’s data definition language (DDL).

Full breadcrumb_spans columns
ColumnTypeNullable
idtextprimary
trace_idtextno
parent_span_idtextyes
nametextno
function_idtextyes
kindtextno
environmenttextno
user_idtextyes
session_idtextyes
modeltextyes
providertextyes
input_tokensintegeryes
output_tokensintegeryes
cached_input_tokensintegeryes
cache_write_tokensintegeryes
reasoning_tokensintegeryes
costrealyes
statustextno
errortextyes
inputjsonyes
outputjsonyes
metadatajsonyes
start_timeintegerno
end_timeintegeryes

Indexes: trace_id; (environment, start_time); (user_id, trace_id); (model, trace_id); (status, trace_id).

Choosing a workflow

  • Apply directly — Breadcrumb runs the DDL against your database. Fastest to start; ideal for local development and simple deployments.
  • Generate files — Breadcrumb writes a .sql migration you commit and apply with your own migration tooling. We recommend this for production: the change is reviewable in a pull request, your app runtime never needs DDL privileges, and your schema stays under one migration history.

Applying directly

In development, the schema is created automatically before the first database operation — start your app and the tables appear. To apply it explicitly, run the CLI:

npx breadcrumb migrate --database $DATABASE_URL

The command detects Postgres versus SQLite from the target and reports what it changed. On an already-current database, it prints already up to date.

Generating files

breadcrumb generate writes a migration file instead of touching your database. There are two modes.

Point it at a database to write only the delta — the exact statements migrate would run:

npx breadcrumb generate --database $DATABASE_URL

Or, with no connection, emit the full fresh schema for a dialect — handy for a first migration in a repo:

npx breadcrumb generate --dialect postgres

Files are written to ./breadcrumb/migrations by default (override with --out). Each is a timestamped, reviewable .sql file:

-- Generated by `breadcrumb generate` at 2026-07-24T14:07:18.778Z
-- Dialect: postgres
-- Additive-only. Review, commit, and apply with your migration tooling.

CREATE TABLE IF NOT EXISTS breadcrumb_spans (id TEXT PRIMARY KEY, …);

CREATE INDEX IF NOT EXISTS breadcrumb_spans_trace_id ON breadcrumb_spans (trace_id);

CREATE TABLE IF NOT EXISTS breadcrumb_meta (key TEXT PRIMARY KEY, value BIGINT NOT NULL);

CREATE TABLE IF NOT EXISTS breadcrumb_mcp_keys (id TEXT PRIMARY KEY, name TEXT NOT NULL, key_hash TEXT NOT NULL, key_prefix TEXT NOT NULL, created_at BIGINT NOT NULL, last_used_at BIGINT);

CREATE UNIQUE INDEX IF NOT EXISTS breadcrumb_mcp_keys_hash ON breadcrumb_mcp_keys (key_hash);

Commit the file and apply it however you run migrations — psql, Flyway, Atlas, or drizzle-kit’s SQL runner all work, since it’s plain SQL.

TIP

Generate against the database dialect you deploy to. If you develop on SQLite but ship to Postgres, run generate --dialect postgres (or point --database at a Postgres instance) so the SQL matches production.

Turning off runtime migrations

When you own migrations, stop Breadcrumb from applying DDL at runtime with the migrations option, so your app never needs schema privileges:

breadcrumb({
  // …
  migrations: "manual", // never auto-migrate; you run generate + apply yourself
});

The default is "auto", which creates the schema on first use.

How it works

Migrations don’t use version numbers or a chain of migration files. Each run reconciles the live database against the library’s column list:

  1. Check whether breadcrumb_spans exists.
  2. If it doesn’t, create it with every column.
  3. If it does, add any columns that are missing.
  4. Create each index that doesn’t exist.
  5. Create breadcrumb_meta if it’s missing.
  6. Create breadcrumb_mcp_keys and its unique index if they’re missing.

migrate executes those statements; generate writes the same statements to a file. Because the plan is a diff, both are idempotent — a second run is a no-op. New columns are always nullable, so upgrading never needs a backfill. That’s what “additive-only” means: migrations create tables, add nullable columns, and add indexes, but never drop, rename, or retype anything.

Per-adapter details

The logic is identical across adapters; only the SQL dialect differs. Each maps the library’s column types to native types:

Column typeSQLitePostgres
textTEXTTEXT
integerINTEGERBIGINT
realREALDOUBLE PRECISION
jsonTEXTJSONB

To support another database, implement the DatabaseAdapter interface, including inspectSchema() — the introspection that powers both migrate and generate.

NOTE

On Postgres, creating an index on a large existing table can briefly lock writes. The first time you migrate a big table, do it during a quiet window.

Downgrades

Migrations are forward-only, but downgrading the library is safe too. An older version ignores any columns a newer version added — the extra columns sit unused rather than causing errors. Nothing is dropped, so you can roll the library back without touching the database.

Next steps