---
title: Tanstack Start
description: The build fetches your public policy once and bundles it, so the
  server never fetches it at runtime.
lastModified: "2026-10-10T14:59:21+01:00"
lastAuthor: Kaylee
---
The build fetches your public policy once and bundles it, so the server never
fetches it at runtime.

Add the plugin before `tanstackStart()`:

```ts title="vite.config.ts"
import { tanstackStart } from '@tanstack/react-start/plugin/vite';
import viteReact from '@vitejs/plugin-react';
import { consentManifest } from 'c15t/tanstack-start/build';
import { defineConfig } from 'vite';

export default defineConfig({
	plugins: [consentManifest(), tanstackStart(), viteReact()],
});
```

The plugin reads the backend URL from `backendURL`, or from
`VITE_C15T_BACKEND_URL`, then `VITE_INTH_PROJECT_URL`, when you leave
`backendURL` out. If `VITE_C15T_BACKEND_URL` is unset, the plugin sets it to the URL it used, so app code can read
`import.meta.env.VITE_C15T_BACKEND_URL` instead of repeating the URL.

The plugin fetches the policy during `vite build`, or in `vite dev` when the
server first loads it, and serves it to server code as `snapshot` from `c15t/generated`. In the browser
bundle, `snapshot` is `undefined`, so the policy never ships to the browser.
`createConsentStateHandler()` reads the snapshot and the backend URL from
there, as the
[quickstart root route](/docs/frameworks/tanstack-start/quickstart#resolve-consent-in-the-root-route)
shows:

```ts title="src/routes/__root.tsx"
import { createServerFn } from '@tanstack/react-start';
import { createConsentStateHandler } from 'c15t/tanstack-start/server';

// Reads the backend URL and the policy consentManifest() downloaded.
const getConsentState = createServerFn({ method: 'GET' }).handler(
	createConsentStateHandler()
);
```

`createConsentRoute()` reads them the same way.

The snapshot is fixed at build time:

* Rebuild after changing policies, translations or vendors. If your CI caches
  build output, force a fresh build.
* Consent choices still go to the backend.

The build reads the backend URL from your public backend URL variable when
the config doesn't pass one: `NEXT_PUBLIC_C15T_BACKEND_URL` in Next.js,
`NUXT_PUBLIC_C15T_BACKEND_URL` in Nuxt, `PUBLIC_C15T_BACKEND_URL` in Astro,
Svelte and SvelteKit, and `VITE_C15T_BACKEND_URL` in TanStack Start and other
Vite apps. Each also reads the matching Inth variable, such as
`NEXT_PUBLIC_INTH_PROJECT_URL`, when the c15t one is unset. See
[set the backend URL](/docs/concepts/modes#set-the-backend-url).

The fetch waits at most 10 seconds. When it fails, or no backend URL is set,
every framework does the same thing:

|Command|Default when the fetch fails|
|--|--|
|Production build: `next build`, `vite build`, `nuxt build`, `astro build`|The build stops with an error.|
|Dev: `next dev`, `vite dev`, `nuxt dev`, `astro dev`|A warning, and the server fetches the policy at runtime.|

Set `onBuildError` to use one behaviour for both. `'fail'` stops dev too.
`'runtime'` lets a production build finish, and the server fetches the policy
at runtime. The `C15T_ON_BUILD_ERROR` environment variable overrides the
option, so you can deploy during a backend outage without a code change:

```sh
C15T_ON_BUILD_ERROR=runtime npm run build
```

Turborepo's strict environment mode hides undeclared variables from tasks, so
list `C15T_ON_BUILD_ERROR` in the build task's `passThroughEnv` there.

The build skips the fetch, without an error, when it can't use a snapshot,
for example when the backend URL is relative. With `onBuildError: 'fail'`, a
relative URL stops the build. [Consent modes](/docs/concepts/modes#what-happens-when-the-download-fails)
lists every case.

`vite build` and `vite dev` fetch the manifest when they start. `vite preview`
serves the last build without fetching. The plugin writes no file into your
app, so there is nothing to keep out of Git. `c15t/generated` ships its own
types, so `tsc`, `vue-tsc` and `svelte-check` pass on a fresh checkout without
a build first.

The plugin can't see the options your app passes to `manifest()`. When the
app passes `source: 'runtime'` or `manifestURL`, set
`consentManifest({ source: 'runtime' })` too. The build then downloads no
manifest, so it doesn't fail when the backend's `/manifest` is down.

If the fetch fails, `vite build` stops. `vite dev` logs a warning instead,
and `snapshot` is `undefined`, so the server fetches and caches the policy at
runtime. To let a build continue the same way, set `onBuildError: 'runtime'`,
or run it with `C15T_ON_BUILD_ERROR=runtime`:

```ts title="vite.config.ts"
consentManifest({ onBuildError: 'runtime' }),
```

For policy updates without a rebuild, pass
`mode: manifest({ source: 'runtime' })` to `createConsentStateHandler` and
keep the plugin, which still supplies the backend URL. See
[runtime fetching](/docs/frameworks/tanstack-start/rendering#await-consent-in-the-loader).
