Skip to main content

TanStack Start Verify and troubleshoot

Troubleshooting

vite build or vite dev can't fetch the manifest

Check. Read the message. The consentManifest plugin downloads the manifest once per build, and in vite dev the first time the server loads it. When the download fails or takes longer than 10 seconds, vite build stops with an error that starts with @c15t/tanstack-start/build: could not fetch the consent manifest from <url> during the build. vite dev logs the same message as a warning and keeps going. snapshot from c15t/generated is then undefined, so the server fetches the policy at runtime.

MessageFix
no backend URL is setSet VITE_C15T_BACKEND_URL (or VITE_INTH_PROJECT_URL) in .env or the build environment, or pass backendURL. vite build stops on this; vite dev warns
skipped the consent manifest fetch because ... is not an absolute http(s) URLSet VITE_C15T_BACKEND_URL to the absolute backend URL, such as https://your-project.inth.app, not /api/c15t. With onBuildError: 'fail', this is the error build-time manifests require an absolute upstream URL
/manifest responded 404The URL is not a c15t backend, or it is missing a path prefix. Copy the backend URL from Inth exactly
/manifest responded with another statusThe backend refused or failed the request. Check that the project exists and that the build machine can reach it
/manifest returned an invalid consent manifestThe URL answered with something other than a c15t manifest, such as an HTML page. Check the URL
fetch failed, with ENOTFOUND or another network error, or no response within 10 secondsThe build machine cannot reach the backend. Allow the request. To deploy while the backend is down, build with C15T_ON_BUILD_ERROR=runtime, or switch to runtime fetching

snapshot is undefined

Check. Log snapshot from c15t/generated in the server function. In the browser bundle it is always undefined: the plugin keeps the policy on the server. On the server, it is undefined when Vite had no policy to serve: consentManifest is missing from plugins in vite.config.ts, the fetch failed in vite dev or in a build with C15T_ON_BUILD_ERROR=runtime, or the backend URL is relative.

Fix. createConsentStateHandler and createConsentRoute read the snapshot themselves, so most apps never import it. On the server, add the plugin or fix the fetch error, then restart vite dev or rebuild. While snapshot is undefined, the server fetches and caches the policy at runtime.

The banner is not in the server HTML

Check. View the page source under a policy that asks for a choice. Look for data-testid="consent-banner-root".

CauseFix
The root loader streams the consent promise and sets streamBanner: falseExpected. The banner mounts after hydration. Remove the option, or await the server function; see rendering
The page is prerendered or the app runs in SPA modeExpected. The browser resolves consent after the page loads
With runtime fetching, the backend took longer than 500 ms, often on the first request after a startThe server rendered without consent and the browser resolved it. Later requests use the cached manifest. Raise timeoutMs in createConsentStateHandler if needed
No country header reaches the serverCheck that your host or CDN adds a geo header; geography headers lists the ones c15t reads. Without one the visitor gets your fallback policy
The visitor has a stored choiceExpected. The server reads the c15t cookie and renders no banner

The server function fails to resolve at runtime

Check. Find where you call createServerFn(). Start's compiler identifies each server function by the file and the top-level assignment where you call it, so a call inside a package, a function or a conditional fails.

Fix. Declare const getConsentState = createServerFn(...).handler(createConsentStateHandler()) at the top level of your own route module, as in the quickstart.

A serialization error mentions a function

Check. Look at what your server functions and loaders return. Something with functions in it, such as vendor script configurations or a hosted() transport from c15t/react, cannot be serialized. hosted() from c15t/tanstack-start is plain data and can be returned.

Fix. Return only the consent state from the server function. Import scripts in the root route module and pass them to ConsentRoot there.

The same-origin setup never renders the banner on the server

Check. Look at the backendURL you pass to createConsentStateHandler, or VITE_C15T_BACKEND_URL. When it is /api/c15t, the helper skips the fetch and returns only the cookie and header state, because a server that fetches its own route during a render can stall.

Fix. Keep VITE_C15T_BACKEND_URL absolute, and don't pass backendURL: '/api/c15t'. To send the browser's requests through the route, pass routePrefix: '/api/c15t' and proxy: true to createConsentStateHandler instead.

Saves through /api/c15t fail with 403 or a challenge page

Check. The backend's firewall blocks the proxied POST /subjects. A server cannot solve a browser challenge, such as Vercel Attack Challenge Mode or Cloudflare Super Bot Fight Mode.

Fix. Exempt the consent paths from the challenge. Requests from the route carry an x-c15t-proxy: @c15t/tanstack-start header you can match in a firewall rule. If rate limits treat every visitor as one IP address, and your host overwrites x-forwarded-for, set trustForwardedHeaders: true on createConsentRoute.

The browser calls /api/c15t/init and gets a 404

Check. ConsentRoot calls /api/c15t/init only when the state carries routePrefix: '/api/c15t', from createConsentStateHandler. Confirm that the app mounts the consent route at src/routes/api/c15t/$.ts.

Fix. Mount the same-origin route, or remove routePrefix, so the browser calls your backend directly. Earlier alphas sent init to /api/c15t/init by default; now there is no default.

ConsentRoot throws that it needs a backend URL

Check. The page throws one of these errors from ConsentRoot:

  • c15t: manifest() needs a backend URL. Set `backendURL` in your config.
  • c15t: hosted() needs a backend URL. Set `backendURL` on the mode or in your config.

The state names no backend URL and consentManifest() supplied none, for example on a page with state={{}} and no plugin in vite.config.ts.

Fix. Add consentManifest() to vite.config.ts and set VITE_C15T_BACKEND_URL (or VITE_INTH_PROJECT_URL). Earlier alphas fell back to offline mode here; now the root throws. To run without a backend, pass mode: offline() to createConsentStateHandler, or state={{ mode: offline() }}.

A vendor loads before the visitor chooses

Check. Search route head() definitions, your root component and any tag manager for the vendor's script or SDK initializer.

Fix. Remove every loader except the one in ConsentRoot's scripts. c15t only gates scripts registered with it. Scripts and embeds covers the options.

The page throws that no IABProvider is mounted

The visitor's policy uses the iab model and the backend sent its vendor list, but no IABProvider is mounted. The standard banner and dialog do not handle the IAB model, so they throw rather than leave the visitor with no consent UI. Server and browser render the same error.

Fix it one of two ways:

  • If the site uses IAB TCF, render IABProvider with IABConsentBanner and IABConsentDialog from c15t/react/iab inside your consent provider. They can sit next to the standard banner.
  • If it does not, remove the iab model from the policy for that region in your Inth project or policy pack.

A backend that answers gvl: null turns IAB off for that request. The policy then runs as opt-in and nothing throws.

The check passes once c15t/react/iab has loaded. If you import the IAB components with a dynamic import(), a render of the standard banner that happens before the import finishes still throws.

Inspect the active policy

Render this component inside ConsentRoot. It reads state and records no choice:

src/components/consent-policy-debug.tsx
import { useSnapshot } from 'c15t/tanstack-start';

export function ConsentPolicyDebug() {
	const snapshot = useSnapshot();
	const resolution = snapshot.resolution;

	return (
		<pre>
			{JSON.stringify(
				{
					status: resolution.status,
					policy: resolution.status === 'matched' ? resolution.policy.id : null,
					prompt: snapshot.promptRequirement,
					location: snapshot.location,
				},
				null,
				2
			)}
		</pre>
	);
}

location shows the country and region the server resolved. Remove the component after you finish.

Names from an earlier alpha are missing

Check. TypeScript or the build reports a missing export or option, such as createConsentServerRoute, DEFAULT_INIT_ROUTE or ConsentRoot's backendURL prop. The v3 alphas removed names that never shipped in a stable release, with no alias.

Fix. Use the current names:

RemovedUse instead
ConsentRoot's backendURL and routePrefix propscreateConsentStateHandler({ routePrefix }). The state carries the backend URL, the mode and the route prefix.
ConsentRoot's initRoute prop, and DEFAULT_INIT_ROUTEcreateConsentStateHandler({ routePrefix: '/api/c15t' }). There is no default route any more.
createConsentServerRoute, and its manifestGET, initGET and proxyHandlercreateConsentRoute(), which returns GET, plus the write methods with proxy
createConsentStateHandler({ backendURL, manifest })createConsentStateHandler(), which reads both from consentManifest(), or { snapshot }
manifestURL on the state handlermode: manifest({ manifestURL })
ConsentManifestOptions, resolveStrictestDefaultInitResolveConsentOptions; resolveUnknownLocationInit
Falling back to offline mode when no backend URL is setConsentRoot throws. Pass mode: offline() to run without a backend.
hosted, offline and manifest from c15t/tanstack-start as transportsThey are data for createConsentStateHandler({ mode }) now.

The renames shared by every framework:

RemovedUse instead
hosted({ url })hosted({ backendURL }), or the framework's backend URL variable
createManifestTransport({ manifest })createManifestTransport({ snapshot })
assertDecisionInputs defaulting to false on hosted()It now defaults to true whenever initURL is set. Pass false to turn it off.
hostedModes from c15t/runtime/providerreadHostedMode(mode)
buildManifest: true in Astro and NuxtonBuildError: 'fail', the default for production builds. buildManifest: false is manifest({ source: 'runtime' }).
A generated c15t-manifest.ts and its .gitignore entryimport { snapshot } from 'c15t/generated'. Most apps no longer import it at all.
The build options outputFile, exportName, importSource and rootDirRemoved. The snapshot is a virtual module, or a file under node_modules/.cache/c15t/ in Next.js.

Frame and its parts still work, with a one-time warning outside production. Use ConsentGate instead.

More help

Troubleshoot consent covers problems shared by every framework, such as a missing banner, analytics that load before a choice, imports that fail because npm installed c15t v2, choices that disappear on reload, server HTML that differs from the browser, static builds that fail and content blockers that hide the consent UI.