JavaScript
Quickstart
Connect Inth
Create an Inth project, set its policy rules, add your app's origin to its trusted origins, and copy the project's backend URL.
This guide uses @c15t/browser as an ES module in a Vite app, with your
project's policy bundled at build time. init() starts the consent runtime
and mounts the stock banner and preference dialog. For your own UI, use the
headless runtime instead. Sites
without a build step load the same package from a script tag;
choose your setup points to that guide.
Bundling the policy at build time is the recommended production setup. Rebuild after changing policies, translations or vendors.
Install
@c15t/browser is not part of the c15t package. c15t supplies the Vite
plugin at c15t/build, and @c15t/integrations the PostHog helper used
below.
Bundle your policy
Add consentManifest to vite.config.ts:
The plugin reads VITE_C15T_BACKEND_URL. Set it to your project's backend URL,
including any path prefix, in .env or wherever you build:
If your app already sets VITE_INTH_PROJECT_URL for other Inth SDKs, that works
too. When both are set, VITE_C15T_BACKEND_URL wins.
consentManifest downloads your project's public policy from /manifest
during vite build, or in vite dev when the app first loads it, and serves
it to your app as snapshot
from c15t/generated. No file is written into your project.
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:
| 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:
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.
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.
Start c15t
Call init() once, in your browser entry point:
manifest() resolves the policy from the bundled snapshot in the browser,
with no /manifest request. It reads the snapshot and the backend URL from
what consentManifest downloaded. Consent choices go to that backend, and so
does an /init request when the policy depends on the visitor's country or
region and overrides does not supply it. That request returns the backend's
current policy, which can be newer than the snapshot. Vite reads
VITE_C15T_BACKEND_URL at build time, so set it before you build, not when
you serve the files.
To apply policy edits without rebuilding, pass hosted() from
@c15t/browser as the mode instead. The browser then requests the policy from
/init on every page load. Your bundle keeps only the mode you import.
PostHog waits for measurement permission with loadMode: 'after-consent', and
cookieless_mode: 'never' turns off its cookieless capture. Replace
phc_your_project_key with your PostHog project key. The PostHog helper sends
events to PostHog's EU cloud; for a US project, add region: 'us'.
Include the measurement category in your Inth policy. Remove any other loader
for PostHog, such as a snippet in index.html, so it loads once and only
through c15t. Add other vendors to the same list, each with the category it
needs; the integration guides give the helper
for each one.
init() returns the client. It reads stored choices, resolves the visitor's
policy, shows the banner when the policy needs one, and loads each
script once its category is allowed. The banner renders in a shadow root with
its own styles, so you do not import a stylesheet. The client lasts for the
page; call consent.dispose() only if your app removes consent entirely.
When a visitor withdraws a permission they had given, the client saves the choice and reloads the page, because code that already ran cannot be unloaded. Scripts explains how to change that.
Gate embeds and add a privacy settings button
Put the embed's URL in data-src and name its category in data-category.
The client sets src only while that category is allowed:
Visitors need a way to change their choice after the banner closes. The
client opens the preference dialog from any link to #c15t-preferences:
A button with data-c15t-action="customize" opens it too, without code.
Change the look
Pass theme tokens in ui.theme, next to the options your init() call
already has. This example turns the buttons purple and rounds the cards:
theme takes the same tokens as every c15t package; see
theme tokens. presentation.prompt changes the
banner's shape and position, ui.css adds CSS inside the shadow root, and
i18n changes the copy. Customize
covers each option.
Check it works
Run the app, open it in a private window, and open the browser's developer tools on the Network tab.
- Before you choose, filter for
posthog. There are no requests, and the YouTube iframe has nosrc. - Click Reject All and reload. The banner stays closed and the PostHog requests stay absent.
- Click Privacy settings, turn on Analytics (the
measurementcategory) and save. PostHog and the YouTube player load. - Open Privacy settings again and turn Analytics off. The page reloads, and PostHog and YouTube do not load again.
If no banner appears, see why is there no banner. Run the full verification checklist before you ship.
Next steps
- Render your own banner with the headless runtime.
- Inspect consent with DevTools.
- Read consent from your code.
consent.has('measurement')answers whether a category is allowed now,consent.on('consent', listener)reports changes, andconsent.save({ measurement: true })records a choice. The @c15t/browser API lists every method, and options every option. - Run code on consent events with callbacks.
- Add languages with translations.