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 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.
Install
npm install @mirafive/sdk-nuxt @mirafive/sdk-vue @mirafive/sdk-browser @mirafive/sdk-serverRequires 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:
export default defineNuxtConfig({
modules: ['@mirafive/sdk-nuxt'],
mirafive: {
features: ['flags'],
},
})The browser and the server use different keys. See 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. |
NUXT_PUBLIC_MIRAFIVE_KEY=mf_…
MIRAFIVE_SECRET_KEY=mf_…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: it sets no cookies, stores nothing on the device and needs no consent banner.
Verify
- Deploy, or run the production build on a host that is not
localhost. The module has no localhost switch: onlocalhostthe browser SDK sends nothing and logs[mirafive] local host: set trackLocalhost, which confirms the client runs. - In the browser's network tab, look for
POST https://events.mirafive.io/v1/batch/mf_…answering202with"accepted": 1. Batches leave 5 seconds after the first event, or when the tab is hidden. - Open the source's live view in MIRA FIVE and find the pageview.
- With
flagsand a secret key, the page source has<script type="application/json" id="mirafive-flags">in its head, and the response carriesCache-Control: private, no-store.
To check the server side, send the reserved install-check event once from a server route:
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.
Track events
useMira() is auto-imported. Call track(name, properties) in a handler:
<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.
Server-side events
useServerMira(event) is auto-imported in server/. It returns one server SDK 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.
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.
Identify users
Identifying users needs full mode. Set mode: 'full'; the module adds the identity() plugin to the bundle:
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():
<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>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 and 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:
<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:
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/:
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. Refreshes and exposures go to event.waitUntil. See Feature flags and 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:
useFlagrenders 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:
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 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 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 and the server utils wrap @mirafive/sdk-server. 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:
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.