Meta-framework SDKs

Nuxt SDK

Install the Nuxt module and evaluate configs and feature flags in the browser, in server handlers, and during SSR

Introduction

The Nuxt SDK combines a client and a server SDK to provide configs to the browser, to server resource handlers, and to SSR. It is intended to be used by Nuxt web applications running on modern web browsers. All modern web browsers on popular platforms should be supported.

The minimum supported version of Nuxt is 3.7.0.

Browser SDKs are tested on the latest versions of Chrome, Firefox, Safari, and Edge.

Telemetry collection within the browser uses Web Workers to aggregate and send telemetry data. If the SDK runs on older browsers without Web Workers support, config evaluation will continue to work but no telemetry data will be collected for that session.

Installation

The SDK can be installed from NPM: https://www.npmjs.com/package/@configdirector/nuxt-sdk

npm install --save @configdirector/nuxt-sdk

Configure the Nuxt module

The Nuxt SDK requires the client SDK key for evaluation in the browser, and the server SDK key for server side rendering (SSR) and for server routes.

You can provide the keys in nuxt.config.ts:

nuxt.config.ts
export default defineNuxtConfig({
  //...
  modules: ["@configdirector/nuxt-sdk"],
  runtimeConfig: {
    public: {
      configdirector: {
        // You can also provide the key at runtime to match the given environment
        // via the NUXT_PUBLIC_CONFIGDIRECTOR_CLIENT_SDK_KEY environment variable
        clientSdkKey: "YOUR-CLIENT-SDK-KEY",
      },
    },
    configdirector: {
      // IMPORTANT: This is a secret, do not commit to your source repository
      // You can provide the server key at runtime via the
      // NUXT_CONFIGDIRECTOR_SERVER_SDK_KEY environment variable
      serverSdkKey: "YOUR-SERVER-SDK-KEY",
    },
  },
});

Since the server SDK key is a secret value, the recommended approach is to provide it via an environment variable:

export NUXT_CONFIGDIRECTOR_SERVER_SDK_KEY=YOUR-SERVER-SDK-KEY

The client key can also be provided via an environment variable, which overrides the key provided in: nuxt.config.ts:

export NUXT_PUBLIC_CONFIGDIRECTOR_CLIENT_SDK_KEY=YOUR-CLIENT-SDK-KEY

Additional configuration options

appName and appVersion

These options allow you to provide your application's name and version. These values can be used in targeting rules conditionals. For example, if a certain feature should only be enabled starting with a certain version of your application.

nuxt.config.ts
export default defineNuxtConfig({
  //...
  modules: ["@configdirector/nuxt-sdk"],
  runtimeConfig: {
    public: {
      configdirector: {
        appName: "YOUR-APP-NAME",
        appVersion: "1.0.2",
      },
    },
  },
});

logLevel

The SDK uses the consola logger provided via Nuxt. You can adjust the ConfigDirector's SDK logging level independently for both the client and server side:

nuxt.config.ts
export default defineNuxtConfig({
  //...
  modules: ["@configdirector/nuxt-sdk"],
  runtimeConfig: {
    public: {
      configdirector: {
        /**
         * Log level for the client/browser side of the ConfigDirector Nuxt SDK,
         * using consola's numeric levels.
         * 0 = error, 1 = warn, 2 = log, 3 = info, 4 = debug, 5 = trace.
         */
        logLevel: 4, // debug
      },
    },
    configdirector: {
      /**
       * Log level for the server side of the ConfigDirector Nuxt SDK,
       * using consola's numeric levels.
       * 0 = error, 1 = warn, 2 = log, 3 = info, 4 = debug, 5 = trace.
       */
      logLevel: 3, // info
    },
  },
});

baseUrl

The base URL used to connect to ConfigDirector services. It can be configure independently for the client and server side. This should only be provided if your environment requires you to configure a proxy server in order to connect to ConfigDirector services.

connection

Connection options for the server-side client used during SSR and in server routes. These mirror the connection options of the Node.js SDK and accept three optional values:

  • mode
    • The connection mode, which can be either streaming or polling. It is recommended to use the default of streaming unless you have a specific need to use polling instead.
    • Defaults to streaming
  • pollingInterval
    • The interval, in seconds, between requests for config updates when mode is polling. It has no effect in streaming mode. A value below the minimum of 60 seconds is raised to 60 seconds and a warning is logged. See Polling intervals.
    • Defaults to 300 seconds (5 minutes)
  • timeout
    • How long, in milliseconds, the server-side client waits for its initial config payload from ConfigDirector services during initialization. Until the payload is received, configs evaluate to their default values. This timeout also bounds how long incoming requests are held while the client initializes (see waitForInitialization below).
    • Defaults to 3000 milliseconds
nuxt.config.ts
export default defineNuxtConfig({
  //...
  modules: ["@configdirector/nuxt-sdk"],
  runtimeConfig: {
    configdirector: {
      connection: {
        mode: "streaming",
        timeout: 5_000, // 5,000 milliseconds initialization timeout
      },
    },
  },
});

Each option can also be provided at runtime via environment variables: NUXT_CONFIGDIRECTOR_CONNECTION_MODE, NUXT_CONFIGDIRECTOR_CONNECTION_POLLING_INTERVAL, and NUXT_CONFIGDIRECTOR_CONNECTION_TIMEOUT.

The client-side (browser) client accepts the same three options under runtimeConfig.public.configdirector.connection, with the client SDK defaults: mode defaults to streaming, pollingInterval defaults to 60 seconds with a minimum of 30 seconds, and timeout defaults to 2000 milliseconds. See Polling intervals.

nuxt.config.ts
export default defineNuxtConfig({
  //...
  modules: ["@configdirector/nuxt-sdk"],
  runtimeConfig: {
    public: {
      configdirector: {
        connection: {
          mode: "polling",
          pollingInterval: 120, // seconds
        },
      },
    },
  },
});

The client-side options can also be provided at runtime via environment variables: NUXT_PUBLIC_CONFIGDIRECTOR_CONNECTION_MODE, NUXT_PUBLIC_CONFIGDIRECTOR_CONNECTION_POLLING_INTERVAL, and NUXT_PUBLIC_CONFIGDIRECTOR_CONNECTION_TIMEOUT.

waitForInitialization

When your Nuxt server starts, the server-side client connects to ConfigDirector and waits for its initial config payload. Any request that arrives before that payload is received (typically only the first few requests after a server start or a deploy) is held until the payload arrives, so SSR and server routes evaluate real config values instead of defaults. This behavior is enabled by default.

The wait is bounded by the connection timeout. If the timeout expires before the payload is received:

  • The held requests are released and handled with default config values, and the client logs a warning.
  • Requests that arrive afterwards are no longer held. They also evaluate to default values until the payload arrives.
  • In streaming mode, the client keeps trying to connect in the background and starts serving real values as soon as the payload is received. In polling mode, the next attempt happens on the following polling interval.

Requests are never held once initialization has settled, whether it succeeded, timed out, or failed with an unrecoverable error such as an invalid server SDK key. In normal operation, the payload arrives well within the timeout and the hold is barely noticeable.

Holding requests during startup avoids rendering pages with default values that are then replaced by the real values once the browser client connects, which shows up as a visible flash of content and, in some cases, a hydration mismatch.

If you prefer requests to be handled immediately during startup, accepting that they will evaluate configs to their default values until the payload arrives, you can opt out:

nuxt.config.ts
export default defineNuxtConfig({
  //...
  modules: ["@configdirector/nuxt-sdk"],
  runtimeConfig: {
    configdirector: {
      waitForInitialization: false,
    },
  },
});

The option can also be set at runtime via the NUXT_CONFIGDIRECTOR_WAIT_FOR_INITIALIZATION environment variable.

Hooks

Hooks are events you can subscribe to in order to be notified of some key actions from the client instances. Since the Nuxt SDK utilizes both client (browser) and server instances, there are two separate composables to register hook handlers.

You can learn more about client side hooks in the Hooks section of the Browser Javascript SDK.

You can learn more about server side hooks in the Hooks section of the Node.js SDK.

Client-side Hooks

The useConfigDirectorClientHooks composable can be used to register client-side hook handlers. There are two recommended ways of using the composable:

  1. Registering in a Nuxt plugin (recommended for clientReady and configsUpdated):

The browser client initializes during Nuxt's app:created lifecycle hook, before any page components mount. Registering clientReady and configsUpdated handlers in a Nuxt plugin ensures they're in place before initialization is completed and the events are fired for the first time:

plugins/configdirector-hooks.client.ts
export default defineNuxtPlugin(() => {
  useConfigDirectorClientHooks({
    clientReady: ({ action }) => {
      console.log(`ConfigDirector client ready (${action})`);      
    },
    configsUpdated: ({ keys }) => {
      console.log("Configs updated:", keys);
    },
  });
});

The .client.ts suffix ensures this plugin runs only in the browser, not during SSR.

This approach of registering hook handlers in a plugin, is also suitable for contextUpdated and configEvaluated in cases where the handlers are for global behavior (as opposed of page or component specific).

  1. Registering in a page component:

For hooks handlers for page or component specific behavior, register them inside a component's <script setup>. The composable automatically removes the handlers when the component unmounts, so they won't accumulate across navigation.

pages/profile.vue
<script setup lang="ts">
const { updateContext } = useConfigDirectorContext();

useConfigDirectorClientHooks({
  contextUpdated: ({ context }) => {
    console.log("Context changed:", context);    
  },
  configEvaluated: ({ evaluation }) => {
    console.log("Config evaluated": evaluation);
  },
});
</script>

Server-side Hooks

The useConfigDirectorServerHooks composable can be used to register server-side hook handlers. Server hooks run against the long-lived Nitro singleton ConfigDirector client instance. Register them in a Nitro plugin in server/plugins/ which runs once at server startup:

server/plugins/configdirector-hooks.ts
export default defineNitroPlugin(() => {
  useConfigDirectorServerHooks({
    clientReady: () => {
      console.log("Server SDK connected to ConfigDirector");
    },
    configsUpdated: ({ keys }) => {
      console.log("Server-side configs updated:", keys);
    },
    configEvaluated: ({ evaluation }) => {
      console.log("Config evaluated": evaluation);
    },
  });
});

Retrieve config values

The useConfigDirectorValue composable is available to app components. On the server side, the useConfigDirectorClient returns the global server instance of the client (an instance of the Node.js SDK).

Retrieving config values in components:

YourComponent.vue
<script setup lang="ts">
const { value } = useConfigDirectorValue("my-config-key", false);
</script>

<template>
  <div>my-config-key is : {{ value }}</div>
</template>

Retrieving config values in server resource handlers:

server-resource.get.ts
export default defineEventHandler(async (_event) => {
  const client = useConfigDirectorClient();
  return client.getValue("my-config-key", false);
});

The value returned by the useConfigDirectorValue composable is a ShallowRef that will update whenever the config value is updated in the ConfigDirector dashboard, or if it evaluates to a different value due to targeting rules (for example, if the user context is updated). Keep in mind that it is a ShallowRef when accessing it in scripts:

YourComponent.vue
<script setup lang="ts">
const { value: myConfigValue } = useConfigDirectorValue("my-config", "default value");

const someOtherDerivedValue = computed(() => `Hello ${myConfigValue.value}`);
</script>

<template>
  <div>my-config is : {{ someOtherDerivedValue }}</div>
</template>

You can also determine if the client is still initializing, so rather than transition from the default value to the evaluated value, you can show a loading state until the client is ready and config values are evaluated:

YourComponent.vue
<script setup lang="ts">
const { value, loading } = useConfigDirectorValue("my-config", "default value");
</script>

<template>
  <div v-if="loading">Loading...</div>
  <div v-else>my-config is : {{ value }}</div>
</template>

Additionally, you can also use the useConfigDirectorStatus composable to retrieve just the status:

YourComponent.vue
<script setup lang="ts">
const { loading } = useConfigDirectorStatus();
</script>

<template>
  <div v-if="loading">Loading...</div>
  <div v-else><SomeComponentThatUsesConfigValues /></div>
</template>

SSR config evaluation

Evaluation of config values during server side rendering (SSR) utilizes the global server instance of the ConfigDirector Node.js client. That means there are no additional network requests that take place since the server SDK evaluates targeting rules locally for any given user context.

For SSR to work correctly, configs need to be made available to both, the client and the server, in the ConfigDirector dashboard.

Update the user context

The user context can be updated via the useConfigDirectorContext composable:

YourComponent.vue
<script setup lang="ts">
const { updateContext } = useConfigDirectorContext();
const { value } = useConfigDirectorValue("my-config-key", false);

const onUpdate = () => {
  updateContext({
    id: "654321",
    name: "Another User",
    traits: {
      region: "Australia",
    },
  });
};
</script>

<template>
  <div>my-config-key is : {{ value }}</div>
  <button type="button" @click="onUpdate">Update Context</button>
</template>

On server resource handlers, the user context is provided at evaluation time as the third argument to getValue. Unlike client SDKs, the server SDKs are able to evaluate targeting rules for the given user context locally without additional network calls.

server-resource.get.ts
export default defineEventHandler(async (_event) => {
  const client = useConfigDirectorClient();
  return client.getValue("my-config-key", false, {
    id: "654321",
    name: "Another User",
    traits: {
      region: "Australia",
    },
  });
});
Updating the context on the client side re-establishes a new connection to ConfigDirector servers with the new context. While the new connection is in flight, config values will continue to evaluate to the currently cached values from the prior user context.

useConfigDirectorClient composable

In the event that you need to have access to the underlying Javascript ConfigDirectorClient instance in app components for more complex behaviors, you can access the instance via the useConfigDirectorClient composable:

YourComponent.vue
<script setup lang="ts">
const { client } = useConfigDirectorClient();

// Utilize the client for more involved logic
</script>

<template>
  <div>A component with more complex usage</div>
</template>
Proceed with caution when using the useConfigDirectorClient composable. The useConfigDirectorValue and useConfigDirectorContext composables manage listeners and cleanup automatically. However, if you make use of the useConfigDirectorClient composable, you must manage cleaning up any listeners yourself.
Additionally, any calls to dispose, unwatch, or unwatchAll on the client instance can have unintended side effects and may result in subtle bugs.

Test your components

Use the SDK's testing entry point, @configdirector/nuxt-sdk/testing, to test components and composables that read your configs and flags. It creates a test client: the SDK's real client connected to an in-memory server that your test controls. Only the connection to ConfigDirector is replaced. Everything else is the same code that runs in production, including the plugin and the composables, so your components behave exactly as they do against ConfigDirector, and no network connection is opened and no telemetry is sent.

Tests run in the nuxt environment of @nuxt/test-utils version 4, which creates your Nuxt app inside the test process. installTestClient makes the ConfigDirector plugin provide the test client's client to the app instead of building one:

Checkout.nuxt.spec.ts
import { nextTick } from "vue";
import { mountSuspended } from "@nuxt/test-utils/runtime";
import { createTestClient, installTestClient } from "@configdirector/nuxt-sdk/testing";
import Checkout from "~/components/Checkout.vue";

const testClient = createTestClient({ values: { "new-checkout": true } });
installTestClient(testClient);

beforeEach(() => testClient.replaceValues({ "new-checkout": true }));

test("shows the new checkout when the flag is on", async () => {
  const component = await mountSuspended(Checkout);
  expect(component.text()).toContain("New checkout");

  testClient.setValue("new-checkout", false);
  await nextTick();
  expect(component.text()).toContain("Classic checkout");
});

@nuxt/test-utils 4 creates the app once per test file, in a beforeAll hook that runs before your own hooks, so call installTestClient at the top level of the test file, or of a vitest setupFiles entry, and not inside beforeAll or beforeEach. @nuxt/test-utils 3 creates the app before your test file loads, so the plugin cannot see a client installed there: the testing entry needs version 4, which in turn needs vitest 4. Because the app and its client live for the whole file, reset the values between tests with replaceValues. Await nextTick() after setValue, removeValue, and replaceValues, since they update the composables' refs synchronously and Vue renders on the next tick.

What the plugin does with an installed client

  • It provides the installed client to the app and does not build one, so the app under test needs no clientSdkKey, and the public configdirector runtime config is ignored.
  • It does not call initialize on a client that is ready or initializing. If the client is ready and the app's context differs from the client's, it calls updateContext instead. The status from useConfigDirectorStatus starts as ready for a ready client.
  • It keeps its clientReady handler registered for the app's lifetime, as in production, and never disposes the client.

Server code is not covered: the nuxt environment does not run Nitro, so the server plugin, the server useConfigDirectorClient composable, and your server handlers run only in end-to-end tests. Those start the built app in a separate process, where the SDK connects to the baseUrl from the runtime config as it does in production.

Test client controls

values, setValue, removeValue, and replaceValues take native values, and the config type follows from each value: a boolean, an integral number (integer), any other number (float), a string, or a plain object or array (json). holdInitialization, completeInitialization, and failInitialization drive initialize, so the loading and default statuses can be tested, and holdContextUpdate, completeContextUpdate, and failContextUpdate do the same for updateContext. contextUpdates records the context of every initialize and updateContext call. The JavaScript SDK's testing section describes every control and what to expect from the client.