Skip to main content

Concepts

Data fetching

Compare the fetching paths

Where policy resolves and where choices are saved are separate from who runs the backend. Inth and a self-hosted backend speak the same protocol, so every path below works with either. Only offline() removes backend requests. For a recommendation by framework, start with choose your setup.

Fetching pathWhere policy resolvesWhere choices are submittedChoose it when
Build-time manifest, recommendedYour server or browser resolves a bundled policy snapshot with each visitor's inputsInth or your c15t backendYour framework supports a build integration and policy changes can ship with a rebuild
Cached manifest on your serverYour application resolves public policy data with each request's location and signalsInth or your c15t backendPolicy changes must reach the app without rebuilding, or builds cannot access the backend
Regular backend /initThe consent backendThe same backendYou want the fewest moving parts, need backend-owned request resolution, or have no application server
Manifest in the browserThe browser, using supplied or unknown locationInth or your c15t backendYou deliberately want client resolution and have planned geography, bundle size and policy refresh
OfflineThe browser or local runtime, using bundled rulesNo backend submissionLocal development and tests. Not recommended for production environments.
Custom transportYour implementationYour implementationAn existing service cannot use the c15t backend protocol

A manifest is a versioned policy document served by GET /manifest. It contains policy rules, translation configuration and related consent configuration. It is public configuration, not a visitor's saved choices. A resolver combines the manifest with country, region, language and privacy signals to produce an init result for one visitor.

Reusing the public document avoids asking the consent backend to resolve policy for every application request. Cache misses and revalidation still fetch the manifest, and consent writes still need the backend. Measure the deployed request path before promising a latency improvement.

Do not put secrets, visitor identifiers or consent records into a manifest. Keep personalized init responses out of shared caches. Changing a policy also requires a refresh strategy for cached or build-time manifests.

Use a build-time manifest by default

The build fetches your public policy once and bundles it, so the server never fetches it at runtime.

manifest() is the default mode in every server-rendered framework. Consent modes covers each framework's build integration and what happens when the download fails.

The framework quickstarts include the build integration for supported deployments. Server resolution uses the snapshot without an upstream manifest request. Browser resolution includes the policy and resolver in the client bundle, and may still request location for regional policies. Static Nuxt and Astro sites and script-tag sites keep the runtime setup from their guides.

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.

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

CommandDefault when the fetch fails
Production build: next build, vite build, nuxt build, astro buildThe build stops with an error.
Dev: next dev, vite dev, nuxt dev, astro devA 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:

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 lists every case.

How manifest mode counts visitors

The backend counts visitors from /init requests, and manifest resolution skips them. To keep the count, the server adapters send a session report after each resolution: the host posts to the backend's POST /sessions from the server, after the response. The browser sends nothing, and the report stores no identity; the visitor's IP address and user agent are forwarded under the backend's usual IP handling. Static output resolves in the browser and sends no report. Set reportSessions: false on an adapter to turn it off.

Each page load gets a journey id, a random UUID sent on requests c15t already makes:

RequestQuery parameters
GET /initjourney, journeyScope, stored (1 if a choice or notice dismissal was stored)
POST /subjectsjourney, journeyScope

Session reports carry it as journey: { id, scope, storedChoice, prompt, domain }. A report and a save with the same id belong to one page load. prompt is due, stored or not-required; a page that falls back to the default opt-in banner counts as due or stored. A stored answer counts as stored even if a policy change makes c15t ask again.

The id is random, is never the subject id and is never written to a cookie. Every save that carries one follows an /init or report with the same id. Saves replayed after going offline carry none.

journeyBehavior
'page' (default)One id per page load, in memory.
'tab'Kept in sessionStorage while a prompt is due, and removed once the visitor chooses or no prompt is due. Only when the browser resolves init; server-rendered pages use 'page'.
falseNo journey.

Set it on the runtime or provider. In Next.js use defineConsentConfig({ journey }); in TanStack Start pass the same value to resolveConsent and ConsentRoot; with the inline prefetch script, pass it to buildPrefetchScript. Vue, Svelte, Astro and the script tag use 'page'. A manifest that resolves locally in the browser reports no init, so it sends no journey.

If you pass a transport hosted() a fetch, match on the path: URLs now carry a query string.

What does regular /init do?

hosted() uses ${backendURL}/init for initialization and ${backendURL}/subjects for consent submissions. The name hosted describes the protocol; the URL can belong to Inth or your own c15t backend.

c15t.config.ts
import { defineConsentConfig, hosted } from 'c15t/next';

export default defineConsentConfig({ mode: hosted() });

Each framework passes the mode in a different place, and server-rendered frameworks take the data hosted() from their own package. Consent modes lists where each one goes.

A regular backend /init request lets the backend resolve the visitor context. A same-origin URL named /api/c15t/init can instead resolve from a cached manifest in your application. The URL name alone does not tell you which path runs.

How do transports and proxies differ?

A transport implements initialization, saving and optional record operations. A proxy changes where HTTP requests travel. It does not change the policy resolver or make personalized responses safe to cache.

For a same-origin init route that resolves a manifest, the hosted transport can separate policy reads and record writes. With a backend rewrite mounted at /api/c15t, a single-page app can use the transport hosted() from c15t:

src/consent-mode.ts
import { hosted } from 'c15t';

export const mode = hosted({
	backendURL: '/api/c15t',
	initURL: '/api/c15t/init',
});

The route must return the c15t init response contract. c15t adds its inputs to initURL as query parameters, replacing any of your own with the same name. See query parameters and CORS for the names. Because initURL is set, assertDecisionInputs defaults to true: saves carry the policy decision they were made against when init returned no signed policy snapshot token, so the backend can reject a save made against a stale policy. Server-rendered frameworks do this for you when you set routePrefix: the browser re-inits through ${routePrefix}/init. Every framework rejects routePrefix: '/' when it sets up, because a catch-all consent route at the site root would catch every page.

The init route resolves policy; the backend rewrite forwards /api/c15t/subjects and other record endpoints. Configure both if you choose this optional proxy variant. Server-rendered frameworks do both with one option: proxy: true in Next.js c15t.config.ts or TanStack Start's createConsentStateHandler, with a consent route created with proxy: true. The Next.js recipe shows the configuration. A direct absolute backend URL works without a rewrite.

This optional optimization keeps c15t requests on the app's origin and avoids a separate browser DNS lookup and TLS connection to the consent backend. The app server still connects to the upstream backend for manifest refreshes and consent writes. Vendor scripts and vendor requests keep their own origins.

The rewrite destination and the init route use the absolute upstream endpoint, such as https://your-project.inth.app. The browser uses /api/c15t without needing the upstream URL. A static export cannot serve a Next.js route or rewrite at runtime; use the absolute Inth URL or a proxy provided by the static host instead.

When should I use offline mode?

Not recommended for production environments. Use Inth or a self-hosted backend for production policy and consent records.

offline() resolves bundled policy rules without an init request and acknowledges saves locally. The runtime's persistence module stores the choice in browser storage. There is no backend audit history, cross-device record service or IP geolocation supplied by this transport.

Use your framework's offline() so its translations and provider context are included. In a React app it replaces the mode passed to ConsentProvider:

src/consent-mode.ts
import { offline } from 'c15t/react';

export const mode = offline();

In Next.js, TanStack Start, Nuxt, Astro and SvelteKit, pass the data offline() from the framework package where the config takes mode; see consent modes.

With no policyRules, the current offline transport uses the recommended rule pack. Supplying policyRules replaces that pack. Unknown country and region are real resolution inputs; offline mode does not discover a visitor's location. Use policy rules to understand matching and defaults, and test the missing-location case.

Offline mode is an explicit architecture choice, not an automatic fallback for a failed Inth request. If hosted initialization fails before a policy resolves, optional permissions remain denied and the stock prompt stays hidden. Observe initialization failures instead of silently changing policy sources.

Can I provide my own transport?

custom(transport) accepts a KernelTransport with the v3 init and save contract. It does not accept v2 endpoint handlers such as setConsent. Keep policy resolution, record acknowledgments and failure behavior consistent with the kernel contract. Prefer a built-in transport when your backend supports it.

Verify the selected path

Inspect browser and server requests separately. A server manifest fetch will not appear in the browser's Network panel. With a build-time snapshot, start a fresh production server and confirm it makes no upstream /manifest request. After a policy edit, rebuild and confirm the new policy appears. On a runtime manifest path, check that page requests do not call the backend /init, a visitor's choice still reaches the backend's /subjects, and policy changes become visible after the configured refresh. With a same-origin consent proxy, the browser should call only /api/c15t paths for consent HTTP traffic; inspect server logs to verify their upstream destinations.

Test different locations, missing location headers, GPC, returning choices and backend failure. See verification.