@breadcrumb-sh/core
Embeddable LLM tracing for TypeScript apps. Your database, your deployment, your UI.
Breadcrumb captures your LLM calls (tokens, model, cost, cached and reasoning
tokens), nests spans into traces, and writes them to your own SQLite or
Postgres. You mount a fetch-native handler into your app and render the
dashboard from @breadcrumb-sh/react, or build your own UI on the
headless API here. Built on OpenTelemetry, with native support for the Vercel
AI SDK.
Install
npm i @breadcrumb-sh/core pg
npm i @breadcrumb-sh/core better-sqlite3
pg and better-sqlite3 are optional peer dependencies. Install only the
driver for the database you use.
Setup
Create one instance and mount its handler:
import { breadcrumb } from "@breadcrumb-sh/core";
import { postgres } from "@breadcrumb-sh/core/adapters";
export const bc = breadcrumb({
database: postgres(process.env.DATABASE_URL!),
basePath: "/api/breadcrumb",
authorize: (req) => isAdmin(req),
pricing: { "gpt-5": { input: 1.25, output: 10, cachedInput: 0.125 } },
});
Mount bc.handler (a (request: Request) => Promise<Response>) at basePath.
It serves the JSON API, not a UI — the dashboard is a separate route rendering
<BreadcrumbDashboard>. Framework bridges are provided:
import { toNextHandler } from "@breadcrumb-sh/core/next";
export const { GET, POST, DELETE } = toNextHandler(bc);
import { toNodeHandler } from "@breadcrumb-sh/core/node";
app.use("/api/breadcrumb", toNodeHandler(bc));
app.all("/api/breadcrumb/*", (c) => bc.handler(c.req.raw));
The schema is created automatically in development (migrations: "auto"). For
production, generate migration files with the CLI and set
migrations: "manual".
Instrumenting calls
Vercel AI SDK. bc.telemetry() returns settings for experimental_telemetry.
Calls made inside a bc.trace() callback nest into the same trace automatically:
import { generateText } from "ai";
const { text } = await generateText({
model: openai("gpt-5"),
prompt,
experimental_telemetry: bc.telemetry({ functionId: "generate-answer", userId, sessionId }),
});
functionId names the call and carries the cost attribution, wherever the call
sits in the trace; userId and sessionId group runs by user and conversation.
Your own OpenTelemetry setup. If the app already has a tracer provider
(@vercel/otel, NodeSDK, Sentry), register bc.spanProcessor on it and model
spans reach breadcrumb without threading bc.telemetry() through every call:
registerOTel({ serviceName: "app", spanProcessors: [bc.spanProcessor] });
It stores only spans breadcrumb can read (ai.*, gen_ai.*, breadcrumb.*)
unless shouldExport says otherwise, so a shared provider's HTTP and filesystem
spans don't land in the trace table.
Manual tracing. bc.trace(name, attrs?, fn), with nested t.span(...):
await bc.trace("support-reply", { userId }, async (t) => {
t.set({ input: prompt });
const docs = await t.span("retrieve", { kind: "retrieval" }, async (s) => {
const result = await search(prompt);
s.set({ output: result });
return result;
});
const answer = await callModel(prompt, docs);
t.set({ output: answer, model: "gpt-5", inputTokens, outputTokens });
});
t.set() accepts model, provider, input/output, token counts
(inputTokens, outputTokens, cachedInputTokens, cacheWriteTokens,
reasoningTokens), an explicit cost, and metadata. A thrown error marks the
span failed and rethrows.
On serverless or edge, call await bc.flush() (or waitUntil(bc.flush()))
before the response returns so no spans are lost. Set flushMode: "sync" for
those runtimes.
Entry points
@breadcrumb-sh/core | breadcrumb(), the Breadcrumb type, migration helpers (planMigration, renderMigrationSql, EMPTY_SCHEMA_STATE), and the full domain type contract. |
@breadcrumb-sh/core/adapters | sqlite(fileOrDb), postgres(connectionOrClient). |
@breadcrumb-sh/core/client | createBreadcrumbClient(), a typed browser fetch client mirroring bc.api. |
@breadcrumb-sh/core/kit | Headless UI helpers: traceModel, flowRows, selfTime, hotspots, asMessages, preview, and formatters (fmtCost, fmtTokens, fmtMs, …). |
@breadcrumb-sh/core/node | toNodeHandler() for Node/Express. |
@breadcrumb-sh/core/next | toNextHandler() for the Next.js App Router. |
Building your own dashboard
The server, the browser client, and the React hooks share one contract. Query
the server directly from a React Server Component with bc.api (listTraces,
listSessions, getTrace, stats, costSummary, …), use the typed client in
the browser, or reach for @breadcrumb-sh/react hooks. Render with
the headless kit:
import { traceModel, selfTime, asMessages, fmtCost } from "@breadcrumb-sh/core/kit";
const model = traceModel(spans);
model.rows;
model.spots;
selfTime(span, children);
const chat = asMessages(span.input);
fmtCost(0.0042);
traceModel is what the shipped waterfall renders from, so a UI you build from
scratch reads exactly the same numbers rather than reimplementing them.
Configuration
Key breadcrumb() options:
database | required | A sqlite() or postgres() adapter. |
basePath | /breadcrumb | Where the handler is mounted, and what the dashboard's api prop points at. |
environment | VERCEL_ENV ?? NODE_ENV ?? development | Stamped on every span. |
authorize | none | Guards the query routes. Your dashboard page is yours to guard. |
ingest | none | { apiKey } enables HTTP ingest endpoints. |
pricing | none | USD per 1M tokens, keyed by model, for cost. |
retention | 90d | Per-environment retention windows. |
redact | none | Scrub or trim each span before storage. |
maxPayloadChars | 16384 | Truncate captured input/output (0 disables). |
flushMode | batch | sync for serverless/edge. |
migrations | auto | manual runs no runtime DDL. |
Breadcrumb ships no default prices. Omit pricing and only costs you set
yourself are stored. See the full reference at
breadcrumb.sh/docs/configuration.
License
MIT