Setup for coding agents
Everything a coding agent needs to install saasco-sdk, identify users, track sign-up events and check it worked, on one page.
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:
npm install saasco-sdkCreate 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).
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:
"use client";
import { useEffect } from "react";
import { saasco } from "../lib/saasco";
export function SaascoAnalytics() {
useEffect(() => {
saasco.init();
}, []);
return null;
}<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:
<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.
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.
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.
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:
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.
{
"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 Viewat 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.saascois undefined untilinit()has run. Page Viewbut noSigned Up,Signed InorSigned Out: thetrack()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
identifyis 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.1and*.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.oruat.
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.