TanStack Start and Lovable
Wire @belvador/tanstack-start into both the browser and the Cloudflare Workers server: router, server entry and middleware.
On this page
@belvador/tanstack-start reports errors from both halves of a TanStack Start app: the browser and the server running on Cloudflare Workers. Apps built with Lovable are TanStack Start apps on Workers, so this is the guide for them too.
One package, but three places to wire it: the router (browser), the server entry and the middleware. Each part reports what only it can see.
Before you start#
A project with platform TanStack Start / Lovable and its DSN. The browser and server parts use the same project.
@tanstack/react-routerand@tanstack/react-start1.168 or newer, React 18 or 19.The server runs on Cloudflare Workers with the
nodejs_compatcompatibility flag. Lovable apps already have it.A custom server entry:
vite.config.tssetstanstackStart: { server: { entry: "server" } }. Lovable apps already have it.
1. Install#
npm install @belvador/tanstack-start@0.6.12. Browser — src/instrument.ts and src/router.tsx#
Create src/instrument.ts. It starts Belvador in the browser:
import { init } from "@belvador/tanstack-start/client";// Imported first by src/start.ts and src/router.tsx. Runs only in the browser.if (typeof window !== "undefined") { init({ dsn: "https://<public-key>@ingest.belvador.dev/<project-id>", release: import.meta.env.VITE_APP_VERSION, // your build or commit id environment: import.meta.env.MODE, });}Import it on the first line of src/router.tsx, before the route tree:
import "./instrument"; // first line, before the route treeimport { withBelvadorRouter } from "@belvador/tanstack-start/client";// In getRouter(): wrap the options you already pass to createRouter.// createRouter(withBelvadorRouter({ routeTree, ...yourOptions }))// It reports route errors, calls your own defaultOnCatch after that, and adds// TanStack's error component only if you set no defaultErrorComponent.initruns only in the browser (typeof window !== "undefined"); the server part is configured separately below.Imports run before the rest of a file. With
initin the body ofsrc/router.tsx, an error thrown while a route module orsrc/start.tsloads happens beforeinitand is never reported; the page renders from the server but nothing on it works. Step 4 imports the same file first insrc/start.ts, which the browser loads before the router.withBelvadorRouter(options)wraps the options you already pass tocreateRouter. It setsdefaultOnCatchto report errors that route error boundaries catch, then calls your owndefaultOnCatchif you had one. TanStack Router callsdefaultOnCatchonly where an error component exists (its outermost fallback does not), so when you set nodefaultErrorComponentthe helper uses TanStack's ownErrorComponent. Route-levelerrorComponents still win. In our lab, a route that crashed in an app without any error component was reported 0 times out of 3 withdefaultOnCatch: belvadorOnCatchalone and 3 out of 3 with the helper.Before 0.5.0 the recipe was
defaultOnCatch: belvadorOnCatchplus an error component; that still works, andbelvadorOnCatchstays exported.Uncaught errors and unhandled promise rejections in the browser are captured once
inithas run.Do not name the file
instrument.client.ts: TanStack Start's import protection rejects.clientfiles imported fromsrc/router.tsx, which runs on the server too.
3. Server — src/server.ts#
import handler from "@tanstack/react-start/server-entry";import { withBelvador } from "@belvador/tanstack-start/server";// Requires Cloudflare Workers with nodejs_compat enabled.// If src/server.ts already exists, wrap its default export instead of `handler`.// vite.config.ts needs tanstackStart: { server: { entry: "server" } } (Lovable apps have it).export default withBelvador( (env: { BELVADOR_DSN?: string; BELVADOR_RELEASE?: string }) => ({ dsn: env.BELVADOR_DSN ?? "https://<public-key>@ingest.belvador.dev/<project-id>", release: env.BELVADOR_RELEASE, }), handler,);If your app already has src/server.ts — Lovable apps do — wrap its default export instead of handler, and keep everything it does, for example:
import { withBelvador } from "@belvador/tanstack-start/server";const app = { async fetch(request: Request, env: unknown, ctx: unknown) { // …your existing server entry, unchanged… },};export default withBelvador( (env: { BELVADOR_DSN?: string; BELVADOR_RELEASE?: string }) => ({ dsn: env.BELVADOR_DSN ?? "https://<public-key>@ingest.belvador.dev/<project-id>", release: env.BELVADOR_RELEASE, }), app,);withBelvador gives every request its own scope, reports errors and re-throws them, and returns your response unchanged. Sending finishes after the response, in waitUntil.
Server settings come from Worker variables when present: BELVADOR_DSN, BELVADOR_RELEASE (else CF_VERSION_METADATA.id), BELVADOR_ENVIRONMENT (default production) and BELVADOR_DISABLED=1. The SDK never reads SENTRY_* variables.
4. Middleware — src/start.ts#
import "./instrument"; // first line: the browser loads start.ts before the routerimport { createStart } from "@tanstack/react-start";import { belvadorFunctionMiddleware, belvadorRequestMiddleware } from "@belvador/tanstack-start";export const startInstance = createStart(() => ({ // Last in each list, after your own middleware. requestMiddleware: [/* …your middleware, */ belvadorRequestMiddleware], functionMiddleware: [/* …your middleware, */ belvadorFunctionMiddleware],}));The first line imports ./instrument, as in step 2: in the browser src/start.ts and its imports load before the router. Put both middleware last in their lists — closest to your handlers — so they see an error before an error-page middleware turns it into a response. If src/start.ts already exists, append them to the existing arrays and keep the rest.
5. CSP#
connect-src 'self' https://ingest.belvador.dev;Merge this into the app's existing policy — for example where security headers are built — rather than replacing it.
What is reported#
| Where | Reported | Not reported |
|---|---|---|
| Browser | Uncaught errors, unhandled rejections, route error boundaries (defaultOnCatch) | Errors your code catches and handles; AbortError from cancelled requests |
| Server functions | Thrown errors, with the server stack | redirect(), notFound(), errors with a status below 500 (499 included), requests the visitor cancelled |
| Server routes | Thrown errors, with the server stack | Responses you return yourself, even with status 500 — only thrown errors are seen; requests the visitor cancelled |
| Route loaders | Thrown errors — during SSR from the server, with the server stack; after a client navigation from the browser. Each error once | redirect(), notFound() and the other errors listed for server functions |
A cancelled request is one whose visitor closed the tab or navigated away: an AbortError, or any error thrown after the client disconnected. These are not bugs, so from 0.2.0 they are dropped; set captureAborted: true to report them.
Loader reporting needs withBelvadorRouter({ routeTree, ... }) from step 2 (from 0.5.1) and the server half from step 3. Without the server half, an SSR loader error is still reported once — from the browser, without the server stack. If you set your own dehydrate/hydrate, pass them inside withBelvadorRouter: the SDK adds a short hash of each error the server reported (never the message) and your hydrate receives exactly what your dehydrate returned. Integrations that wrap them after createRouter, such as the TanStack Query SSR integration, keep working.
Check that it works#
Run the connection check for both halves — the project's Connection tab shows each one turn Verified.
Browser, once (for example from a debug button):
import { verifyIntegration } from "@belvador/tanstack-start/client";// Run once after setup or deploy (for example behind a debug flag), never on every page load.// The console shows one expected "Belvador verification (uncaught)" error — that is the test.const result = await verifyIntegration();// result.outcome: "verified" | "problem" | "unconfirmed"; the console prints one summary line.Server — a temporary route that must run inside the middleware:
import { createFileRoute } from "@tanstack/react-router";import { verifyIntegration } from "@belvador/tanstack-start/server";// Temporary: open /api/verify-belvador once after deploying, then delete this file.// It must run inside belvadorRequestMiddleware (src/start.ts) — that is part of the check.export const Route = createFileRoute("/api/verify-belvador")({ server: { handlers: { GET: async () => Response.json(await verifyIntegration()), }, },});Then read the result back, or watch the Connection tab:
curl -s "https://ingest.belvador.dev/api/<project-id>/verification/bv1_<id printed by your app>/?sentry_key=<public-key>"Remove the temporary code afterwards. Every result and code is explained in Verify the connection.
Lovable apps#
Ask the Lovable agent to do the setup: on the project page in Belvador, copy Paste into your AI agent and send it in your Lovable chat. The prompt contains the exact steps above with your DSN. Guide for AI agents lists what the agent must keep intact.
Troubleshooting#
Only browser errors arrive — check the middleware is last in both lists,
src/server.tswraps the real entry, andnodejs_compatis enabled.Errors arrive twice — remove any old
@sentry/*setup andSentry.initcall.More in Troubleshooting.
Posts on this topic
All posts- A prompt to add error tracking to a Lovable app, and how to check itThe prompt to add error tracking to a Lovable app: which files, in which order, what to keep, and how to prove it works. Old order 0 of 12, fixed 12 of 12.
- Monitoring vibe-coded apps: what breaks after you publishWhat breaks in a vibe-coded app after you publish, why uptime checks still see 200 OK, and the three checks that catch it. Tested on five real failures.
- TanStack Start loader errors: why the SSR stack points at your HTML pageWhy a TanStack Start loader error thrown during SSR shows a stack from your HTML page, how reporting from the server can double it, and how to get it once.
Spotted something wrong or missing? Tell us: hello@belvador.dev