# Install StepsKit in a Vue app

> Add StepsKit to a Vue 3 app built with Vite or Vue CLI. Install the stepskit npm package, identify users from your auth store, and keep URL targeting working with Vue Router.

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

For Vue 3 single-page apps built with Vite or Vue CLI. Using
[Nuxt](/docs/install/nuxt.md)? It renders on the server, so it needs a different
install — go there instead.

## Prerequisites

- Your project API key, from the **Integrations** page in the
  [dashboard](/app).
- Vue 3. Vue 2 works too — the package is framework-agnostic — but the file
  paths below assume Vue 3.

## 1. Install the package

```bash
npm install stepskit
```

Then call `init()` once, **first**, in `src/main.ts`:

```ts
import stepskit from "stepskit";

stepskit.init("YOUR_API_KEY");

import { createApp } from "vue";
import App from "./App.vue";
import router from "./router";

createApp(App).use(router).mount("#app");
```

Keep it above the Vue imports. It runs at module-evaluation time, so StepsKit
fires once before Vue mounts — nothing to put in a lifecycle hook, and nothing
that can run twice.

If your lint rules forbid statements between imports, move the call into its
own `src/stepskit.ts` and side-effect-import that file first:

```ts
import stepskit from "stepskit";

// Call once, at your app's entry point. SSR-safe and idempotent.
stepskit.init("YOUR_API_KEY");
```

That ordering matters because of the call queue. The engine loads
asynchronously, so an `identify()` call made while your app is booting would
otherwise arrive before there's anything to receive it. The package buffers
those calls and replays them in order when the engine is ready.

### Vue CLI instead of Vite

The entrypoint is `src/main.js` rather than `src/main.ts`; everything else is
identical. Note that Vue CLI's HTML shell lives at `public/index.html`, whereas
Vite's is `index.html` at the project root — this trips people up when they
follow the [script tag guide](/docs/install/js.md) instead.

## 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.

Call `identify` wherever your app resolves the signed-in user — a Pinia store
action, your login handler, or a router navigation guard:

```ts
import { defineStore } from "pinia";
import stepskit from "stepskit";

export const useAuthStore = defineStore("auth", {
  actions: {
    async signIn(credentials) {
      this.user = await api.login(credentials);

      stepskit.identify({
        id: this.user.id,
        email: this.user.email,
        plan: this.user.plan,
      });
    },
  },
});
```

Call it **once per page load**, as soon as auth resolves — not on every route
change. Pass whatever your user object already carries; every key you pass
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.

If your app has signed-out routes, just skip `identify` there. Tours aimed at
everyone still play.

## 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.

## Vue 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 to call `refresh()` in an `afterEach`
hook.

**Hash-mode routing breaks URL targeting.** StepsKit compares the URL
*pathname*, and deliberately ignores hash and query changes so a tour isn't torn
down by unrelated churn. With `createWebHashHistory()` every route shares the
same pathname (`/`), so a tour scoped to a URL pattern will never match the
route you meant. Use `createWebHistory()` if you target tours by URL.

**No SSR concerns.** A Vite or Vue CLI SPA has no server render — and `init()`
is a no-op on the server anyway, so it's safe either way.

**Content Security Policy.** Only relevant if your app sets one:

- `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 package appends an external `<script src>` rather than running an
inline block, you do **not** need `script-src 'unsafe-inline'`. If your CSP uses
nonces, pass one: `stepskit.init(key, { nonce })`.

## Troubleshooting

**Dashboard still says "Not connected".** `init()` never ran. Check that it's
actually reached in `src/main.ts` — and if you put it in its own
`src/stepskit.ts`, that the side-effect import has no named bindings, or a
bundler may tree-shake it away.

**Connected, but no tour plays.** Usually identity. Run
`validateEnvironment()`: if `visitorId` is null, `identify()` isn't being
called. If it's set, read the `toursFiltered` table for the reason.

**Tour plays on the wrong route, or never on the right one.** You're likely on
hash-mode routing — see above.

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

**`Property 'stepskit' does not exist on type 'Window'`.** You're reading the
global instead of the typed import — use `import stepskit from "stepskit"`. If
you deliberately installed without the package (see below), add the ambient
declaration from the [API reference](/docs/api.md#typescript) to `src/`; Vite picks
up any `.d.ts` under `src/` automatically.

## Without the package

If you can't add a dependency, save this as `src/stepskit.ts` instead and
side-effect-import it first from `src/main.ts`:

```ts
// Queue API calls made before the SDK finishes loading.
const QUEUED = [
  "identify",
  "setUserAttributes",
  "track",
  "refresh",
  "playTour",
  "stopTour",
  "dismissAnnouncement",
  "on",
  "off",
  "destroy",
  "validateEnvironment",
];

const w = window as unknown as { stepskit?: Record<string, unknown> };
const stepskit = (w.stepskit ??= {});
stepskit._q ??= [];

for (const method of QUEUED) {
  stepskit[method] ??= (...args: unknown[]) =>
    (stepskit._q as unknown[]).push([method, ...args]);
}

// Load the StepsKit SDK asynchronously.
const script = document.createElement("script");
script.async = true;
script.src = "https://cdn.stepskit.com/stepskit.latest.js";
script.dataset.apiKey = "YOUR_API_KEY";
document.head.appendChild(script);

// Marks this file a module, so the consts above stay local to it.
export {};
```

For a plain JavaScript project, name it `src/stepskit.js` and delete the three
type annotations — `as unknown as { stepskit?: Record<string, unknown> }`,
`as unknown[]`, and `: unknown[]`. The rest is valid JavaScript as written.

## 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.
