Skip to main content

Next.js Advanced

Data fetching reference

Config reference

defineConsentConfig from c15t/next declares the app's consent setup in c15t.config.ts at the project root. withConsentManifest in next.config.ts finds the file and hands it to ConsentRoot, resolveConsent, withConsentProps, createConsentRoute and createPagesConsentRoute, so the app never imports it. For help choosing a setup, start with rendering and deployment. Requires Next.js 15 or 16 (next ^15.0.0 || ^16.0.0).

PropertyDefaultBehavior
backendURLNEXT_PUBLIC_C15T_BACKEND_URL, then NEXT_PUBLIC_INTH_PROJECT_URLBackend base URL. Consent submissions go to /subjects under it, and the server reads /manifest or /init from it.
modemanifest()manifest(), hosted() or offline() from c15t/next. See consent modes.
routePrefixnoneWhere the catch-all consent route is mounted, such as /api/c15t. A browser that resolves consent itself asks ${routePrefix}/init instead of the backend's /init. Leave it unset when every page resolves consent on the server.
proxyfalseThe route at routePrefix forwards saves (createConsentRoute({ proxy: true })), so the browser saves there instead of ${backendURL}/subjects. The server helpers keep backendURL. Needs routePrefix. See optimization.
journey'page'The consent journey scope, read by both resolveConsent and ConsentRoot so they agree.
scripts, vendors, clearOnRevocation, networkBlocker, persistence, scriptLoader, optionsnoneBrowser options. ConsentRoot props of the same name win over them.

The file is bundled into the browser as well as the server, so it can hold functions such as scripts but must hold no secrets. Read server-only values, such as a token for forwardHeaders, in server code.

Values can be absolute HTTP or HTTPS URLs or paths beginning with /. Server helpers resolve relative paths against the incoming request's host. If your server answers requests for any Host header, such as a Node server exposed directly or a proxy that forwards Host from its default virtual host, use an absolute backendURL. A relative one resolves against whatever Host the request sent, so the request would choose the origin the server fetches.

defineConsentConfig returns a frozen object. On the server, which includes withConsentManifest reading the file at build time, it validates first and throws a TypeError with one of these messages. Browser bundles skip the checks, so they add no bytes there:

MessageCause
@c15t/nextjs: defineConsentConfig expects an object.The argument is not an object.
@c15t/nextjs: defineConsentConfig needs backendURL, or NEXT_PUBLIC_C15T_BACKEND_URL (or NEXT_PUBLIC_INTH_PROJECT_URL) set at build time.No backend URL, and the mode is not offline() or a hosted() with its own backendURL.
@c15t/nextjs: defineConsentConfig backendURL must be an absolute http(s) URL or a /-relative path, received "...".A URL is protocol-relative, such as //your-project.inth.app, or bare, such as api/c15t. The same check covers routePrefix, mode.manifestURL, mode.geoURL and mode.backendURL.
@c15t/nextjs: routePrefix can't be '/': a consent route at the site root would catch every page. Use a path such as '/api/c15t'.routePrefix is '/'. A catch-all route at the site root would catch every page.
@c15t/nextjs: defineConsentConfig mode must be manifest(), hosted() or offline() from c15t/next. Pass a custom transport through ConsentRoot options.mode.mode is not one of the data factories.
@c15t/nextjs: defineConsentConfig journey must be 'page', 'tab' or false.journey has another value.
@c15t/nextjs: proxy sends saves through the consent route, so it needs routePrefix.proxy: true without routePrefix.

Protocol-relative and bare URLs are rejected because the server helpers would resolve them against the request host and reach an unintended origin.

Not when every page resolves consent on the server. The root layout or withConsentProps supplies the initial policy directly to the browser.

Add the route when some pages render without server state: static, ISR or 'use cache' pages in the App Router, or Pages Router pages without withConsentProps. Set routePrefix: '/api/c15t' in c15t.config.ts and mount the route:

app/api/c15t/[...c15t]/route.ts
import { createConsentRoute } from 'c15t/next/api';

export const { GET } = createConsentRoute();

For Pages Router, use this file instead:

pages/api/c15t/[...c15t].ts
import { createPagesConsentRoute } from 'c15t/next/pages';

export default createPagesConsentRoute();

The route answers GET ${routePrefix}/init by resolving the bundled manifest with the browser request's location headers, so those pages get the same policy a server render would. It also answers GET ${routePrefix}/manifest for manifest({ resolve: 'browser' }).

Without routePrefix, browser initialization calls the backend's /init. See geography and privacy signals.

Mode combinations

ConfigServer renderBrowser initialization
manifest(), the default, with routePrefixResolves the snapshot with request inputsCalls ${routePrefix}/init, whose route resolves the snapshot with request inputs
manifest() without routePrefixResolves the snapshot with request inputsCalls ${backendURL}/init
manifest({ resolve: 'browser' })Resolves the snapshot with request inputsLoads the resolver lazily and fetches ${routePrefix}/manifest, else ${backendURL}/manifest
manifest({ source: 'runtime' })Fetches and caches ${backendURL}/manifestAs manifest()
hosted()Calls ${backendURL}/initCalls ${backendURL}/init
offline()No policy requestResolves bundled local rules

Hosted and manifest modes submit choices to ${backendURL}/subjects. Offline mode uses browser persistence only. A prepared server result satisfies the first browser initialization; the browser column describes what happens when initialization is needed, including recovery after the server render failed.

The browser loads only the code of the mode it runs. With manifest() resolved on the server, the snapshot, the resolver and other languages stay out of the browser bundle; see what each mode adds to first-load JavaScript.

Switch to regular backend init

Set mode: hosted() in c15t.config.ts. The client-side guide has the complete config. For server rendering, keep the layout from your router guide. resolveConsent will call backend /init per request.

Remove the consent route if no page needs it, unless proxy: true sends browser saves through it. See optimization.

Server helper options

resolveConsent is the one server helper. c15t/next/server exports it for the App Router, where the default request context reads next/headers and calls await connection() from next/server before reading the clock. c15t/next/pages exports the same function for the Pages Router, taking the Node req instead, and withConsentProps wraps it as a getServerSideProps. All of them accept the options in this section; the Pages Router entry replaces only the request adapter.

resolveConsent() needs no options: it reads c15t.config.ts and the snapshot withConsentManifest downloaded. What it does depends on the mode. manifest() resolves policy from the snapshot, or from ${backendURL}/manifest through the in-process cache without one. hosted() calls ${backendURL}/init. offline(), or no backend URL at all, makes no network request and returns cookie- and header-only state; see cookie-only state.

resolveConsent options

OptionDefaultBehavior
configc15t.config.tsThe config to use instead of the file. Explicit options on this bag win over its fields. In manifest mode, its routePrefix names the same-origin route the browser loads a deferred IAB Global Vendor List from.
backendURLthe mode's, then the config's, then NEXT_PUBLIC_C15T_BACKEND_URL or NEXT_PUBLIC_INTH_PROJECT_URLBackend base URL. With hosted(), resolveConsent calls ${backendURL}/init.
manifestURLthe mode's absolute manifestURL, then ${backendURL}/manifestAbsolute GET /manifest URL. Setting it resolves from the manifest whatever the mode, through the in-process manifest cache. A /-relative manifestURL on the mode is the browser's, so the server ignores it.
snapshotthe snapshot withConsentManifest downloadedA manifest to resolve from instead. Setting it resolves from the manifest whatever the mode.
fetchglobalThis.fetchFetch implementation for the backend /init or manifest request. Manifest requests already pass next: { revalidate: 300 } for the App Router Data Cache. The /init call can carry the consent cookie and returns per-visitor state, so it uses cache: 'no-store'. A custom fetch must preserve these options. It also keeps the IAB Global Vendor List inline, since the browser cannot replay it.
forwardHeaders[]Extra request header names copied onto the outgoing call, such as a token a private backend needs. They travel only over https or to a loopback host. cookie and forwarded/x-forwarded-* cannot be named here. Headers absent from the request are skipped.
trustForwardedHeadersfalseResolve a /-relative backendURL or manifestURL against the request's forwarded, x-forwarded-host and x-forwarded-proto headers instead of host, and forward the visitor IP to backend /init as x-forwarded-for. Set it only behind a proxy that sets those headers and drops the ones a client sends.
onErrornoneReceives the failure from the backend or manifest request, including a ManifestUnavailableError when timeoutMs runs out or the manifest is backing off after a failure. When omitted, failures are logged with console.warn only when NODE_ENV is not production.
timeoutMs500Longest the render waits for policy, in milliseconds, counted from the start of the resolution. When it runs out, the helper returns the baseline state described below. false or Infinity waits for the manifest cache's 5 second request timeout, or for the fetch implementation on backend /init. Any other value that is not a finite, non-negative number uses the default.
reportSessionstrueIn manifest mode, report the resolved init to the backend's POST /sessions after the render, server-to-server, so the backend still counts the visitor. Forwards the visitor's user agent and client IP on x-c15t-client-ip, never cookies. false sends none.
journeythe config's journeyThe consent journey scope this render reports. ConsentRoot must use the same value.
experimentnoneThe banner experiment with the arm this request runs. See banner experiments.
waitUntilnoneReceives work that outlives the render so it survives the response: the session report, a manifest refresh, and a manifest request that timeoutMs stopped waiting for. In the App Router pass (task) => after(() => task) with after from next/server. The promise never rejects.
nowDate.now()Clock used to validate stored records and stamped into the result.
cookieNamec15tCookie holding persisted consent. Must match the client storageConfig.storageKey.
countryheader detectionOverrides the country read from request headers.
languageheader detectionOverrides the language read from accept-language.
requestnext/headersRequest context adapter with cookies() and headers(). The default only works in the App Router.

resolveConsent throws when the request adapter's headers() or cookies() rejects (the default adapter rejects outside a request scope), and when your onError callback throws. Thrown URL resolution, network, timeout and policy resolution errors are handled: they return the same baseline state as the cookie-only call, with stored records, geography, language and GPC from the request but no resolved policy. The page still renders and the browser initializes consent on mount. Non-2xx responses from backend /init are failures too, so a 500 renders the baseline rather than throwing. A successful response whose body reports policyResolution.status: 'failed', for example an unsupported policy contract, is different: that failed resolution is kept as the prepared state, the browser does not re-initialize on mount, and the consent UI stays hidden until the cause is fixed.

Slow or unavailable backends

resolveConsent waits at most timeoutMs (500 ms by default) for policy. A warm manifest cache answers in about a millisecond, and a cold read over a new connection to a hosted backend usually takes a few hundred. When the budget runs out, the render uses the baseline state: no consent UI in the server HTML, optional categories denied, and consent-gated scripts and iframes blocked. ConsentRoot then initializes in the browser and shows the banner once the backend answers, retrying failed attempts with backoff.

The manifest request does not stop when the render gives up. It keeps running, up to the cache's 5 second request timeout, and stores the manifest for later renders. Pass waitUntil so serverless platforms keep it alive after the response.

Server manifest reads, in resolveConsent and in the route handlers, share these rules:

  • Concurrent requests for the same manifest URL share one upstream request.
  • A render that joins a request already in flight waits only for what is left of that request's timeoutMs.
  • After a failed request, with no usable copy cached, that URL is not asked again for 1 second. Each further failure doubles the wait, up to 5 seconds. Requests in between fail at once with a ManifestUnavailableError whose reason is 'backoff', so a struggling backend sees at most one request per URL per server instance in each interval.
  • A stale copy is served only inside the backend's stale-while-revalidate window, while one background request refreshes it. Past that window the read waits for the backend like a miss and never falls back to the expired copy.

In the App Router, both resolveConsent and createConsentRoute pass next: { revalidate: 300 } to upstream manifest fetches. Next.js's Data Cache can satisfy an in-process cache miss, including on a new server instance when your host provides a shared cache. A manifestURL pointing directly at the backend uses these same cache layers. The consent route also provides a cacheable response for browsers and your CDN.

Pages Router helpers keep the in-process cache, but ordinary Pages Router fetches do not use the App Router Data Cache. A build-time snapshot avoids manifest cache misses in both routers and stays fixed until rebuilding.

Absolute http(s) URLs are used as given. resolveConsent resolves a /-relative backendURL or manifestURL against the request's host header. A domain name resolves over https. localhost, an IP address or a single-label host such as app:3000 resolves over http. The x-forwarded-host, x-forwarded-proto, forwarded and referer headers are ignored, because any client can send them and the /init call carries the visitor's consent cookie to the resolved host. When no host header is available, resolveConsent reports the error and returns the baseline state.

A render never fetches your own consent route: a URL on the request's origin under the config's routePrefix. resolveConsent reports the URL and returns the baseline state instead. Keep the config's backendURL absolute, and use proxy: true to keep the browser on your origin.

Any other same-origin backendURL is a backend reached through your app, such as /api/c15t with a rewrite or a mounted @c15t/backend. resolveConsent calls its /init like any backend. That request goes back through your own server, which costs a second function invocation on serverless hosts and fails behind deployment protection; the render then returns the baseline state and the browser resolves the policy. Pass the backend's own URL to skip the hop. If the prefix reaches no backend and a page answers instead, that page's resolveConsent sees the request came from a render and does not fetch again.

Behind a proxy that sets x-forwarded-host and x-forwarded-proto and drops the values a client sends, pass trustForwardedHeaders: true to resolve against them instead. Use https for any production backendURL. Over plain HTTP to anything but a loopback host, the /init call carries no cookie, no forwardHeaders and no client IP.

Forwarded headers differ by path, and only what the backend needs travels:

  • The backend /init call carries the resolved x-c15t-country, x-c15t-region, accept-language and sec-gpc, the user-agent, and the experiment arm while the visitor has no stored choice. Over https or to a loopback host it also carries the consent cookie (cookieName, never the rest of the cookie jar), any forwardHeaders, and with trustForwardedHeaders the visitor IP as x-forwarded-for. It is sent with cache: 'no-store'.
  • The manifest request carries only the headers named in forwardHeaders, because the manifest is public policy data.

Cookies are still read locally on both paths to restore records.

An inline snapshot, including the one withConsentManifest downloaded, is never refreshed. The manifest transport returns the object as given instead of fetching it, so the snapshot is the source of truth for that request rather than a cache seed. A stale snapshot resolves stale policy until the application ships a new one. One request can still happen: when the inline manifest has iab.enabled: true with an iab.gvl reference and the matched rule uses the iab model, the transport fetches the Global Vendor List with the fetch option, and a blocked network there also falls back to the baseline. backendURL is still required because choices post to ${backendURL}/subjects.

onError replaces the default logging entirely. Without it, production deployments render the baseline silently; pass onError to report failures to your monitoring. cookieName must match the client storageKey for stored choices to be restored at all: with a mismatch the server supplies empty records, the provider treats them as prepared and skips browser hydration, so the visitor's existing choice stays ignored for the whole mount, not only at first paint.

With mode: offline(), or when no backend URL is configured anywhere, resolveConsent returns state that needs the visitor's stored records but no resolved policy. It makes no network request. It reads the request and returns a JSON-serializable ConsentState with initialRecords, initialPrivacySignals.gpc, now and, when any value was detected, initialOverrides with country, region and language. It does not set cookies and does not cache across requests. Only these options apply:

OptionDefaultBehavior
nowDate.now()Clock used to validate stored records and stamped into the result.
cookieNamec15tCookie holding persisted consent. Must match the client storageConfig.storageKey.
countryheader detectionOverrides the country read from request headers.
languageheader detectionOverrides the language read from accept-language.
requestnext/headersRequest context adapter with cookies() and headers(). The default only works in the App Router.

The cookie header is read from headers().get('cookie') first and from request.cookies() only when that header is absent. Country and region come from the headers listed in geography and privacy signals.

Types

TypeExported fromWhat it names
ConsentStatec15t/next, c15t/next/server, c15t/next/pagesThe value resolveConsent returns and ConsentRoot takes as state
ResolveConsentOptionsc15t/next/serverThe App Router options bag
ConsentRequestOptionsc15t/next/serverThe request-reading subset: now, cookieName, country, language, request
PagesResolveConsentOptionsc15t/next/pagesResolveConsentOptions with req in place of request
ConsentRootPropsc15t/nextProps of ConsentRoot; ConsentRootProps['state'] also accepts the pending promise
ConsentConfigc15t/next, c15t/next/server, c15t/next/pages, c15t/next/apiThe frozen defineConsentConfig result
ConsentModec15t/nextWhat manifest(), hosted() and offline() return
NextConsentRouteOptionsc15t/next/apiOptions of createConsentRoute and createPagesConsentRoute
ConsentPagePropsc15t/next/pagespageProps of a page that exports withConsentProps, for AppProps<ConsentPageProps>
WithConsentPropsOptionsc15t/next/pagesPagesResolveConsentOptions without req

Pages Router differences

withConsentProps(getServerSideProps?, options?) from c15t/next/pages is a getServerSideProps that resolves consent and adds it to the page's props as consent. It wraps the page's own getServerSideProps when given; consent resolves while it runs, and its redirect and notFound results pass through. options takes every option in the App Router table except request. The state is round-tripped through JSON, because Next.js rejects undefined prop values such as an absent GPC signal.

c15t/next/pages also exports resolveConsent with the Node request in place of the request adapter. resolveConsent({ req, ...options }) accepts every option in the App Router table except request; req is the request from getServerSideProps or an API route. Its result can hold undefined fields, so round-trip it through JSON.parse(JSON.stringify(result)) before returning it as a prop, or use withConsentProps, which does that for you.

createPagesRequestContext(req) builds the request adapter itself. Headers are converted to Web Headers, and cookies are read from the cookie header. Use it when calling the c15t/next/server helper from a custom server or test harness where next/headers is unavailable:

server/consent.ts
import type { IncomingMessage } from 'node:http';

import { createPagesRequestContext } from 'c15t/next/pages';
import { resolveConsent } from 'c15t/next/server';

export function resolveRequestConsent(req: IncomingMessage) {
	return resolveConsent({ request: createPagesRequestContext(req) });
}

Route handler options

createConsentRoute options

createConsentRoute(options?) from c15t/next/api returns GET for one catch-all route, such as app/api/c15t/[...c15t]/route.ts. GET answers /init and /manifest under the route; every other path answers 404. With proxy, it also returns POST, PATCH, PUT, DELETE and OPTIONS, and forwards the other consent paths to the backend. Everything defaults to c15t.config.ts and the snapshot withConsentManifest downloaded.

OptionDefaultBehavior
configc15t.config.tsThe config to use instead of the file.
backendURLthe config's, then NEXT_PUBLIC_C15T_BACKEND_URL or NEXT_PUBLIC_INTH_PROJECT_URLBackend base URL. Without a snapshot, the route fetches ${backendURL}/manifest.
manifestURLthe mode's absolute manifestURLFull upstream manifest URL. Takes precedence over backendURL plus /manifest.
snapshotthe snapshot withConsentManifest downloadedTakes precedence over upstream URLs, serves the manifest and resolves each visitor without fetching policy. Stays fixed until rebuilding.
proxyfalseForward subjects, subjects/:id, health and status to the backend, so browser saves stay on your origin. Pair it with proxy: true in c15t.config.ts. createPagesConsentRoute takes it too.
manifestRevalidateSeconds300Next.js Data Cache revalidation for the manifest fetch. false disables it.
fetchglobalThis.fetchFetch implementation for the manifest and Global Vendor List requests.
trustForwardedHeadersfalseResolve a /-relative backendURL or manifestURL against the request's forwarded, x-forwarded-host and x-forwarded-proto headers instead of request.url. Set it only behind a proxy that sets those headers and drops the ones a client sends.
onBackgroundRevalidatenoneReceives detached work started by a request: a background manifest refresh, and the init route's session report. Keep it alive with after from next/server or a platform waitUntil. Called inside the handler; the promises never reject. See Optimization.
reportSessionstrueReport each init the route resolves to the backend's POST /sessions, server-to-server and detached from the response, so the backend still counts the visitor. Needs an absolute backendURL; nothing is inferred from a manifest URL. false sends none.
fetchGvlbuilt-in cached fetcherLoads the Global Vendor List for IAB policies. Called only under the conditions described in this section.

With no snapshot, manifestURL wins over backendURL. URLs are resolved per request, so a missing or invalid value fails the request rather than the build: with no snapshot and neither backendURL nor manifestURL, the handler throws. A /-relative value is resolved against the origin of request.url, which Next.js builds itself. Forwarding headers are read only with trustForwardedHeaders: true. A relative value still points the handler at your own app, so keep upstream URLs absolute; otherwise the consent route fetches itself.

A /-relative manifestURL on the config's mode names the route itself, so the handler ignores it. Keep the config's backendURL absolute. To keep the browser on your origin, set proxy: true rather than a relative backendURL.

The handlers are built on the consent route handler in @c15t/core/server, which the TanStack Start, SvelteKit, Astro and Nuxt adapters share, so the rules below are the same in every framework.

GET ${routePrefix}/init resolves the build snapshot or runtime cached manifest with the request's geography, language and GPC headers and responds with cache-control: private, no-store and x-c15t-policy-contract: 1. The payload echoes the inputs it used as resolvedOverrides and resolvedPrivacySignals. The browser sends its overrides and policy contract as the country, region, gpc and contract query parameters, which win over the matching x-c15t-* headers. When the request declares a different policy contract in either form, the response keeps the translations and UI data but sets policyResolution to status: 'failed' with reason: 'unsupported-contract'. Whenever the resolution is not matched, the payload carries no policySnapshotToken, gvl, gvlReference, cmpId or customVendors. Requests that declare no contract are treated as compatible.

With a snapshot, GET ${routePrefix}/manifest returns the complete snapshot as JSON with status 200. It makes no upstream request, adds no cache or validator headers and does not answer If-None-Match with 304 or slice by language.

With runtime fetching, GET ${routePrefix}/manifest forwards the upstream cache-control, etag, last-modified and content-language headers, adds content-type: application/json and an age computed from the in-process cache, and answers a matching If-None-Match with 304. It never invents a cache-control header the upstream did not send. The only query parameter passed upstream is a language that looks like a language tag, lower-cased; every other parameter is dropped, so a visitor's query string neither reaches the backend nor adds cache entries.

With runtime fetching, when the manifest cannot be read and backendURL is set, GET ${routePrefix}/init asks the backend's own GET /init instead, forwarding only the geography, language and GPC headers. This covers backends without /manifest. It happens for a 404 and for the first failure of a failing backend, not for every request while the manifest cache backs off. Otherwise the handler rejects and Next.js answers 500.

fetchGvl runs inside the init path only when the manifest has iab.enabled: true, the manifest includes an iab.gvl reference, and the resolved policy matched with model: 'iab'. It receives the reference, the fetch option, and the language taken from the first segment of the resolved translations language (en when empty). The default fetcher caches the vendor list in process and aborts the upstream request after five seconds. A fetched list becomes a gvlReference and a small banner summary in the serialized payload. A null result keeps IAB unavailable. A rejected or timed-out fetch fails the request rather than answering gvl: null, because the browser reads null as "IAB is off" and would show a policy that requires the TCF without it. With routePrefix, the browser reads the list from that same-origin route; otherwise it uses the manifest's public list URL. The banner shows the same purpose names and vendor count on the server and during hydration. A client-only vendors allowlist on the IAB config makes that server summary unusable, so the banner waits for the full list. Filter vendors on the server when the banner must render immediately.

createPagesConsentRoute(options?, param?) from c15t/next/pages accepts the same options except proxy and returns the default export of a catch-all pages/api route, pages/api/c15t/[...c15t].ts. param names the catch-all parameter and defaults to c15t; a file with another name throws @c15t/nextjs: createPagesConsentRoute found no `c15t` catch-all parameter. Name the file [...c15t].ts or pass its parameter name. Because a pages/api default export receives every method, requests other than GET and HEAD are answered with 405 and an allow: GET header before the wrapped handler runs. The bridge rebuilds the request URL from the host header, over http for localhost, IP and single-label hosts and https otherwise, and reads forwarding headers only with trustForwardedHeaders: true.

Manifest request resolution

The App Router and Pages Router manifest setups use this flow:

  1. next build bundles the backend's /manifest response as a snapshot. With manifest({ source: 'runtime' }) instead, the server fetches the absolute backend's /manifest endpoint and caches the public policy data.
  2. resolveConsent() resolves policy from that snapshot or cache with the current request's inputs and restores valid consent cookies.
  3. ConsentRoot receives that result as state and reads the rest from c15t.config.ts. Hydration preserves the resolved state.
  4. If browser initialization is needed, it asks ${routePrefix}/init, whose route resolves the same snapshot on the server, or ${backendURL}/init without routePrefix. manifest({ resolve: 'browser' }) resolves in the browser instead.
  5. Browser choices post to ${backendURL}/subjects.

A warm policy cache avoids backend /init during request resolution. The app may still read its consent route, cold caches fetch upstream data, and choices still reach the backend. IAB policies can also require a Global Vendor List fetch. See manifest caching for cache settings, and rendering and deployment for awaiting or streaming prefetch results.

Static manifest helpers

c15t/next/static exports loadStaticManifest, createStaticManifestModule, createStaticConsentResolver and resolveUnknownLocationInit for resolving consent from a manifest bundled at build time, for example in an output: 'export' site. The two resolvers come from c15t/static, which c15t/tanstack-start/static re-exports too.

  • createStaticManifestModule({ manifestURL }) fetches a manifest and returns TypeScript source for a module you write to disk and import.
  • loadStaticManifest fetches and returns the manifest object.
  • createStaticConsentResolver({ manifest, geo, geoURL }) returns a synchronous initial result and a resolved promise. Without geo, or when a geoURL returns no location, it uses the manifest's unknown-location policy: its fallback rule, else its default rule. That is the same policy the server helpers apply when a request has no location headers. Rules scoped to a country or region never apply to an unknown location.
  • resolveUnknownLocationInit(manifest, { language, gpc }) returns that unknown-location result on its own.

A manifest with neither a fallback rule nor a default rule cannot resolve an unknown location. The resolvers then return a failed result with the reason insufficient-inputs, and the client applies its safe fallback. Give your policy a fallback rule before you bundle it.

A generated manifest contains public policy, not a visitor's choice. The helpers do not mount a provider, persist choices or send consent to the backend. You wire them into a custom transport passed as options.mode, and publishing new policy needs a rebuild. The static export guide uses backend /init instead, which needs none of this. Never bake a build machine's location or cookies into a shared static page.

A static export can also resolve a manifest in the browser without these helpers. Set mode: manifest({ resolve: 'browser', manifestURL }) in c15t.config.ts, with an absolute manifestURL the browser can fetch from your site's origin. Browser manifest resolution has no location input, so every visitor gets your unknown-location rule.

Geography and privacy signals

Server manifest resolution reads location headers from the hosting platform. It recognizes country headers such as cf-ipcountry and x-vercel-ip-country, and region headers such as cf-region-code and x-vercel-ip-country-region. The application overrides x-c15t-country and x-c15t-region take precedence. Only trusted infrastructure should supply location overrides in production.

Request helpers also read language and GPC. Missing location stays unknown; the resolver does not infer country from the Next.js server's IP. Test unknown country and region against your configured policy rules.

Browser manifest resolution uses location overrides from prefetch, inputs or geoURL. Without them, a location-based policy asks the backend's /init. The consent route reads geographic headers on the server and keeps resolver code and translations out of browser initialization.

Offline configuration

Not recommended for production environments. Use offline mode for local development, tests or demos that do not need backend records.

Set mode: offline() in c15t.config.ts. It needs no backend URL:

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

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

Keep your layout. resolveConsent() then makes no network request and the browser resolves the rules after hydration. offline() from c15t/next is data, so the offline rules load with import() only in apps that use them.

The default local policy pack handles missing geography; offline mode does not perform IP lookup. Supply policyRules to replace that pack when your local policy needs different behavior. Browser persistence stores choices, but this setup has no backend record service. See transport choices for the tradeoffs.

Offline mode runs only when you choose it. ConsentRoot throws when manifest() or hosted() finds no backend URL: not in the mode, the config or NEXT_PUBLIC_C15T_BACKEND_URL. That happens only without a c15t.config.ts, because defineConsentConfig throws without a backend URL. See troubleshooting.

Authenticated hosted vendor lists

When hosted resolveConsent() forwards the consent cookie or additional request headers, or uses a custom fetch, it retains the fetched vendor list in server state. The browser cannot replay a private server fetch. This fallback preserves consent loading and vendor filtering without copying credentials into the page. Its payload size is unchanged from inline GVL loading. For compact pages with private upstreams, expose the public list through the consent route with routePrefix.