Skip to main content

Reference

Backend configuration

Configure the server

Use Inth when you want managed hosting. These options belong to a self-hosted @c15t/backend instance. Browser hosted() options only select how a client connects; they do not configure the backend's policy.

c15t-backend.config.ts
import { defineConfig, policyRulePresets } from '@c15t/backend';

const url = process.env.DATABASE_URL;
if (!url) throw new Error('Set DATABASE_URL');

export default defineConfig({
	database: { dialect: 'postgres', url, schema: 'c15t' },
	basePath: '/api/c15t',
	trustedOrigins: ['https://app.example.com'],
	manifest: { policyRules: [policyRulePresets.europeOptIn()] },
	ipAddress: { tracking: false },
});

The example uses the Europe preset for its matching locations and unknown-location fallback. Add reviewed rules for other locations in policy configuration.

Instance options

OptionPurpose and default
databaseRequired PostgreSQL, MySQL or SQLite connection, or an Effect SQL client layer.
basePathMount prefix to strip before routing. Set it when the handler receives paths such as /api/c15t/init.
trustedOriginsHosts whose browsers may call the backend cross-origin, such as app.example.com or *.example.com. Empty or absent allows none. GET /init answers every origin regardless; see query parameters and CORS.
tenantIdFixed tenant for this instance's database queries. Omitted uses the single-tenant scope with null tenant IDs. An empty or padded string, or a non-string such as null, throws when the instance is built. This is the only tenant setting; manifest.tenantId is no longer accepted and throws.
requireTenantIdSet to true when several tenants share one database. The instance throws at construction if tenantId is missing, instead of running in the single-tenant scope. Defaults to false.
hostingWho runs this backend: 'self-hosted' (the default) or 'inth', which only Inth's hosted platform sets. /init and /manifest report it, and browsers read it as window.c15t.hosting. It is not signed, so use it for debugging and support, not as proof of hosting. Any other value throws when the instance is built, and so does manifest.hosting.
manifestPolicy rules, translations, branding and IAB configuration published by /manifest and resolved by /init.
manifestCachePublic manifest cache durations in seconds. Defaults to sMaxAge: 300, staleWhileRevalidate: 86400.
apiKeysKeys accepted as Authorization: Bearer <key> for administrative endpoints. No keys means no request authenticates.
policySnapshotOptional policy decision signing key, issuer, audience and lifetime. Default lifetime is 1,800 seconds when signing is enabled. replayWindowSeconds (default 604,800, 7 days) is how long after a token expires a save made while it was valid is still accepted; 0 refuses every late save. See late saves.
identityTokensigningKey of at least 32 bytes, plus optional issuer (default c15t) and audience (default c15t-identity), for verifying identity tokens. Without it, only API-key requests verify links. Keep the key secret, one per tenant.
legalDocumentSnapshotLegal-document signing configuration type. It does not automatically issue or verify HTTP tokens. Use the explicit signing helpers; their default lifetime is 86,400 seconds.
gvlServer-side Global Vendor List loading and cache configuration. Writing the block is the opt-in: /init carries the list for every matched IAB policy. enabled: false declines.
ipAddressIP recording options. Masking is on unless disabled; tracking: false records no IP.
openapiEnabled by default at /spec.json. Supports enabled, specPath, title and the documented server basePath.
scriptScript-tag routes at /c15t.js, /c15t.headless.js and /c15t.iab.js. On by default; see browser script routes.
versionVersion reported by the status endpoint and OpenAPI document.
observabilityLogging configuration passed directly to c15tInstance. Defaults to warning and error requests.
sessionsonReport receives a report for each visitor resolution: framework servers send them to POST /sessions, and the backend's own /init adds one. Nothing is stored, and a failing sink never fails the request. Pass it to c15tInstance, not defineConfig.

defineConfig() checks types and returns the supplied object. It does not load environment variables or run migrations. It deliberately rejects observability; pass logging callbacks when constructing the instance instead.

Manifest options

OptionWhat it changes
policyRulesOrdered canonical policy rules. Author this field, not the generated manifest's policyPacks.
appNameApplication name included in configuration.
brandingBranding setting, defaulting to c15t.
customTranslations, i18nTranslation overrides and locale configuration.
iabenabled, cmpId, customVendors and the GVL endpoint.
vendors, vendorListVersionVendors offered for vendor-level consent outside IAB, returned from /init and merged with vendors declared in the client. The version label is shown to the visitor and surfaced to dev tools, never used to force re-consent; a saved vendor decision carries the grant map and its time, not the label.

Browser script routes

The backend serves /c15t.js, /c15t.headless.js and /c15t.iab.js by default, so a self-hosted instance can serve a configured script tag without a separate bundle deployment. Turn them off when you do not use the script tag:

const backend = c15tInstance({ ...config, script: { enabled: false } });

The /c15t.js route serves the hosted bundle with its backend URL already configured. It resolves each visitor's policy through /init. The headless and IAB routes inline the public manifest and resolve it in the browser. Keep secrets out of script.config.

@c15t/backend installs @c15t/browser as a runtime dependency, including its core, UI, IAB, and DevTools dependencies. Disabling the routes removes the HTTP endpoints; it does not remove those packages from the installation. Bundle files load when requested. A missing bundle returns 503.

With c15tInstance({ basePath: '/api/c15t', ... }), a script loaded from /api/c15t/c15t.js calls /api/c15t/init and /api/c15t/subjects. You do not need to repeat the mount path in script.backendURL.

Pass these options under script in the c15tInstance() or defineConfig() options:

OptionDefaultBehavior
enabledtrueRegister all three script routes. Set to false to remove them.
path/c15t.jsPath for the ordinary banner and preference UI.
headlessPath/c15t.headless.jsPath for the runtime without UI.
iabPath/c15t.iab.jsPath for the optional IAB CMP and preference UI.
backendURLDerived from the request origin and configured basePath, or the request path when basePath is omittedBackend base URL embedded in the bundle. Set it explicitly when a proxy rewrites the public path or origin.
config{}Browser defaults embedded in the response. Page-queued configuration overrides these defaults.
bundlesFiles resolved from @c15t/browserOverride filesystem paths by full, headless, or iab key. Requires a runtime with Node filesystem APIs.

The routes share the /manifest cache policy through manifestCache and return ETags for conditional requests. See the endpoint reference and script-tag guide for usage.

Origins, identity and secrets

A trustedOrigins entry matches a hostname on any web scheme. Add a port to pin one, such as localhost:3000. *.example.com matches subdomains, and * allows every origin. CORS controls which browser pages can read responses. It is not authentication and does not prevent a non-browser client from calling a public endpoint. GET /init is open to every origin without credentials, because it returns only public policy data for the request's location; consent saves and subject reads keep the allowlist. Keep API keys, database credentials and signing keys in server environment variables.

A tenantId scopes an instance, not an arbitrary incoming header. Construct the correct tenant instance in trusted application code. Do not choose a tenant from an unchecked browser value.

When several tenants share one database, set requireTenantId: true on every instance. Without it, an instance whose tenant lookup returned undefined starts in the single-tenant scope. It then writes consents with a null tenant, which the tenant that owns them never reads, and nothing reports the problem.

lib/c15t.ts
import { c15tInstance, type C15TInstance } from '@c15t/backend';

const url = process.env.DATABASE_URL;
if (!url) throw new Error('Set DATABASE_URL');

// One instance per tenant: each instance owns a connection pool.
const instances = new Map<string, C15TInstance>();

export function instanceFor(tenantId: string | undefined) {
	const key = tenantId ?? '';
	let instance = instances.get(key);
	if (!instance) {
		instance = c15tInstance({
			database: { dialect: 'postgres', url },
			tenantId,
			// Throws here if the tenant lookup returned undefined.
			requireTenantId: true,
		});
		instances.set(key, instance);
	}
	return instance;
}

Subject IDs are chosen by the browser and are unique across the whole database, not per tenant. When a save names a subject ID that another tenant already holds, the backend answers 409 SUBJECT_CONFLICT and writes nothing. The c15t client then generates a new subject ID for that visitor and sends the choice once more.

ipAddress.ipAddressHeaders overrides the ordered headers used to record an IP. Configure the reverse proxy to replace trusted forwarding headers. IP recording and policy location resolution are separate operations.

See HTTP endpoints, request logging and legal-document snapshots.