Spoar

Run your own copy

Deploy the API on your own Postgres database and host, sign in, and schedule the jobs.

This guide runs the whole v2 stack yourself: a Postgres database, the API, and the scheduled jobs. The SDK then sends to your API instead of api.analytics.remcostoeten.nl. It needs Bun 1.3 or later, a Postgres database such as Neon, a host that runs Bun, and a GitHub account for sign-in.

1. Set up the database

Clone the repository, install, and run the setup against an empty database with your GitHub login and, optionally, your first site:

git clone https://github.com/remcostoeten/analytics
cd analytics
bun install
DATABASE_URL=postgres://... bun run setup --owner your-github-login --project my-site --domain example.com

It runs every migration, adds your login to dashboard_users, the allowlist of who may sign in, and creates the project with a public and a secret key. The secret key is printed once; keep it. Running it again skips what is already there and leaves the keys unchanged. --name sets the project's display name, which defaults to the domain. bun run migrate --dry-run lists the migrations without running them.

2. Who may sign in

The first allowed login to sign in owns the organization and the projects created by the setup. Later logins join as viewers of no projects until an owner gives them access. To allow another login, run the setup again with its --owner, or insert it yourself:

INSERT INTO dashboard_users (github_login) VALUES ('their-github-login');

3. Create a GitHub OAuth app

In GitHub, open Settings, Developer settings, OAuth Apps, New OAuth App:

FieldValue
Homepage URLYour API's URL, such as https://analytics-api.example.com
Authorization callback URL<API URL>/v2/auth/callback/github

Keep the client id and generate a client secret.

4. Set the environment

Generate the three secrets once with openssl rand -hex 32, then set the variables below. apps/api/.env.example lists them all:

VariableNeededValue
DATABASE_URLYesThe connection string from step 1
IP_HASH_SECRETYes in productionA generated secret; the API refuses to start in production without it
BETTER_AUTH_SECRETYes in productionA generated secret; signs session cookies
CRON_SECRETFor jobsA generated secret; the Authorization: Bearer value for the job routes
API_URLYes in productionThe API's public URL, as in step 3
GITHUB_CLIENT_ID, GITHUB_CLIENT_SECRETFor sign-inFrom step 3
DASHBOARD_ORIGINNoThe one origin that may call the API with credentials, such as a dashboard
AUTH_COOKIE_DOMAINNoA parent domain such as .example.com, so sites on its subdomains send the admin cookie and your own visits count as internal
MAIL_URL, MAIL_FROMFor mail alertsAn SMTP URL such as smtps://user:password@host:465, and the sender
CRUX_API_KEYNoA Google API key with the Chrome UX Report API, for the weekly speed cross-check
MAXMIND_LICENSE_KEYFor production buildsA free GeoLite2 license key from maxmind.com; the build downloads the geo files with it and checks their checksum

apps/api/README.md lists the optional limits and paths.

5. Run the API

The API is an Elysia app in apps/api. bun run --cwd apps/api build downloads the GeoLite2 City and ASN files into apps/api/data, which country, city and network lookups need.

  • On Vercel: create a project with root directory apps/api. apps/api/vercel.json sets the Bun runtime, the build command and the GeoLite2 files.
  • On any Bun host: run the build, then bun apps/api/src/server.ts. It listens on PORT, 3100 by default.

Check that <API URL>/v2/health answers "ok": true.

6. Sign in and create a project

Open <API URL>/v2/setup and sign in with GitHub. The page lists your projects, creates one with its public and secret key, prints the .env.local block and the two Next files to paste, rotates a secret and edits the allowed origins. New projects accept events from https://<domain> and https://www.<domain>; clear the list to accept any origin. The secret key is shown once.

The first allowed login to sign in becomes the owner. Later logins see their role and a note that an owner must grant access.

7. Schedule the jobs

Four routes keep the data in shape. Call each one with Authorization: Bearer <CRON_SECRET>:

RouteWhenDoes
POST /v2/admin/jobs/rollupDailyScores the previous day's sessions for bots, rolls speed data into daily percentiles, and drops raw speed rows past 30 days
POST /v2/admin/jobs/cleanupDaily, after rollupDeletes events, sessions and speed rows past each project's retentionDays
POST /v2/admin/jobs/alertsEvery 10 minutesSends new issues, regressions and other alerts to the project's targets
POST /v2/admin/jobs/cruxWeekly, optionalCompares speed with the Chrome UX Report

A fork of the repository can use the jobs GitHub workflow: set the API_URL variable and the CRON_SECRET secret on a production environment. Any other scheduler works too, such as cron with curl -X POST -H "Authorization: Bearer $CRON_SECRET" "$API_URL/v2/admin/jobs/rollup".

8. Point the SDK at your API

Behind the proxy, set the proxy's endpoint to your API and its secret to the project's secret key. Without the proxy, set the client's endpoint to your API's /v2/events URL:

import { createAnalytics } from "@spoar/sdk";

const analytics = createAnalytics({
  project: "my-site",
  key: "pk_...",
  endpoint: "https://analytics-api.example.com/v2/events",
});

Add each site's origin to the project's allowedOrigins when the browser sends to the API directly.

On this page