Print a starter server for Workers, Vercel, Bun or Node, then deploy it like any small app.
npx analytik init --target workerPaste it into the <head> of every page. It sets no cookies.
<script defer src="https://analytics.example.com/js/analytik.js" data-site="my-site" data-mode="strict"></script>Embed it in any React app and sign in with your admin password.
import { Dashboard } from 'analytik/dashboard'
import 'analytik/dashboard/styles.css'
export const Admin = () => <Dashboard api="https://analytics.example.com/api" /><script defer src="https://analytics.example.com/js/analytik.js" data-site="my-site" data-mode="strict"></script>Let your coding assistant deploy Analytik. Pick where it runs; the prompt uses the host and mode from More options.
# Set up the Analytik server on Cloudflare Workers + D1
## Goal
Deploy analytik (privacy-first, self-hosted web analytics, npm package `analytik`) as a Cloudflare Worker with a D1 database, serving the tracker as a static asset. It must answer at https://analytics.example.com: the API under `/api` and the tracker script at `/js/analytik.js`. Work in a new, separate project, not inside the website that will be tracked.
## Install
```sh
npm install analytik hono && npm install -D wrangler @cloudflare/workers-types
```
Analytik is ESM only and needs Node 22.19+ or Bun 1.3+ when it runs outside Workers. `hono` is a peer dependency.
## Code
`src/worker.ts`:
```ts
import { createAnalytik, type Analytik } from 'analytik/server'
import { d1Storage } from 'analytik/storage/d1'
interface Env {
DB: D1Database
ADMIN_PASSWORD: string
SESSION_SECRET: string
}
// Build the app once per isolate, not once per request.
let analytik: Analytik | undefined
export default {
fetch(request: Request, env: Env, ctx: ExecutionContext) {
analytik ??= createAnalytik({
storage: d1Storage(env.DB),
admin: { password: env.ADMIN_PASSWORD, secret: env.SESSION_SECRET },
privacy: { mode: 'strict' },
})
return analytik.app.fetch(request, env, ctx)
},
}
```
`wrangler.jsonc`:
```jsonc
{
"name": "analytik",
"main": "src/worker.ts",
"compatibility_date": "2026-10-01",
// Only API calls run the Worker; analytik.js is a free static asset.
"assets": { "directory": "./public", "run_worker_first": ["/api/*"] },
"d1_databases": [{ "binding": "DB", "database_name": "analytik", "database_id": "<from wrangler d1 create>" }],
"routes": [{ "pattern": "analytics.example.com", "custom_domain": true }]
}
```
## Deploy
1. `npx wrangler d1 create analytik` and put the printed `database_id` into `wrangler.jsonc`.
2. `mkdir -p public/js && cp node_modules/analytik/dist/analytik.js public/js/analytik.js` (repeat after every package upgrade).
3. `npx wrangler secret put ADMIN_PASSWORD` and `npx wrangler secret put SESSION_SECRET`: `ADMIN_PASSWORD` (the dashboard password, not empty) and `SESSION_SECRET` (`openssl rand -hex 32`, at least 32 characters). For local runs put both in `.dev.vars`.
4. `npx wrangler deploy`. The schema is created on the first request (autoMigrate).
Client IP and location come from Cloudflare automatically (`cf-connecting-ip`, `request.cf`).
## Privacy mode
The default mode for new sites is `strict` (each site can override it). Strict (default): the tracker stores nothing on the device and the server never stores the IP. The visitor id is a salted hash that changes every month. It is designed so no cookie banner is needed (this is a design goal, not legal advice). `identify()` is ignored.
## Create the first site
```sh
curl -c cookies.txt -X POST https://analytics.example.com/api/auth/login -H 'Content-Type: application/json' -d '{"password":"<ADMIN_PASSWORD>"}'
curl -b cookies.txt -X POST https://analytics.example.com/api/sites -H 'Content-Type: application/json' -d '{"name":"My site","domain":"example.com","currency":"EUR"}'
```
Use the real domain of the tracked website (events from other hosts are rejected). The response contains `site.id` (the value for `data-site`) and `apiKey` (for server-side payments and goals, shown once).
## Verify
1. `curl https://analytics.example.com/api/health` returns `{"ok":true,...}`.
2. `curl -I https://analytics.example.com/js/analytik.js` returns 200 with a JavaScript content type.
3. `npx analytik doctor --url https://analytics.example.com` reports no problems.
4. A test event is accepted with 202: `curl -i -X POST https://analytics.example.com/api/event -H 'Content-Type: text/plain' -H 'Origin: https://example.com' -d '{"w":"<site id>","t":"pageview","u":"https://example.com/"}'` (use the site's domain).
## Rules
- Never commit secrets; read them from the platform environment.
- Do not add cookies, fingerprinting or third-party scripts on top of what Analytik does.
- Keep the API under `/api`; the tracker posts to `/api/event` on the origin of the script by default.
- Stop and tell me if a step needs an account, a paid plan or DNS access you do not have.
Let your coding assistant add the tracker to your site, in the right file for your framework.
# Add Analytik tracking to your Plain HTML site
## Goal
Add the Analytik tracker (privacy-first, self-hosted web analytics) to this Plain HTML site so every page sends pageviews to https://analytics.example.com. The analytics server is already running; only the website changes. Site id: `my-site`, privacy mode: `strict`. If the site id looks like a placeholder, ask me for the real one.
## The snippet
```html
<script defer src="https://analytics.example.com/js/analytik.js" data-site="my-site" data-mode="strict"></script>
```
## Where to put it in Plain HTML
Paste the tag into the `<head>` of every page, or once into the shared header include if the site has one.
`index.html (every page)`:
```html
<script defer src="https://analytics.example.com/js/analytik.js" data-site="my-site" data-mode="strict"></script>
```
Add it once, site wide. Do not self-host or bundle the script, and do not add a second analytics snippet for the same site.
## Privacy
- Strict mode stores nothing on the device: no cookie, no localStorage, no sessionStorage. The IP is used for a salted hash and never stored.
- By design no cookie banner is needed for Analytik in this mode, so do not put it behind a consent manager and do not list a cookie for it (not legal advice; the site owner decides).
- Do not call `analytik.identify()`: strict mode ignores it. Identify people only with their consent, on a site switched to enriched or persistent mode.
## Goals and revenue
Pick the events that matter for this site (sign-ups, checkouts) and add them:
```html
<!-- Declarative: a click on this element sends the goal "signup" with the prop plan=pro. -->
<button data-analytik-event="signup" data-analytik-plan="pro">Start free trial</button>
```
```js
window.analytik?.track('signup', { plan: 'pro' })
// Revenue in major units; orderId de-duplicates repeats (for example a reloaded thank-you page).
window.analytik?.revenue('purchase', 49, { currency: 'EUR', orderId: 'order_1042' })
```
For payments confirmed on the server (Stripe, Dodo, Lemon Squeezy or your own backend), prefer `POST https://analytics.example.com/api/v1/payments` with the site API key, so revenue is recorded even when the visitor closes the tab. To call `analytik` before the script has loaded, add the queue stub first: `window.analytik = window.analytik || function () { (window.analytik.q = window.analytik.q || []).push(arguments) }`.
## Verify
1. Open the site in a normal browser window (the tracker ignores automated browsers) with ad blockers off. The first pageview is sent after the first interaction or 3 seconds of visibility.
2. In DevTools > Network, filter for `event`: a `POST https://analytics.example.com/api/event` request returns **202**. 403 means the page's host is not the site's domain (on localhost add `data-allow-localhost` and enable allowLocalhost for the site); 404 means the site id is wrong.
3. Open the Analytik dashboard: the visit shows up in Realtime within a few seconds.
4. If you added goals or revenue, trigger one and check that it appears under Events or Revenue.