Self-hosting on Cloudflare

Run your own copy on your own Cloudflare account with a Self-hosted License.

A License lets you run your own copy of Supersaas Analytics on your own Cloudflare account. The copy has no Trial, no billing, no Quota and no limits on Sites or Members, because you pay for the infrastructure yourself.

The License key is checked offline when the Worker starts. Your copy never calls our servers.

What you need

  • Your License key, from the License email or from My licenses on the account you bought it with.
  • Access to the private repository. Accept the GitHub invitation that Polar sends after the purchase.
  • A Cloudflare account with Workers. The free plan is enough to start, but D1 and Workers have daily limits on it.
  • Node 22 or later, and pnpm.
  • A Resend account and a verified sending domain, so people can verify their email and sign in.

1. Get the code

git clone git@github.com:<owner>/supersaas-data.git
cd supersaas-data
pnpm install
npx wrangler login

2. Create the resources

npx wrangler d1 create supersaas-data-db
npx wrangler kv namespace create supersaas-data-kv
npx wrangler kv namespace create supersaas-data-cache
npx wrangler r2 bucket create supersaas-data

Put the ids these commands print into wrangler.jsonc:

  • d1_databases[0].database_id. Leave migrations_table and migrations_dir as they are.
  • kv_namespaces: the ids for KV and CACHE.
  • r2_buckets[0].bucket_name, if you named the bucket differently.

The KV namespaces and the R2 bucket are not used by any feature yet, but the Worker expects their bindings.

3. The cron

triggers.crons in wrangler.jsonc wakes the Worker every day at 00:10 UTC to roll up the previous day, at 00:40 UTC to write and email the Digests, and hourly for the search sync. Leave it as it is: it must match nitro.scheduledTasks in nuxt.config.ts, and without it, past days show no data.

4. Settings

Add these to vars in wrangler.jsonc:

"vars": {
    "DEPLOYMENT_MODE": "selfhosted",
    "NUXT_AUTH_URL": "https://analytics.example.com",
    "NUXT_EMAIL_FROM": "Analytics <analytics@example.com>",
    "NUXT_PUBLIC_TRACKER_ORIGIN": ""
}

Then add the secrets:

npx wrangler secret put LICENSE_KEY
npx wrangler secret put NUXT_AUTH_SECRET          # openssl rand -base64 32
npx wrangler secret put RESEND_API_TOKEN
npx wrangler secret put NUXT_ANTHROPIC_API_KEY    # optional, see below
  • DEPLOYMENT_MODE must be selfhosted. Without it, the copy behaves as our Hosted service, with Trials and billing.
  • LICENSE_KEY is your License key. Without a valid key, the copy shows only a setup page, and its API and /collect answer 503.
  • NUXT_AUTH_SECRET signs sessions. Changing it signs everyone out.
  • NUXT_AUTH_URL is the copy's public origin. Email links (Digests included) and OAuth callbacks use it.
  • RESEND_API_TOKEN and NUXT_EMAIL_FROM send email. Without them, emails are only printed to the Worker logs, so nobody can verify their email.
  • NUXT_ANTHROPIC_API_KEY is your own Anthropic API key. The AI features use it (Ask AI, and the summary on top of each Digest), and they are hidden when it is not set. Its usage is billed to your Anthropic account.
  • Google and GitHub sign-in are optional: see NUXT_AUTH_GOOGLE_* and NUXT_AUTH_GITHUB_* in .env.example in the repository.

5. Deploy

pnpm run deploy

Use pnpm run deploy, not pnpm deploy, which is a pnpm built-in. The script builds the Worker, applies new migrations to D1, then deploys. The first deploy creates every table.

The Worker has no workers.dev URL ("workers_dev": false), so before the first deploy uncomment routes in wrangler.jsonc and set the pattern to your domain, whose zone must be on the same Cloudflare account.

Open your domain and sign up. The first account is yours; the admin area of our Hosted service does not exist on a Self-hosted copy.

To check the copy:

  • curl -i https://analytics.example.com/api/deployment shows "license":"valid".
  • curl -i https://analytics.example.com/sd.js answers 200.

Search Console (optional)

Search Console data is read with OAuth on behalf of the person who connects. Your copy needs a Google OAuth client with the read-only https://www.googleapis.com/auth/webmasters.readonly scope.

  1. In the Google Cloud console, choose or create the project. The one that holds the "Sign in with Google" client works.
  2. Enable the Google Search Console API under APIs & Services → Library.
  3. Under Google Auth Platform → Data access, add the scope .../auth/webmasters.readonly.
  4. Under Clients, create a Web application client, or open the sign-in one, and add the redirect URI <origin>/api/search-connections/google/callback.
  5. Set NUXT_SEARCH_CONSOLE_GOOGLE_CLIENT_ID and NUXT_SEARCH_CONSOLE_GOOGLE_CLIENT_SECRET. Left empty, the sign-in client (NUXT_AUTH_GOOGLE_CLIENT_ID and NUXT_AUTH_GOOGLE_CLIENT_SECRET) is used.

webmasters.readonly is a sensitive scope, so submit the app for verification early: it takes days to weeks. Until Google verifies it, only the test users listed under Audience can connect, and they see an "unverified app" warning.

Refresh tokens are stored encrypted with a key derived from NUXT_AUTH_SECRET. Rotating that secret means every Site has to connect Search Console again.

Updating

Updates arrive in the private repository. Keep your wrangler.jsonc changes in a commit of their own, so they merge cleanly:

git pull
pnpm install
pnpm run deploy

Migrations are applied before the new Worker goes live. Read the release notes before a large update.

Troubleshooting

  • Only the setup page shows: LICENSE_KEY is missing or was not copied whole. The setup page says which. Set the secret again; wrangler secret put redeploys the Worker.
  • Trials or billing show: DEPLOYMENT_MODE is not selfhosted. Check vars and deploy again.
  • Past days are empty but today has data: the cron has not run. Check Workers → supersaas-data → Settings → Triggers.
  • No verification email arrives: RESEND_API_TOKEN is not set, or NUXT_EMAIL_FROM is not on a domain verified in Resend.