---
title: Quickstart
description: Bundle your c15t policy into a SvelteKit server at build time,
  resolve consent in the root layout load so the banner is in the server HTML,
  then hydrate ConsentRoot with consent-gated scripts and a preferences link.
group: frameworks
lastModified: "2026-10-10T16:01:45+01:00"
---
## Before you start

This guide sets up a server-rendered SvelteKit app. The root layout's server
`load` resolves each visitor's consent before the page renders, so the banner
is part of the first HTML and a returning visitor's choice applies from the
first paint. For prerendered pages, a static site or SPA mode, follow this
guide and then [rendering and deployment](/docs/frameworks/sveltekit/rendering).
The `examples/sveltekit` app in the c15t repository is the finished result.

`@c15t/svelte` supports SvelteKit 2.63 and later, including SvelteKit 3,
with Svelte 5. The examples use SvelteKit 3. On SvelteKit 2, keep the adapter
in `svelte.config.js` instead of passing it to `sveltekit()`.

The guide uses [Inth](https://inth.com) for policies and consent records.
Create an Inth project, then:

1. Set its policy rules, including the `measurement` category for the
   vendor below.
2. Add your app's origin, such as `http://localhost:5173`, to its trusted
   origins.
3. Copy the project's backend URL. The build downloads the policy from it and
   the browser saves choices to it.

A self-hosted backend works the same way with your own URL; see
[self-hosting](/docs/self-host/overview).

The setup bundles your policy into the server at build time, the recommended
production setup. Rebuild after changing policies, translations or vendors.

## Install

|Package manager|Command|
|:--|:--|
|npm|`npm install @c15t/svelte@alpha @c15t/integrations@alpha`|
|pnpm|`pnpm add @c15t/svelte@alpha @c15t/integrations@alpha`|
|yarn|`yarn add @c15t/svelte@alpha @c15t/integrations@alpha`|
|bun|`bun add @c15t/svelte@alpha @c15t/integrations@alpha`|

SvelteKit uses `@c15t/svelte`: components from `@c15t/svelte`, server
helpers from `@c15t/svelte/kit` and the Vite plugin from `@c15t/svelte/vite`.
The `c15t` package has no Svelte entry point, so you don't install it.

## Set the backend URL

Set `PUBLIC_C15T_BACKEND_URL` to your project's backend URL, including any
path prefix, in `.env` or your host's build environment:

```sh title=".env"
PUBLIC_C15T_BACKEND_URL=https://your-project.inth.app
```

If your app already sets `PUBLIC_INTH_PROJECT_URL` for other Inth SDKs, that works
too. When both are set, `PUBLIC_C15T_BACKEND_URL` wins.

The example app's `.env` points at `https://example-inth.inth.app`, a demo
project. The build reads the variable, and the server and the browser use the
value the build read, so rebuild after you change it.

## Bundle the policy at build time

Add `consentManifest` to the plugins in `vite.config.ts`:

```ts title="vite.config.ts"
import { consentManifest } from '@c15t/svelte/vite';
import adapter from '@sveltejs/adapter-auto';
import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vite';

export default defineConfig({
	plugins: [consentManifest(), sveltekit({ adapter: adapter() })],
});
```

`consentManifest` downloads your project's public policy from
`<backendURL>/manifest` during `vite build`, and in `vite dev` the first time
the server loads it. The policy stays on the server:
the browser bundle never gets it. No file is written into your project. In a
build, the plugin also lets `c15tHandle`, added below, preload the script
loader on pages that register scripts.

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](/docs/concepts/modes#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:

```sh
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](/docs/concepts/modes#what-happens-when-the-download-fails)
lists every case.

To apply policy edits without a rebuild, fetch the policy at runtime instead;
see [fetch the manifest at runtime](/docs/frameworks/sveltekit/rendering#fetch-the-manifest-at-runtime).

## Add the server hook

`c15tHandle` holds the consent config and reads the consent cookie, location
headers and Global Privacy Control once per request. It stores them on
`event.locals.c15t` for `loadConsent` and any other load or endpoint that
needs them. It also writes a server-rendered banner's rules into the HTML
`<head>` as `<style>` elements, so the banner is styled before hydration
without a stylesheet request. Add it to `src/hooks.server.ts`:

```ts title="src/hooks.server.ts"
import { c15tHandle } from '@c15t/svelte/kit';

export const handle = c15tHandle();
```

With no options, the mode is `manifest()`: the server resolves each visitor
from the bundled policy. [Rendering and deployment](/docs/frameworks/sveltekit/rendering)
covers `hosted()`, `offline()` and the other options.

Type `event.locals.c15t` with one line in `src/app.d.ts`:

```ts title="src/app.d.ts"
/// <reference types="@c15t/svelte/kit/locals" />
```

## Resolve consent in the root layout load

Export `loadConsent` as the root layout's server load:

```ts title="src/routes/+layout.server.ts"
export { loadConsent as load } from '@c15t/svelte/kit';
```

`loadConsent` resolves the visitor's policy from the bundled snapshot with
the inputs `c15tHandle` read, and returns `{ consent }`, a plain,
serializable object for `ConsentRoot`. Rendering a page makes no policy
request to the backend. `loadConsent` never throws. If resolution takes
longer than 500 ms, it returns the stored choice without a policy, and the
browser resolves the policy after hydration.

While SvelteKit prerenders pages at build time, `loadConsent` returns no
visitor's consent and no policy, because a prerendered page goes to every
visitor. It reads SvelteKit's `building` flag itself.
[Prerender some pages](/docs/frameworks/sveltekit/rendering#prerender-some-pages)
covers how the browser resolves consent there.

## Render ConsentRoot in the root layout

```svelte title="src/routes/+layout.svelte"
<script lang="ts">
	import { posthog } from '@c15t/integrations/posthog';
	import {
		ConsentBanner,
		ConsentDialog,
		ConsentDialogLink,
		ConsentRoot,
	} from '@c15t/svelte';

	let { children, data } = $props();

	const scripts = [
		posthog({
			id: 'phc_your_project_key',
			initOptions: { cookieless_mode: 'never' },
			loadMode: 'after-consent',
		}),
	];
</script>

<ConsentRoot state={data.consent} {scripts}>
	{@render children()}
	<footer>
		<ConsentDialogLink>Privacy settings</ConsentDialogLink>
	</footer>
	<ConsentBanner />
	<ConsentDialog />
</ConsentRoot>
```

Pass `data.consent` through unchanged. `ConsentRoot` starts from it, so the
server and the browser render the same banner and the browser makes no second
request for the policy. A page the server resolved ships no resolver, policy
rules or snapshot to the browser.

PostHog waits for measurement permission. Replace `phc_your_project_key`
with your PostHog project key. Remove any `<script>` tags in `src/app.html` or
SDK imports that already load PostHog, so c15t is the only thing that loads
it. Create `scripts` in the layout component, not in `+layout.server.ts`:
they hold functions, which a server load cannot send to the browser.

`ConsentBanner` shows the banner when the policy asks for one.
`ConsentDialog` is the preference dialog; it loads in its own chunk after the
page, before anyone opens it. `ConsentDialogLink` is the persistent way back to
preferences. Put it where visitors look for privacy settings, such as the
footer. There is no stylesheet to import: the components bring their own
rules, and the server hook writes the banner's into the HTML.

Keep `ConsentRoot` in the root layout so it stays mounted across client-side
navigation.

## Verify consent

Build the app with `vite build` and serve the production build with
`vite preview`, then open it in a private window with DevTools open:

1. View the page source. It contains `data-testid="consent-banner-root"`, so
   the banner is in the server HTML, and `<head>` holds a
   `<style data-c15t-styles>` element with its rules.
2. In the Network panel, no requests go to PostHog.
3. Select **Reject All** and reload. The page source no longer contains the
   banner, and PostHog requests stay absent.
4. Select **Privacy settings**, turn on **Analytics** (the `measurement`
   category) and save. PostHog loads.
5. Turn it off and save. The page reloads and PostHog does not load.
6. Navigate to another route and back. The choice holds without a new banner.

If the build fails or the banner is missing from the page source, see
[troubleshooting](/docs/frameworks/sveltekit/troubleshooting).
[Verify consent](/docs/guides/verify-consent) has the release checklist.

## Next steps

* [Rendering and deployment](/docs/frameworks/sveltekit/rendering): runtime
  policy fetching, prerendered pages, static sites, SPA mode and edge adapters.
* [Customize](/docs/frameworks/sveltekit/customize): brand colors rendered on
  the server, slots and copy.
* [Scripts and embeds](/docs/frameworks/sveltekit/scripts): more vendors,
  gated iframes and network blocking.
* [Context getters](/docs/frameworks/sveltekit/getters): read consent in your
  own components.
