Skip to content

TanStack Start and Lovable

Wire @belvador/tanstack-start into both the browser and the Cloudflare Workers server: router, server entry and middleware.

Markdown
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-router and @tanstack/react-start 1.168 or newer, React 18 or 19.

  • The server runs on Cloudflare Workers with the nodejs_compat compatibility flag. Lovable apps already have it.

  • A custom server entry: vite.config.ts sets tanstackStart: { server: { entry: "server" } }. Lovable apps already have it.

1. Install#

Terminal
npm install @belvador/tanstack-start@0.6.1

2. Browser — src/instrument.ts and src/router.tsx#

Create src/instrument.ts. It starts Belvador in the browser:

src/instrument.ts
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:

src/router.tsx
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.
  • init runs only in the browser (typeof window !== "undefined"); the server part is configured separately below.

  • Imports run before the rest of a file. With init in the body of src/router.tsx, an error thrown while a route module or src/start.ts loads happens before init and is never reported; the page renders from the server but nothing on it works. Step 4 imports the same file first in src/start.ts, which the browser loads before the router.

  • withBelvadorRouter(options) wraps the options you already pass to createRouter. It sets defaultOnCatch to report errors that route error boundaries catch, then calls your own defaultOnCatch if you had one. TanStack Router calls defaultOnCatch only where an error component exists (its outermost fallback does not), so when you set no defaultErrorComponent the helper uses TanStack's own ErrorComponent. Route-level errorComponents still win. In our lab, a route that crashed in an app without any error component was reported 0 times out of 3 with defaultOnCatch: belvadorOnCatch alone and 3 out of 3 with the helper.

  • Before 0.5.0 the recipe was defaultOnCatch: belvadorOnCatch plus an error component; that still works, and belvadorOnCatch stays exported.

  • Uncaught errors and unhandled promise rejections in the browser are captured once init has run.

  • Do not name the file instrument.client.ts: TanStack Start's import protection rejects .client files imported from src/router.tsx, which runs on the server too.

3. Server — src/server.ts#

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:

TypeScript
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#

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#

Text
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#

WhereReportedNot reported
BrowserUncaught errors, unhandled rejections, route error boundaries (defaultOnCatch)Errors your code catches and handles; AbortError from cancelled requests
Server functionsThrown errors, with the server stackredirect(), notFound(), errors with a status below 500 (499 included), requests the visitor cancelled
Server routesThrown errors, with the server stackResponses you return yourself, even with status 500 — only thrown errors are seen; requests the visitor cancelled
Route loadersThrown errors — during SSR from the server, with the server stack; after a client navigation from the browser. Each error onceredirect(), 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):

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

src/routes/api/verify-belvador.ts
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:

Terminal
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.ts wraps the real entry, and nodejs_compat is enabled.

  • Errors arrive twice — remove any old @sentry/* setup and Sentry.init call.

  • More in Troubleshooting.

Posts on this topic

All posts

Spotted something wrong or missing? Tell us: hello@belvador.dev