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
| Column | Type | Nullable |
|---|---|---|
id | text | primary |
trace_id | text | no |
parent_span_id | text | yes |
name | text | no |
function_id | text | yes |
kind | text | no |
environment | text | no |
user_id | text | yes |
session_id | text | yes |
model | text | yes |
provider | text | yes |
input_tokens | integer | yes |
output_tokens | integer | yes |
cached_input_tokens | integer | yes |
cache_write_tokens | integer | yes |
reasoning_tokens | integer | yes |
cost | real | yes |
status | text | no |
error | text | yes |
input | json | yes |
output | json | yes |
metadata | json | yes |
start_time | integer | no |
end_time | integer | yes |
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
.sqlmigration 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:
- Check whether
breadcrumb_spansexists. - If it doesn’t, create it with every column.
- If it does, add any columns that are missing.
- Create each index that doesn’t exist.
- Create
breadcrumb_metaif it’s missing. - Create
breadcrumb_mcp_keysand 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 type | SQLite | Postgres |
|---|---|---|
text | TEXT | TEXT |
integer | INTEGER | BIGINT |
real | REAL | DOUBLE PRECISION |
json | TEXT | JSONB |
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
- Local development: automatic migrations with SQLite.
- Production: generating and applying migrations on deploy.