---
title: Rendering and deployment
description: Pick where a Vue app's consent policy comes from with the plugin's
  mode option, host it statically, and why server-rendered Vue apps should use
  Nuxt.
group: frameworks
lastModified: "2026-10-11T18:47:03+01:00"
lastAuthor: Kaylee
---
## Pick the setup for your deployment

The Vue plugin runs in the browser only. The `mode` option of
`app.use(c15tVue, { mode })` decides where each visitor's policy comes from.
Import the mode from `c15t/vue/vue-plugin`:

|Your app|`mode`|
|--|--|
|Vue with Vite, served as static files|[`hosted()`](#ask-the-backend-for-each-visitors-policy), as in the [quickstart](/docs/frameworks/vue/quickstart). The backend resolves each visitor's policy from their location.|
|A policy without location rules, or a page that already knows the visitor's location|[`manifest()`](#bundle-the-manifest-during-builds). The build bundles the policy and the browser resolves it.|
|Browser resolution, with policy edits that apply without a rebuild|[`manifest({ source: 'runtime' })`](#fetch-the-manifest-at-runtime)|
|A prototype with no backend|[`offline()`](#resolve-the-policy-without-a-backend). Not recommended for production environments.|
|Vue rendered on the server without Nuxt|Any of the above; the banner mounts after hydration. For the banner in server HTML, [use Nuxt](#server-rendering-without-nuxt).|

`mode` is required. Without it, `app.use()` throws and asks for one. Each
mode is imported statically, so the bundle carries only the modes your app
imports.

## Ask the backend for each visitor's policy

The [quickstart](/docs/frameworks/vue/quickstart) and `examples/vue` in the
c15t repository use this setup. `hosted()` sends each page load to the
backend's `/init`. The backend reads the visitor's location from the request,
resolves the current policy and returns the text in the visitor's language:

```ts title="src/main.ts"
import { c15tVue, hosted } from 'c15t/vue/vue-plugin';

app.use(c15tVue, { mode: hosted(), scripts });
```

`hosted()` reads the backend URL from `consentManifest()`, or takes it as
`hosted({ backendURL: 'https://your-project.inth.app' })`. Keep
`consentManifest()` in `vite.config.ts` either way: it also keeps the c15t
components, which are `.vue` files, out of Vite's dependency pre-bundling.
A `hosted()` build never downloads the manifest, so a backend outage does
not stop it.

Until `/init` answers, every optional category is denied, so gated scripts
and iframes wait. The build output is plain static files. Deploy it to any
static host. The backend must allow your site's origin. With Inth, add it to
the project's trusted origins.
[Data fetching](/docs/concepts/data-fetching) compares the two paths.

### Send `/init` before the app loads

`consentManifest()` adds a small inline script to the `<head>` of
`index.html`. It sends `/init` while the browser downloads your JavaScript,
and `hosted()` uses that response instead of sending its own. The banner
usually shows 50 to 250 ms sooner. There is nothing to configure.

The early request can't read your app's options, so in a few setups it won't
match and the app sends a second `/init`. Pass `initPrefetch: false` when the
app uses `offline()` or gives `hosted()` its own `backendURL`, `fetch`,
`initURL` or `headers`. With its own `fetch`, `hosted()` sends `/init` through
it and ignores the early response. If it sets `credentials`, `overrides`, `storageKey`, an
`experiment`, or `journey: false`, pass the same values as
`initPrefetch: { credentials, experiment, journey, overrides, storageKey }`.
With `journey: 'tab'`, passing it too makes a tab's first `/init` report the
`'tab'` scope instead of `'page'`. It doesn't save a request.
Pass `experiment` only when c15t assigns the arm. If a feature flag picks
it, leave it out, or the script sends an arm the visitor may never see.
The dev server adds the script even to a `manifest()` app, which then sends
one `/init` per page load that it doesn't use. A production build leaves it
out.

## Bundle the manifest during builds

`manifest()` resolves the policy in the browser from a snapshot the build
downloaded. It saves the `/init` request only when the policy has no location
rules, or when the browser knows the visitor's location. Otherwise the bundle
adds bytes without saving a request. The resolver adds about 3 KB gzipped,
policy edits need a rebuild, and the backend doesn't count visitors the
browser resolves this way.

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

`consentManifest()` from `c15t/vue/vite` downloads the policy during
`vite build`, when the app uses `manifest()`, and in `vite dev` when the app
first loads it:

```ts title="vite.config.ts"
import vue from '@vitejs/plugin-vue';
import { consentManifest } from 'c15t/vue/vite';
import { defineConfig } from 'vite';

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

Without a `backendURL` option, the plugin reads `VITE_C15T_BACKEND_URL`, then
`VITE_INTH_PROJECT_URL`, from the environment or `.env`. `manifest()` from `c15t/vue/vue-plugin`, with no
options, reads the downloaded policy and the same backend URL, so `src/main.ts`
needs no import from `c15t/generated`:

```ts title="src/main.ts"
import { c15tVue, manifest } from 'c15t/vue/vue-plugin';

app.use(c15tVue, { mode: manifest(), scripts });
```

The browser resolves the visitor's policy from the bundled snapshot, with no
`/manifest` request. When the policy depends on the visitor's country or
region and neither `inputs` nor `geoURL` supplies one, the browser asks the
backend's `/init` instead. The app bundles the policy and English copy, and
loads the resolver as its own chunk when it needs it.

When the build has no snapshot, as in `vite dev` after a failed download,
`manifest()` fetches `${backendURL}/manifest` when the app starts.

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.

`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.

For policy edits without a rebuild, use
[`hosted()`](/docs/frameworks/vue/rendering#ask-the-backend-for-each-visitors-policy)
or [`manifest({ source: 'runtime' })`](/docs/frameworks/vue/rendering#fetch-the-manifest-at-runtime).

### Tell the browser where the visitor is

The browser has no request location. When the policy depends on the
visitor's country or region, `manifest()` asks the backend's `/init` on the
first visit, as React and Svelte do, rather than apply the rule for an
unknown location, which could be another region's. `vite build` warns when
it bundles such a policy and suggests `hosted()`, because the bundled policy
then saves no request. To resolve in the browser instead:

* Pass `country` and `region` props to `ConsentRoot` when the page already
  knows the location, for example from a value your edge server injected.
* Pass `inputs: { country, region }` to `manifest()` for the same purpose,
  set once when the plugin installs.
* Set `geoURL` to a same-origin route you run that returns the visitor's
  `{ country, region }` as JSON. `manifest()` requests it only when the
  policy depends on a location it does not have.
* Pass `initFallback: false` to `manifest()` to resolve the policy for an
  unknown location without asking the backend. Only do this when that rule
  is right for every visitor it reaches.

### Load the visitor's language

The bundle includes English copy. When a visitor resolves to another
language your project has text for, the browser loads the base text for that
language from your site the first time it is needed. A visitor downloads
their own language only, never the full set.

## Fetch the manifest at runtime

`manifest({ source: 'runtime' })` ignores the build's snapshot and fetches
`${backendURL}/manifest` when the app starts. The browser resolves the
policy from it, so policy edits apply without a rebuild:

```ts title="src/main.ts"
import { c15tVue, manifest } from 'c15t/vue/vue-plugin';

app.use(c15tVue, { mode: manifest({ source: 'runtime' }), scripts });
```

Keep `consentManifest()` in `vite.config.ts`: `manifest()` reads the backend
URL from it. The plugin can't see options you pass in app code, so set
`consentManifest({ source: 'runtime' })` as well. Without it, the build still
downloads the manifest, and fails when the backend is down.

To fetch the manifest from somewhere else, such as a CDN in front of your
backend, pass `manifest({ manifestURL })`. The app fetches that URL when it
starts and ignores the build's snapshot. As with `source: 'runtime'`, set
`consentManifest({ source: 'runtime' })` so the build doesn't download the
manifest. Consent saves still go to the backend URL from `consentManifest()`.

The manifest is the same for every visitor, so a CDN can cache it. Location,
`geoURL` and language loading work as in the
[build-time setup](#tell-the-browser-where-the-visitor-is). Consent saves
still go to the backend.

## Resolve the policy without a backend

`offline()` resolves policy rules in the browser and stores choices in the
visitor's cookie and localStorage. There is no backend, so no consent
records are kept. Not recommended for production environments.

```ts title="src/main.ts"
import { c15tVue, offline } from 'c15t/vue/vue-plugin';

app.use(c15tVue, { mode: offline(), scripts });
```

Without options, `offline()` uses c15t's recommended policy rules. Pass
`offline({ policyRules })` to replace them.

## Server rendering without Nuxt

c15t has no server helper for plain Vue. The plugin accepts `prefetch` and
`initialRecords`, but nothing produces them for a custom Vue server. A
server-rendered page renders without the banner, and the banner appears after
hydration.

Your theme tokens can still be in the first HTML. `generateTokensCSS()` from
`c15t/vue/vue-plugin` returns the CSS the plugin applies. Put it in a
`<style id="c15t-css-vars">` element in your server HTML, and the plugin
reuses that element in the browser:

```ts
import { generateTokensCSS } from 'c15t/vue/vue-plugin';

const tokensStyle = `<style id="c15t-css-vars">${generateTokensCSS(tokens)}</style>`;
```

For the banner in server-rendered HTML, use the c15t Nuxt module. Start with
the Nuxt quickstart in the [framework list](/docs/frameworks).
It resolves the policy on the server, renders the banner and tokens in the
first HTML, and hydrates without another request.

## Verify your deployment

Serve the production build and open the Network tab:

|`mode`|Policy requests on the first page load|
|--|--|
|`hosted()`|One `GET` to `${backendURL}/init`|
|`manifest()` with a build snapshot|None to the backend when the policy has no location rules or the browser knows the location, otherwise one `GET` to `${backendURL}/init`. A language other than English loads its text from your site.|
|`manifest({ source: 'runtime' })`|One `GET` to `${backendURL}/manifest`|
|`manifest({ manifestURL })`|One `GET` to `manifestURL`|
|`offline()`|None|

Reject, reload, and confirm the banner stays closed. Then follow
[verify consent](/docs/guides/verify-consent) for the vendor checks.
