# Nuxt

Source: https://docs.mirafive.io/sdks/nuxt

> Add MIRA FIVE analytics and feature flags to a Nuxt 4 app with one module, with flags answered in the server render and server-side events.

`@mirafive/sdk-nuxt` is a Nuxt 4 module. It bundles the [browser SDK](https://docs.mirafive.io/sdks/browser) with only the plugins you list, auto-imports composables for events and feature flags, answers flags in the server render, and adds server utils that send events from `server/` routes. For Vue without Nuxt, use [`@mirafive/sdk-vue`](https://docs.mirafive.io/sdks/vue).

## Install

```bash
npm install @mirafive/sdk-nuxt @mirafive/sdk-vue @mirafive/sdk-browser @mirafive/sdk-server
```

Requires Nuxt 4 and Node.js 20 or later. `@mirafive/sdk-vue`, `@mirafive/sdk-browser` and `@mirafive/sdk-server` are peer dependencies. `@mirafive/sdk-server` runs on the server only and is never bundled for the browser.

## Set up

Add the module to `nuxt.config.ts`:

```ts title="nuxt.config.ts"
export default defineNuxtConfig({
  modules: ['@mirafive/sdk-nuxt'],
  mirafive: {
    features: ['flags'],
  },
})
```

The browser and the server use different keys. See [Keys](https://docs.mirafive.io/keys) for where to find them.

| Variable | Value | Read by |
| --- | --- | --- |
| `NUXT_PUBLIC_MIRAFIVE_KEY` | The **website key** of a website source (`mf_…`). Public. | The browser client, through public runtime config. |
| `MIRAFIVE_SECRET_KEY` | The **secret key** of a server source (`mf_…`). Server only. | The server utils and the flag bootstrap, at runtime. `NUXT_MIRAFIVE_SECRET_KEY` works too and wins. |
| `NUXT_PUBLIC_MIRAFIVE_HOST` | Optional. Default `https://events.mirafive.io`. | The browser and the server, at runtime. |
| `MIRAFIVE_HOST` | Optional. | Baked into the browser build at build time; read by the server utils at runtime when the public host is empty. |

```sh title=".env"
NUXT_PUBLIC_MIRAFIVE_KEY=mf_…
MIRAFIVE_SECRET_KEY=mf_…
```

> **Warning:** Never put the secret key under `runtimeConfig.public`, in `app.config` or in client code. The module keeps it in private runtime config only. MIRA FIVE marks a secret key that arrives from a browser as exposed, and you have to rotate it.

That is the whole install. Pageviews need no code: the `pageviews()` plugin follows the Nuxt router and records the first page and every navigation. With `router.options.hashMode`, the module turns on hash routing for it.

The client starts in [consentless mode](https://docs.mirafive.io/guides/consent): it sets no cookies, stores nothing on the device and needs no consent banner.

## Verify

1. Deploy, or run the production build on a host that is not `localhost`. The module has no localhost switch: on `localhost` the browser SDK sends nothing and logs `[mirafive] local host: set trackLocalhost`, which confirms the client runs.
2. In the browser's network tab, look for `POST https://events.mirafive.io/v1/batch/mf_…` answering `202` with `"accepted": 1`. Batches leave 5 seconds after the first event, or when the tab is hidden.
3. Open the source's live view in MIRA FIVE and find the pageview.
4. With `flags` and a secret key, the page source has `<script type="application/json" id="mirafive-flags">` in its head, and the response carries `Cache-Control: private, no-store`.

To check the server side, send the reserved install-check event once from a server route:

```ts title="server/api/install-check.get.ts"
export default defineEventHandler((event) => useServerMira(event).send([{ name: '$install_check' }]))
```

With a working secret key and host it answers `"accepted": 0, "dropped": 1, "reason": "install_check"`, and MIRA FIVE records the check on the source. Remove the route afterwards. More in [Verify and debug](https://docs.mirafive.io/guides/verify-and-debug).

## Track events

`useMira()` is auto-imported. Call `track(name, properties)` in a handler:

```vue title="components/CheckoutButton.vue"
<script setup lang="ts">
const mira = useMira()
</script>

<template>
  <button @click="mira.track('checkout_started', { plan: 'pro' })">Checkout</button>
</template>
```

During the server render `useMira()` returns a stand-in that does nothing, so the same component renders on both sides. With `features: ['autocapture']`, clicks, submits and changes are recorded without code. Event names, property limits and revenue are covered in [Track events](https://docs.mirafive.io/guides/track-events).

## Server-side events

`useServerMira(event)` is auto-imported in `server/`. It returns one [server SDK](https://docs.mirafive.io/sdks/node) client per process, on the secret key. `track()` only buffers; after the response, the module flushes this request's events with `event.waitUntil`, so the response is not delayed.

```ts title="server/api/order.post.ts"
export default defineEventHandler(async (event) => {
  const { userId, total } = await readBody<{ userId: string; total: number }>(event)

  useServerMira(event).track('order_completed', {
    userId,
    properties: { revenue: total, currency: 'EUR' },
  })

  return { ok: true }
})
```

`userId` is your own pseudonymous id for the person, never an email address. Server events are sent in full mode: you decide the lawful basis for the ids you send. Transport errors are logged with a `[mirafive]` prefix, never thrown. For batching, retries and idempotency, see [Server-side](https://docs.mirafive.io/guides/server-side).

## Identify users

Identifying users needs [full mode](https://docs.mirafive.io/guides/consent#full-mode). Set `mode: 'full'`; the module adds the `identity()` plugin to the bundle:

```ts title="nuxt.config.ts"
export default defineNuxtConfig({
  modules: ['@mirafive/sdk-nuxt'],
  mirafive: {
    mode: 'full',
    features: ['flags'],
  },
})
```

Then pass your consent manager's answer and the user id through `useMira()`:

```vue title="components/ConsentBanner.vue"
<script setup lang="ts">
const mira = useMira()
</script>

<template>
  <button @click="mira.consent({ statistics: true, experiments: true, targeting: false })">Accept</button>
  <button @click="mira.consent(false)">Decline</button>
</template>
```

```ts
const mira = useMira()

// After login:
mira.identify(user.id, { plan: user.plan })

// After logout:
mira.reset()
```

Before a consent answer, full mode stores and sends nothing. `consent(false)` forgets everything stored on the device. Do Not Track, Global Privacy Control and prerendering send nothing from the browser. See [Identify users](https://docs.mirafive.io/guides/identify-users) and [Consent](https://docs.mirafive.io/guides/consent).

## Feature flags

With `'flags'` in `features`, read a flag with the auto-imported `useFlag` and its remote config with `useFlagConfig`. Both return a read-only ref:

```vue title="components/BuyButton.vue"
<script setup lang="ts">
const newCheckout = useFlag('new-checkout', false)
const limits = useFlagConfig('checkout-limits', { maxItems: 10 })
</script>

<template>
  <button>{{ newCheckout === true ? 'Buy now' : `Checkout (max ${limits.maxItems})` }}</button>
</template>
```

`useFlag` answers what `mira.flag(key, fallback)` answers: the variant key, or `true`/`false` for an on/off flag.

### Flag bootstrap

With `'flags'` and a secret key, every server render reads the visitor's flags once, renders `useFlag` and `useFlagConfig` from them, writes the `mirafive-flags` block into the head and sends `Cache-Control: private, no-store`. The browser SDK starts from that block, so hydration matches the server render and nothing flickers. Without a secret key there is no bootstrap: the refs render their fallback on the server and update once the browser has loaded the flags.

The bootstrap is computed for an anonymous visitor unless a server middleware names the unit on `event.context.mirafive`. This example uses `getUserSession` from nuxt-auth-utils; use your own session lookup:

```ts title="server/middleware/mirafive.ts"
export default defineEventHandler(async (event) => {
  const session = await getUserSession(event)

  event.context.mirafive = { userId: session.user?.id, properties: { plan: session.user?.plan } }
})
```

The server reads `Sec-GPC: 1` and `DNT: 1` from the request and sets `optedOut`: no ids, no segment lookup, no exposure. The first flag read of a cold server instance waits up to 1.5 seconds for the flag document; later reads are synchronous.

### Flags in server routes

`miraFlagsFor(event, unit?)` is auto-imported in `server/`:

```ts title="server/api/checkout.get.ts"
export default defineEventHandler(async (event) => {
  const flags = await miraFlagsFor(event, { userId: 'u_42' })

  return { newCheckout: flags.enabled('new-checkout') }
})
```

It returns `UserFlags` with `enabled(key, fallback?)`, `variant(key, fallback?)`, `config(key, fallback)`, `evaluate(key)` and `bootstrap()`, as described in the [server SDK](https://docs.mirafive.io/sdks/node#feature-flags). Refreshes and exposures go to `event.waitUntil`. See [Feature flags](https://docs.mirafive.io/guides/feature-flags) and [Experiments](https://docs.mirafive.io/guides/experiments).

### Cached routes

Nitro's `swr`, `isr` and `cache` route rules, and prerendered pages, store one rendered page and serve it to every visitor. Those caches ignore `Cache-Control: private, no-store`, so the module writes no bootstrap on such routes, nor in handlers Nitro caches:

- `useFlag` renders its fallback on the server.
- The browser loads the flags itself and the refs update after hydration.
- The server logs `[mirafive] no flag bootstrap on cached route …` once per process.

Keep pages that must render a flag's value on the server off shared caches:

```ts title="nuxt.config.ts"
export default defineNuxtConfig({
  modules: ['@mirafive/sdk-nuxt'],
  mirafive: { features: ['flags'] },
  routeRules: {
    // Served from a shared cache: no bootstrap, flags load in the browser.
    '/blog/**': { swr: 3600 },
  },
})
```

## API reference

### Module options

Set under `mirafive` in `nuxt.config.ts`.

| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `key` | `string` | `''` | The website key. `NUXT_PUBLIC_MIRAFIVE_KEY` sets it at runtime. Without one, the build warns and the browser logs `[mirafive] no website key` and sends nothing. |
| `host` | `string` | `MIRAFIVE_HOST` at build, else `https://events.mirafive.io` | Where events and flag requests go. Needs a scheme. `NUXT_PUBLIC_MIRAFIVE_HOST` sets it at runtime. |
| `mode` | `'consentless' \| 'full'` | `'consentless'` | The browser's mode. `'full'` adds `identity()`. Build time only. |
| `features` | `('autocapture' \| 'search' \| 'flags' \| 'experiments')[]` | `[]` | Browser plugins besides pageviews; nothing else is bundled. `search` and `experiments` need `mode: 'full'`. `experiments` adds `flags`. Build time only. |
| `secretKey` | `string` | `''` | The secret key, for the server utils and the bootstrap. Prefer `MIRAFIVE_SECRET_KEY` or `NUXT_MIRAFIVE_SECRET_KEY` at runtime over a literal. |

The build fails with a `TypeError` for an unknown `mode` or feature, a `host` without a scheme, or `search` or `experiments` without `mode: 'full'`.

### Auto-imports

In the app:

| Export | Description |
| --- | --- |
| `useMira<Events>()` | The browser client. During the server render, a stand-in that does nothing and answers fallbacks. See the [browser SDK reference](https://docs.mirafive.io/sdks/browser#api-reference) for its methods. |
| `useFlag(key, fallback)` | `Readonly<Ref<string \| boolean>>`. The variant, or `true`/`false` for an on/off flag. The bootstrap during the server render and hydration, then follows flag loads. |
| `useFlagConfig<T>(key, fallback)` | `Readonly<Ref<T>>`. The remote-config value of the flag's variant. |

In `server/`:

| Export | Description |
| --- | --- |
| `useServerMira(event)` | `Mira`. A process-wide [server SDK](https://docs.mirafive.io/sdks/node) client on the secret key. This request's events are flushed with `event.waitUntil` after the response. |
| `miraFlagsFor(event, unit?)` | `Promise<UserFlags>`. One visitor's flags from a process-wide `MiraFlags`. Sets `optedOut` from `Sec-GPC: 1` or `DNT: 1`. `unit` takes `userId`, `anonymousId`, `properties`, `consent` and `optedOut`. |

`event.context.mirafive` takes the same `unit` and names the visitor for the flag bootstrap.

The composables come from [`@mirafive/sdk-vue`](https://docs.mirafive.io/sdks/vue) and the server utils wrap [`@mirafive/sdk-server`](https://docs.mirafive.io/sdks/node). The module stores nothing itself.

## Troubleshooting

| Symptom | Cause and fix |
| --- | --- |
| Nothing arrives | The page runs on `localhost`, where nothing is sent. Do Not Track or Global Privacy Control is on. In `mode: 'full'`, no `consent()` call has run. The browser logs `no website key`: set `NUXT_PUBLIC_MIRAFIVE_KEY`. The site's origin is not allowed on the source. |
| Build fails with `[@mirafive/sdk-nuxt] …` | An unknown `mode` or feature, a `host` without a scheme, or `search` or `experiments` without `mode: 'full'`. |
| `403 secret_key_in_path` or `403 website_key_as_bearer` | The keys are swapped. `key` is the website key, `secretKey` the server's. |
| `403 origin_not_allowed` | Add the site's domain to the source in MIRA FIVE. |
| A flag always returns its fallback | The flag is not in this source's flags. `'flags'` is missing from `features`. Experiments consent is missing. |
| No `#mirafive-flags` block in the head | `'flags'` is not in `features`, there is no secret key at runtime, or the route is prerendered or cached (`swr`, `isr`, `cache`); the server logs the cached case. |
| Server events missing | `MIRAFIVE_SECRET_KEY` is not set at runtime. Transport errors are logged with a `[mirafive]` prefix. |

## Set up with an AI agent

Paste this into your coding agent:

```text
Add MIRA FIVE analytics and feature flags to this Nuxt 4 app with @mirafive/sdk-nuxt.
Docs: https://docs.mirafive.io/sdks/nuxt.md

1. Install @mirafive/sdk-nuxt, @mirafive/sdk-vue, @mirafive/sdk-browser and @mirafive/sdk-server with
   the project's package manager, and add '@mirafive/sdk-nuxt' to modules in nuxt.config.ts.
2. In nuxt.config.ts add mirafive: { features: [] }. Add 'flags' if the app reads feature flags,
   'autocapture' if clicks should be counted. Leave mode out (consentless).
3. Env: NUXT_PUBLIC_MIRAFIVE_KEY=<website key, mf_…> (public). If server events or server-rendered
   flags are needed: MIRAFIVE_SECRET_KEY=<secret key of a server source>, server only. Never put it
   in runtimeConfig.public, app.config, client code or a nuxt.config literal.
4. Do not add pageview code; pageviews are automatic. Use the auto-imports without importing them:
   in components const mira = useMira() and mira.track(name, properties) in handlers,
   useFlag(key, fallback) and useFlagConfig(key, fallback) (read-only refs);
   in server/ routes useServerMira(event).track(name, { userId, properties }) and
   await miraFlagsFor(event, { userId }).
5. To bootstrap flags for a signed-in user, set event.context.mirafive = { userId } in a server
   middleware. Pages under swr, isr, cache or prerender get no bootstrap; that is expected.
6. Only if the app already has a consent manager and needs user ids: set mode: 'full' and call
   useMira().consent({ statistics, experiments, targeting }) from its callback.
7. Verify: run nuxi build and start it on a non-localhost host; check the network tab for
   POST https://events.mirafive.io/v1/batch/<key> answering 202; with flags, check the page head for
   id="mirafive-flags". Server side, useServerMira(event).send([{ name: '$install_check' }]) resolves
   with reason 'install_check'. Report what you changed.
Do not add other analytics libraries, cookies or consent banners.
```
