Spoar

Auth overview

Who can call what, how sign-in works, and the keys and tokens each caller uses.

Every route declares one access level, and the API decides it on each request from the credential it receives.

Callers and credentials

CallerCredentialCan do
AnyonenoneRead public projects' aggregates, the project list and the docs
A visitor's browser, through the SDKX-Project-Key: pk_... or ?key=pk_..., from an origin in the project's allowed originsSend events
Your server, through the SDKAuthorization: Bearer sk_...Send events, forwarding the visitor's user agent and IP for hashing
You, signed inThe session cookie from GitHub sign-inEverything your role allows, including private projects
Scripts, CI and other frontendsAuthorization: Bearer at_..., an API token with scope read, admin or sql and an optional project listWhat the scope allows
The schedulerAuthorization: Bearer <CRON_SECRET>Run the jobs under /v2/admin/jobs

Secret keys and API tokens are shown once, when created or rotated, and stored hashed.

Access levels

LevelWho passes
publicAnyone
projectAnyone when the project is public; otherwise a signed-in member whose role lists the project, or an API token that lists it
detailAn owner, admin or analyst who lists the project, or an API token that lists it; also anyone when the project is public and has publicVisitorData on. Viewers get aggregates only
adminAn owner, an admin for the projects their role lists, or an admin token for its projects. Organization-wide routes such as creating projects and tokens need an owner, or an admin or admin token that lists no projects
ingestA public key from an allowed origin, or a secret key
cronThe cron secret

A private project answers 404, not 403, to callers without access, so its name does not leak. A project the caller can read but not change answers 401 when signed out and 403 otherwise. An unknown or expired at_ token is 401, never treated as anonymous.

Roles

RoleScopeCan
OwnerOrganizationEverything, including members, keys and deleting projects
AdminOrganization or listed projectsSettings, visibility, internal-traffic marking, tokens and SQL
AnalystListed projectsAll reads including visitor-level data, and SQL
ViewerListed projectsAggregate reads only, the same as a public project shows

Sign-in

Sign-in goes through the API, so every frontend shares one access check.

  1. The browser goes to the API's GitHub sign-in route under /v2/auth.
  2. GitHub redirects back to the API. The API checks the GitHub login against the dashboard_users allowlist, stores a session, and sets an httpOnly, Secure, SameSite=Lax cookie for the parent domain.
  3. Server components forward that cookie on API calls; browser calls use credentials: "include". CORS allows credentials only for the configured DASHBOARD_ORIGIN.
  4. GET /v2/auth/session answers who is signed in, their role and isAdmin.
{
  "user": { "id": "usr_01J8Z3", "login": "remcostoeten", "name": "Remco Stoeten", "avatarUrl": "https://avatars.githubusercontent.com/u/57683378" },
  "session": { "expiresAt": "2026-10-27T16:40:01.000Z" },
  "role": "owner",
  "isAdmin": true
}

Signed out, the same route answers { "user": null, "session": null, "role": null, "isAdmin": false }.

Events sent with a signed-in owner's or admin's session cookie are stored as internal traffic, and traffic=human leaves them out. The browser client sends without cookies, so the cookie arrives only through the /_ra proxy or the server client given the incoming request or headers; both forward ra.session_token (or __Secure-ra.session_token) and no other cookie. The site's domain receives that cookie only when AUTH_COOKIE_DOMAIN sets it for a shared parent domain. A browser sending to the API directly is not marked internal this way.

API tokens

Create a token with POST /v2/tokens as an owner or admin, list them with GET /v2/tokens and revoke one with DELETE /v2/tokens/:token. The response to the create call is the only time the token's value is shown.

curl https://api.analytics.remcostoeten.nl/v2/projects/remcostoeten.nl/stats?period=7d \
  -H "authorization: Bearer $RA_TOKEN"

On this page