Nuxt
@avsbhq/nuxt is a Nuxt module. One line in nuxt.config, and every component can read feature flags with auto-imported composables. The datafile is fetched while the page renders on the server and travels to the browser in the payload, so the first paint shows real values instead of your defaults.
It is built on @avsbhq/vue, which is built on @avsbhq/browser. Anything the Vue SDK can do, this module can do: it installs the same plugin for you and adds the Nuxt half (config, auto-imports, server rendering).
Nuxt 3.7 or later is required. Nuxt 4 is supported.
Install
One package. The Vue SDK and the browser client arrive with it.
Register the module
Add it to modules and give it your SDK key.
Read flags anywhere
The composables are auto-imported. There is no provider to mount and no plugin to write.
Identify your visitors
Set the context on the server for a signed-in user, or let the module keep a persisted anonymous id.
Install
npm install @avsbhq/nuxtRegister the module
// nuxt.config.ts// docs-example: not typechecked here, because defineNuxtConfig is a Nuxt// auto-import that only exists inside a Nuxt project.export default defineNuxtConfig({ modules: ['@avsbhq/nuxt'], avsb: { sdkKey: process.env.AVSB_SDK_KEY, },})The module reads avsb.sdkKey first, then NUXT_PUBLIC_AVSB_SDK_KEY, then AVSB_SDK_KEY. With none of them set it warns at build time, and at runtime it creates no client and says so once, naming the fix.
Because the key lives in runtimeConfig.public, you can point a built app at another environment without rebuilding it:
NUXT_PUBLIC_AVSB_SDK_KEY=sdk_production_ttqm0eaj4vth1krcb2xn node .output/server/index.mjsRead a flag
<!-- components/CheckoutButton.vue --><script setup lang="ts">// No import line: the module auto-imports the composables.const newCheckout = useBoolFlag('new_checkout_flow', false)const track = useTrack()// The visitor is about to see this decision, so record it once.useExposure('new_checkout_flow')</script><template> <button @click="track('checkout_started', { value: 99 })"> {{ newCheckout.value ? 'Start checkout (new)' : 'Buy now' }} </button></template>That is the whole setup.
useBoolFlag returns a Vue ref holding a Flag<boolean>, which is why a read inside <script setup> is newCheckout.value.value: the first .value unwraps the ref, the second reads the flag's value. In a template, Vue unwraps the ref for you, so newCheckout.value is the flag's value. The Vue guide covers that distinction in full.
What gets auto-imported
useFlag useFlagValue useBoolFlag useStringFlaguseNumberFlag useJsonFlag useAllFlags useFlagReadyuseAvsbStatus useAvsbClient useIdentify useAliasuseReset useTrack useExposure useFlagSubscriptionEach one is the composable from @avsbhq/vue, with the same signature and behaviour. Set autoImports: false and import them yourself instead:
import { useBoolFlag } from '@avsbhq/vue'AvsbProvider and AvsbPlugin are deliberately not auto-imported. The module already installs one, and a second provider inside it would run two clients with two visitor ids.
Module options
interface AvsbModuleOptions { /** SDK key. Falls back to NUXT_PUBLIC_AVSB_SDK_KEY, then AVSB_SDK_KEY. */ sdkKey?: string /** Fetch the datafile during server rendering and send it in the payload. Default true. */ bootstrap?: boolean /** Auto-import the composables. Default true. */ autoImports?: boolean /** CDN base for datafile fetches. Default 'https://cdn.avsb.cloud'. */ cdnHost?: string /** Milliseconds the server-side datafile fetch may take. Default 3000. */ bootstrapTimeout?: number /** Milliseconds a server-fetched datafile is reused across requests. Default 60000. */ bootstrapMaxAge?: number /** Everything else AvsbClient accepts that survives JSON. */ client?: { pollingInterval?: number autoRefresh?: boolean cdnHost?: string initTimeout?: number cache?: boolean cacheMaxAgeMs?: number adoptSnippetVisitorId?: boolean pauseWhenHidden?: boolean refetchOnFocus?: boolean maxPendingEvents?: number streaming?: boolean streamingEndpoint?: string logLevel?: 'silent' | 'debug' | 'info' | 'warn' | 'error' }}// nuxt.config.ts// docs-example: not typechecked here, because defineNuxtConfig is a Nuxt// auto-import that only exists inside a Nuxt project.export default defineNuxtConfig({ modules: ['@avsbhq/nuxt'], avsb: { sdkKey: process.env.AVSB_SDK_KEY, bootstrapMaxAge: 30_000, client: { pollingInterval: 30_000, logLevel: 'debug' }, },})logger and onError are absent from client because runtimeConfig cannot carry functions. If you need them, build your own client and mount <AvsbProvider :client="client"> from @avsbhq/vue instead of using this module.
Settings under avsb.client never override the SDK key, and never turn polling, caching or streaming back on during a server render.
Server rendering
The module registers one plugin that runs on both sides:
- Server: it fetches the datafile (once per process per TTL, and once for a burst of concurrent requests), puts it in the payload, and installs the Vue plugin with a client that has no timers, no cache and no network. Flags are answered from that datafile while the page renders.
- Browser: it reads the same datafile out of the payload and installs the Vue plugin with a live client that starts ready. Nothing is fetched before the first paint, and no flag flickers from a default to its real value.
A failed server fetch is never cached: the page renders your default values, one warning says why, and the next request tries again.
If you already run a server client
Apps that evaluate flags in Nitro routes with @avsbhq/node already hold a datafile. Hand that one over instead of paying for a second fetch:
// plugins/avsb-bootstrap.server.ts// docs-example: not typechecked here, because defineNuxtPlugin and useState// are Nuxt auto-imports that only exist inside a Nuxt project.import { AVSB_BOOTSTRAP_STATE_KEY, getAvsbBootstrap } from '@avsbhq/nuxt/server'import { avsbServer } from '../server/avsb'export default defineNuxtPlugin(() => { const bootstrap = useState(AVSB_BOOTSTRAP_STATE_KEY, () => null) bootstrap.value = getAvsbBootstrap(avsbServer)})The module's own plugin runs with enforce: 'post', so a plugin like that one has already run and the module leaves the value alone. getAvsbBootstrap hands back the datafile, which is exactly what the browser client's bootstrap option takes. It is the same helper, with the same name and meaning, that @avsbhq/svelte/sveltekit and @avsbhq/solid/solid-start ship.
Turning bootstrap off
// nuxt.config.ts// docs-example: not typechecked here, because defineNuxtConfig is a Nuxt// auto-import that only exists inside a Nuxt project.export default defineNuxtConfig({ modules: ['@avsbhq/nuxt'], avsb: { bootstrap: false },})The datafile then stays out of the HTML and the browser fetches it itself. The trade is honest: a smaller payload, and the first paint shows the default value you passed until the datafile lands.
Identity
Without a context the visitor gets a persisted anonymous id (localStorage, with a cookie fallback), so a returning visitor buckets into the same variation instead of being re-randomised on every load.
For a signed-in user, set the context for the request on the server, and both sides evaluate the same person:
// plugins/avsb-identity.server.ts// docs-example: not typechecked here, because defineNuxtPlugin, useCookie and// useState are Nuxt auto-imports that only exist inside a Nuxt project.import { AVSB_CONTEXT_STATE_KEY } from '@avsbhq/nuxt/server'import type { EvalContext } from '@avsbhq/core'export default defineNuxtPlugin(() => { const userId = useCookie<string | null>('uid').value const context = useState<EvalContext | null>(AVSB_CONTEXT_STATE_KEY, () => null) if (userId) context.value = { kind: 'user', key: userId, plan: 'pro' }})After a login in the browser, switch identity through the composables. alias stitches the anonymous session to the identified one so the earlier events still count:
// composables/useSignIn.ts// docs-example: not typechecked here, because the composables are Nuxt// auto-imports that only exist inside a Nuxt project.import type { EvalContext } from '@avsbhq/core'export function signIn(anonymousKey: string, user: { id: string; plan: string }): void { const identify = useIdentify() const alias = useAlias() alias({ kind: 'user', key: anonymousKey }, { kind: 'user', key: user.id }) const context: EvalContext = { kind: 'user', key: user.id, plan: user.plan } identify(context)}Multi-context works the same way it does in every A vs B SDK, so a rule can bucket on user.key while matching an audience condition on organization.tier:
// plugins/avsb-org-identity.server.ts// docs-example: not typechecked here, because defineNuxtPlugin and useState// are Nuxt auto-imports that only exist inside a Nuxt project.import { AVSB_CONTEXT_STATE_KEY } from '@avsbhq/nuxt/server'export default defineNuxtPlugin(() => { const context = useState(AVSB_CONTEXT_STATE_KEY, () => null) context.value = { kind: 'multi', user: { kind: 'user', key: 'u_123', plan: 'pro' }, organization: { kind: 'organization', key: 'org_456', tier: 'enterprise' }, }})Readiness and failure
<script setup lang="ts">const { status, error, degraded } = useAvsbStatus()</script><template> <SkeletonApp v-if="status === 'loading'" /> <template v-else> <StaleDataNotice v-if="degraded" :message="error?.message" /> <NuxtPage /> </template></template>With the default bootstrap the status is already ready on the first render, so this is a safety net rather than a loading screen you will see.
loading: no datafile yet and nothing cached.ready: flags answer. A degraded client is ready: a refresh failed while a cached datafile is being served, so values work and may be stale.error: nothing could be loaded. Every flag returns the default you passed, anderror.messagenames the HTTP status, the URL tried, and the fix.
Testing
Component tests do not go through Nuxt's plugin, so use the Vue package's test provider directly:
// components/CheckoutButton.test.tsimport { mount } from '@vue/test-utils'import { AvsbTestProvider } from '@avsbhq/vue/testing'import CheckoutButton from './CheckoutButton.vue'const wrapper = mount(AvsbTestProvider, { props: { flags: { 'new_checkout_flow': true } }, slots: { default: CheckoutButton },})expect(wrapper.text()).toContain('Start checkout (new)')That is the real provider around a real client whose datafile is built from your flags map, with the network, the cache and logging switched off. Your components use the same composables and the same evaluator they use in production.
What this module does not do
- It does not evaluate flags inside Nitro server routes. For server-side evaluation, use
@avsbhq/nodein your Nitro code and, if you like, hand its datafile to the page withgetAvsbBootstrap. - It does not register components. The composables are the API.
- It does not add its own devtools panel.
What's next
- Vite + Vue: the composable reference this module auto-imports.
- Multi-context identity
- SDK installation:
Flag<T>, evaluation sources, and the shared options.