---
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-10T16:01:45+01:00"
---
## 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|`manifest()`, as in the [quickstart](/docs/frameworks/vue/quickstart). The build bundles the policy and the browser resolves it. Plan location inputs or an unknown-location rule.|
|Policy edits must apply without a rebuild|[`manifest({ source: 'runtime' })`](#fetch-the-manifest-at-runtime)|
|Regional policies that need the visitor's location from the backend|[`hosted()`](#ask-the-backend-for-each-visitors-policy)|
|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.

## Bundle the manifest during builds

The [quickstart](/docs/frameworks/vue/quickstart) and `examples/vue` in the
c15t repository use this setup.

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 { posthog } from '@c15t/integrations/posthog';
import { c15tVue, manifest } from 'c15t/vue/vue-plugin';
import { createApp } from 'vue';

import App from './App.vue';

createApp(App)
	.use(c15tVue, {
		mode: manifest(),
		scripts: [
			posthog({
				id: 'phc_your_project_key',
				initOptions: { cookieless_mode: 'never' },
				loadMode: 'after-consent',
			}),
		],
	})
	.mount('#app');
```

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
[`manifest({ source: 'runtime' })`](/docs/frameworks/vue/rendering#fetch-the-manifest-at-runtime),
or [`hosted()`](/docs/frameworks/vue/rendering#ask-the-backend-for-each-visitors-policy)
when you need the backend's geolocation.

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

## Ask the backend for each visitor's policy

`hosted()` sends each visitor's first page load to the backend's `/init`.
The backend reads the visitor's location from the request, resolves the
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.

## 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|
|--|--|
|`manifest()` with a build snapshot|None to the backend. 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`|
|`hosted()`|One `GET` to `${backendURL}/init`|
|`offline()`|None|

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