# TanStack Start and Lovable

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

`@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.

> [!IMPORTANT]
> Keep what your app already does. If `src/server.ts` or `src/start.ts` exist, add to them — do not replace them. Existing middleware (security headers, CSRF, authentication, error pages) stays in place and in order.

## 1. Install

```bash
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:

```ts title="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:

```ts title="src/router.tsx"
import "./instrument"; // first line, before the route tree
import { 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 `errorComponent`s 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`

```ts title="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:

```ts
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`

```ts title="src/start.ts"
import "./instrument"; // first line: the browser loads start.ts before the router
import { 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

| 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`](https://belvador.dev/en/docs/sdk/configuration#common-options) 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):

```ts
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:

```ts title="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:

```bash
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](https://belvador.dev/en/docs/sdk/verify).

## 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](https://belvador.dev/en/docs/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](https://belvador.dev/en/docs/troubleshooting).
