# Install StepsKit in an Angular app

> Add StepsKit to an Angular app with the stepskit npm package, called once from main.ts. Covers why angular.json scripts cannot load a CDN URL, SSR guards, and identifying users from your AuthService.

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

For Angular single-page apps, standalone or NgModule-based. If your app uses
Angular SSR, read the [server rendering](#server-rendering) note before you
start.

## Prerequisites

- Your project API key, from the **Integrations** page in the
  [dashboard](/app).
- Angular 15 or newer. The package itself has no Angular dependency; only the
  file paths below assume a standard CLI project.

## Don't use `angular.json`

The instinct is to add the CDN URL to `architect.build.options.scripts[]`. That
doesn't work: that array is for **local files that get bundled into your
output**, not remote URLs. Angular will try to resolve
`https://cdn.stepskit.com/...` from disk and fail the build.

Use one of the two mechanisms below instead.

## 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 { bootstrapApplication } from "@angular/platform-browser";
import { AppComponent } from "./app/app.component";
import { appConfig } from "./app/app.config";

bootstrapApplication(AppComponent, appConfig);
```

Keep it above the Angular imports. It runs at module-evaluation time, so
StepsKit fires once before Angular bootstraps — no `APP_INITIALIZER`, no
component lifecycle hook, nothing that can run twice.

That ordering matters because of the call queue. The engine loads
asynchronously, so an `identify()` call made while your app is bootstrapping
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.

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");
```

### 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`. There's no IIFE and no wrapper:
an ES module is already a private scope, so nothing leaks onto `window`.

```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 {};
```

The [HTML snippet](/docs/install/js.md) in `src/index.html` before `</body>` also
works, and keeps the loader out of your bundle entirely. Prefer it if your app
already loads its other third-party scripts that way.

## 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` from wherever your app resolves the signed-in user — typically
an `AuthService`:

```ts
import { Injectable, signal } from "@angular/core";

import stepskit from "stepskit";

@Injectable({ providedIn: "root" })
export class AuthService {
  readonly user = signal<User | null>(null);

  async signIn(credentials: Credentials): Promise<void> {
    const user = await this.api.login(credentials);
    this.user.set(user);

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

Call it **once per page load**, as soon as auth resolves — not on every
`NavigationEnd`. 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.

If auth resolves during bootstrap rather than on an explicit sign-in, call
`identify` from wherever that resolution completes. The queue stub means you
never have to worry about being too early.

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

## Angular notes

**Route changes need no extra wiring.** StepsKit patches `history.pushState`
and `replaceState` and listens for `popstate`, so it sees Angular Router
navigation on its own. You don't need to subscribe to `Router.events` and call
`refresh()`.

**`HashLocationStrategy` 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. Under hash routing every route shares the same pathname
(`/`), so a tour scoped to a URL pattern will never match the route you meant.
Use `PathLocationStrategy` — the default — if you target tours by URL.

### Server rendering

If your app uses Angular SSR, `src/main.ts` still only runs in the browser, so
the module import above is safe as written. But any code **you** add that
touches `window` — including an `identify()` call in a service that also runs
server-side — must be guarded:

```ts
import { PLATFORM_ID, inject } from "@angular/core";
import { isPlatformBrowser } from "@angular/common";

const isBrowser = isPlatformBrowser(inject(PLATFORM_ID));
if (isBrowser) {
  stepskit.identify({ id: user.id });
}
```

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

Angular's own `ngCspNonce` covers Angular's inline styles, not StepsKit's — you
still need `style-src 'unsafe-inline'`. Because the loader appends an external
`<script src>` rather than running an inline block, you do **not** need
`script-src 'unsafe-inline'`.

## Troubleshooting

**Build fails resolving `https://cdn.stepskit.com/...`.** You added the URL to
`angular.json` `scripts[]`. See the top of this page.

**Dashboard still says "Not connected".** The loader never ran. Check that
`import "./stepskit"` is in `src/main.ts` and has no named bindings — a
side-effect-only import must stay side-effect-only or it can be tree-shaken.

**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
`HashLocationStrategy` — see above.

**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 `src/`, and make
sure your `tsconfig.app.json` `include` covers it.

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