# Setup for coding agents

This page is the whole install. You don't need to read the SDK source in `node_modules` or any other docs page. If the SDK is already installed, change only the gaps.

## 1. Install and call init()

Install `saasco-sdk` with the package manager the lockfile uses:

```bash
npm install saasco-sdk
```

Create one instance and export it. Use the live Project ID. On localhost the SDK sends browser events to the test project by itself (see [Test hosts](#test-hosts)).

```ts title="lib/saasco.ts"
import { Saasco } from "saasco-sdk";

export const saasco = new Saasco({ projectId: "YOUR-PROJECT-ID" });
```

Constructing `Saasco` is safe during server rendering. It only touches `window` behind a browser check, and nothing is tracked until `init()` runs. So this module can be imported from server and client code alike.

Call `init()` once, in the browser, at app startup, on the document people browse. If the root layout also renders a document for embeds or iframes, skip that one. `init()` starts automatic page views and sets `window.saasco` to the instance.

In the Next.js App Router, use a client component and render it once from the root layout of the document people browse:

```tsx title="components/SaascoAnalytics.tsx"
"use client";

import { useEffect } from "react";

import { saasco } from "../lib/saasco";

export function SaascoAnalytics() {
  useEffect(() => {
    saasco.init();
  }, []);

  return null;
}
```

```tsx title="app/layout.tsx"
<body>
  {children}
  <SaascoAnalytics />
</body>
```

A second `init()` (React Strict Mode runs effects twice in development) is ignored. Page views are automatic, including client-side navigations, so don't add router listeners or call `page()`.

In the Pages Router or Vite, call `saasco.init()` at the client entry point (`pages/_app.tsx`, `main.tsx`). Without a build step, use the script tag instead; it calls `init()` for you:

```html
<script
  src="https://www.saasco.com/sdk/saasco-sdk.js"
  data-projectId="YOUR-PROJECT-ID"
  async
></script>
```

## 2. Identify the signed-in user

Stay on the client. The id that client code already uses for the current user is what `identify()` takes. Read server auth only if the client never exposes an id of its own.

One effect is the source of truth: the client component that already reads the signed-in user. When that hook has an id and email, call `identify()`. That covers an existing session, the page after email sign-in, and the page after an OAuth return. Don't add `identify()` beside each sign-in call.

```tsx
useEffect(() => {
  if (!user?.id || !user.email) return;
  saasco.identify(user.id, { email: user.email, name: user.name });
}, [user]);
```

The only extra `identify()` is in the email or password success handler, immediately before `await track()`, because navigation can leave the page before the event is sent. The same id a second time is fine. A third call site is not.

Repeat calls with the same ID and properties are skipped, so calling it from an effect that re-runs is fine. Send `email` so the contact can receive email. Don't send tokens, passwords or whole session objects.

## 3. Track Signed Up, Signed In and Signed Out

Track each action in one place. Skip dev-only logins. Use these names so the setup check can see them. Don't add other events yet.

Email or password, when the result includes `user.id` and `user.email`: identify, `await track("Signed Up")` or `await track("Signed In")`, then navigate. Don't also queue that event.

```ts
saasco.identify(user.id, { email: user.email });
await saasco.track("Signed Up"); // or "Signed In"
```

Don't add a missing-id fallback on email unless the response omitted the id. Don't open `node_modules` or the SDK source to confirm the shape.

OAuth, or any call that leaves the origin before a user id exists: store one event name before the redirect. Flush it from the same effect that identifies, once the id is known. Use Signed Up on the sign-up page and Signed In on the sign-in page, unless the return says the account was just created.

Write one helper that awaits `track("Signed Out")` and then calls `logout()`. Call it from each button labeled Sign out or Log out, and keep that button's existing sign-out. Leave `signOut` calls whose label is not sign-out alone. Search those labels and submit handlers. Ignore tests, CLI packages, and server routes.

```ts
async function trackSignedOut() {
  await saasco.track("Signed Out");
  saasco.logout();
}
```

`logout()` clears the user, session and anonymous IDs so the next person on the device starts fresh. A server `track()` stays on the live project, because the test-host check only runs in the browser.

## 4. Check it worked

Sign-up already starts a session, and that session only counts as Signed Up. Signed In is a separate event and only comes from signing in afterward.

Run the dev server. If a session is already signed in, sign out through the account or user menu first.

Create a new account with email and password at a new `@example.com` address. Skip dev-only logins. A signed-in page is enough. Don't finish onboarding or other product setup.

Sign out. If that page has no sign-out control, open the account page and use Log out.

Sign in with the same account.

Check progress once:

```bash
curl -s "https://www.saasco.com/api/setup/progress?projectId=YOUR-PROJECT-ID"
```

Ignore framework error overlays unless a required event is still missing. When `recentTestEvents` contains Page View, Signed Up, Signed Out, and Signed In, stop. Don't wire extra auth entry points after that.

If one of those is missing, wait 2 seconds and check once more. Then use the `recentTestEvents` diagnostics below.

```json
{
  "identify": true,
  "recentTestEvents": [
    { "action": "Signed In", "timestamp": "2026-10-08 01:02:03" },
    { "action": "Signed Out", "timestamp": "2026-10-08 01:01:58" },
    { "action": "Signed Up", "timestamp": "2026-10-08 01:01:40" },
    { "action": "Page View", "timestamp": "2026-10-08 01:01:12" }
  ],
  "sdk": true,
  "started": true,
  "track": true
}
```

| Field              | True when                                                                          |
| ------------------ | ---------------------------------------------------------------------------------- |
| `sdk`              | Any event, such as the first `Page View`, reached the project or its test project. |
| `identify`         | An `identify()` with a user ID created a contact.                                  |
| `track`            | A `Signed Up` event arrived.                                                       |
| `recentTestEvents` | The last 10 event names and times in the test project, newest first.               |

This check is the verification. You don't need the browser console.

Read `recentTestEvents`:

- **No `Page View` at all**: `init()` isn't running in the browser. Check that the client component is rendered on the document people browse, and skipped on a second root-layout document used for embeds or iframes. `window.saasco` is undefined until `init()` has run.
- **`Page View` but no `Signed Up`, `Signed In` or `Signed Out`**: the `track()` call isn't reached. It may sit in code that only runs on the server, or a redirect left the page before the id existed and the pending event was never flushed.
- **Events arrive but `identify` is false**: `identify()` wasn't called with a user ID, or it ran before the user was known.

The booleans stay true once any matching event has arrived, including events from before this setup. Use `recentTestEvents` and the timestamps to see what your own test just sent.

## Test hosts

In the browser, the SDK sends to the test project, `test_` plus the Project ID, on:

- `localhost`, `127.0.0.1` and `*.localhost`
- Vercel preview URLs (`-git-` branch URLs and deployment-hash URLs), Netlify `--` deploys, Cloudflare Pages branch subdomains, and Render `-pr-<n>` services
- Subdomains starting `staging.`, `stage.`, `preview.`, `dev.`, `test.`, `qa.` or `uat.`

Every other host is live. Do not set `test` on the shared instance. Left on, it would send production traffic to the test project.

## After setup

The install prompt lists the apps this project has on, and a Further setup line for what those apps still need. Ask the user whether they want you to look through the project and implement any of that. Don't search the codebase until they say yes. If the prompt has no Further setup line, stop.
