# Install StepsKit in a Nuxt app

> Add StepsKit to a Nuxt app with the stepskit npm package in a client-only plugin. Covers why the .client suffix is required, identifying users from a session composable, and CSP for server-rendered Vue.

*Source: https://stepskit.com/docs/install/nuxt*

For Nuxt 3 and 4. Nuxt renders on the server by default, which is the one thing
that makes this install different from [plain Vue](/docs/install/vue.md) — get it
wrong and your app crashes during SSR rather than failing quietly.

## Prerequisites

- Your project API key, from the **Integrations** page in the
  [dashboard](/app).
- Nuxt 3 or 4.

## 1. Install the package

```bash
npm install stepskit
```

Create `plugins/stepskit.client.ts`:

```ts
// plugins/stepskit.client.ts
import stepskit from "stepskit";

export default defineNuxtPlugin(() => {
  stepskit.init("YOUR_API_KEY");
});
```

**The `.client` suffix is not optional.** Nuxt runs plugins on both the server
and the client, and only the client should be loading a browser SDK. Nuxt reads
the suffix from the filename — there is no config to set.

Nuxt auto-registers anything in `plugins/`, so there is nothing else to wire up.

The package's call queue is doing real work here: the engine is fetched
asynchronously, so an `identify()` call made before it arrives would otherwise
be dropped on the floor. The package buffers those calls and replays them the
moment the engine loads — which is what makes step 2 safe to write without
worrying about ordering.

### Without the package: `nuxt.config.ts`

If you can't add a dependency, or you'd rather keep this out of `plugins/`:

```ts
// nuxt.config.ts
export default defineNuxtConfig({
  app: {
    head: {
      script: [
        {
          src: "https://cdn.stepskit.com/stepskit.latest.js",
          async: true,
          "data-api-key": "YOUR_API_KEY",
        },
      ],
    },
  },
});
```

This emits the tag into the server-rendered HTML, so it starts loading
fractionally earlier. The trade-off is that there's **no queue stub** — an
`identify()` call that runs before the SDK arrives is silently dropped. If you
take this route, pass user data as `data-user-*` attributes on the same tag
instead of calling `identify()`.

## 2. Identify the user

**This is half the install, not a nice-to-have.** Without an `id`, show-once
capping degrades to a per-tab check and any tour with a "filtered" audience is
hidden outright.

The cleanest spot is the same client plugin, below the loader you pasted in step
1, watching whatever composable your auth library exposes:

```ts
// plugins/stepskit.client.ts
import stepskit from "stepskit";

export default defineNuxtPlugin(() => {
  stepskit.init("YOUR_API_KEY");

  // `useUserSession` here is nuxt-auth-utils; swap in whatever your app uses
  // (`useSupabaseUser`, a Pinia store, your own composable).
  const { user } = useUserSession();

  watch(
    user,
    (current) => {
      if (!current) return;
      stepskit.identify({
        id: current.id,
        email: current.email,
        plan: current.plan,
      });
    },
    { immediate: true },
  );
});
```

`{ immediate: true }` covers the case where the session is already resolved when
the plugin runs; the watcher covers sign-in afterwards.

Pass whatever your user object already carries — every key becomes targetable,
and there's no fixed schema to conform to. Re-call it when the user switches
workspace or changes plan, so what you target on doesn't go stale.

## 3. Verify it worked

Load any page of your app, then:

1. **Dashboard** — the project's Integrations page flips to **Connected**.
2. **Console, on a signed-in page** — run
   `window.stepskit.validateEnvironment()`. You want a non-null `visitorId` and
   a populated `userAttributes`. A null `visitorId` means step 2 never ran.
3. **Network** — `cdn.stepskit.com/stepskit.latest.js` returns `200`.

`validateEnvironment()` also returns a `toursFiltered` table that says exactly
why any given tour isn't showing.

## Nuxt notes

**Route changes need no extra wiring.** StepsKit patches `history.pushState`
and `replaceState` and listens for `popstate`, so it sees Vue Router navigation
on its own. You don't need `useRouter().afterEach()` or the `page:finish` hook.

**Guarding other browser-only code.** If you add StepsKit calls outside a
`.client` plugin — in a composable or a component that also renders on the
server — guard them:

```ts
if (import.meta.client) {
  stepskit.identify({ id: user.id });
}
```

**Attributes don't survive a full page load.** Client-side navigation is fine —
StepsKit keeps user context across it. But a hard reload starts clean, which is
why `identify` belongs in a plugin that runs on every page load rather than in
one route's setup.

**Content Security Policy.** Only relevant if your app sets one (commonly via
the `nuxt-security` module or Nitro route rules):

- `script-src`: `https://cdn.stepskit.com`
- `connect-src`: `https://stepskit.com`
- `style-src`: `'unsafe-inline'` — **required**. The embed injects its entire
  stylesheet as a `<style>` element when the script evaluates. Without this the
  tour engine renders completely unstyled rather than merely off-brand.
- `img-src`: `https://*.supabase.co` — only if your steps use images.

Because the plugin appends an external `<script src>` rather than running an
inline block, you do **not** need `script-src 'unsafe-inline'`.

## Troubleshooting

**`document is not defined` / 500 on first render.** Your plugin is missing the
`.client` suffix. Rename it to `plugins/stepskit.client.ts`.

**Dashboard still says "Not connected".** Check the plugin file is directly in
`plugins/` (Nuxt only auto-registers the top level by default) and that the page
you loaded actually rendered client-side.

**Connected, but no tour plays.** Usually identity. Run
`validateEnvironment()`: if `visitorId` is null, your watcher isn't firing —
check that the composable actually resolves a user on that page.

**Tour renders as unstyled text.** Your CSP is missing
`style-src 'unsafe-inline'`.

**`Property 'stepskit' does not exist on type 'Window'`.** Add the ambient
declaration from the [API reference](/docs/api.md#typescript) to your project —
anywhere your `tsconfig` already includes.

## Next steps

- [JavaScript API reference](/docs/api.md) — every method on `window.stepskit`.
- [User attributes](/docs/concepts/user-attributes.md) — what you can target on.
- [Frequency capping](/docs/concepts/frequency-capping.md) — how "show once" works.
