Runs entirely on the Cloudflare edge — no cookies, no external database,
and no cross-session identity to leak.
Facet is a self-hosted analytics platform that runs 100% on Cloudflare Workers + D1 — no external
database, no long-running server, one wrangler deploy. It measures your site by facet: pages,
referrers, countries, devices, channels, sessions, goals, funnels, and experiments. It is
cookieless and GDPR-friendly by construction: unique visitors are counted with a
daily-rotating, salted SHA-256 hash, raw IP addresses are never stored, and there is no
cross-session identity to leak. The browser client is a drop-in for umami — existing sites migrate
by swapping a single script tag.
- Single deploy. One Worker serves ingest, the stats API, the dashboard, and cron rollups.
- No database to run. State lives in Cloudflare D1; sessions and rollups are materialized by an hourly cron.
- Cookieless & GDPR-friendly. Privacy-safe uniques via
SHA-256(ip + user_agent + daily_salt + site_id); no cookies, no cross-session identity, no raw IP stored. - umami-compatible client.
window.umami.track(name, props), auto-pageviews, SPA navigation, UTM capture, and form-submission tracking work out of the box. - Sessions & engagement. Bounce rate, pages/session, and average visit duration.
- Traffic channels. Automatic direct / referral / organic / social / paid / email classification from referrer + UTM.
- Goals, conversions & funnels. Define goals and multi-step funnels; get conversion rates and in-order funnel drop-off.
- A/B experiments & feature flags. Privacy-first, client-side variant assignment; two-proportion significance testing in the dashboard.
- Anomaly detection & autopsy. Automatic z-score detection with a plain-language root-cause summary (largest-contributing segment).
- Ask in plain English. Natural-language queries over your stats via Workers AI, translated to a constrained, safe query intent.
- Realtime. Active-visitor snapshot over a 5-minute window (distinct daily hashes; no cookies or persistent id).
- Ad-block-resilient. First-party
POST /api/eventserver-to-server ingest — no client script to block. - Visitor opt-out & Do-Not-Track. Honors browser DNT and a per-visitor opt-out; ignored visitors are never recorded.
- CSV / JSON export. Export any series or breakdown from the API or dashboard; CSV is spreadsheet formula-injection-safe.
- In-dashboard admin. A Settings tab manages sites and API keys, with one-click multi-site switching.
- Verifiable trust & provenance. Optional signed statements about the deployment — published keys (
did:web+ JWKS), a W3C VC 2.0 privacy attestation, a RATS build/config evidence EAT, and a SCITT transparency log — with hardware-rootable signing keys. Seedocs/trust.md. - Free, self-issued API keys and unlimited, first-class multi-site.
A visitor is identified for one UTC day only by SHA-256(ip ⧊ user_agent ⧊ daily_salt ⧊ site_id),
rendered as lowercase hex. The salt rotates at UTC midnight, so the same person produces a different
hash the next day and cross-day re-identification is cryptographically prevented. The raw IP is used
only to compute that hash in memory and is never stored, logged, or returned. See
docs/privacy.md for the full model.
One Cloudflare Worker is the whole backend — ingest, the stats API, the dashboard assets, and the scheduled rollups all run in it. State lives in D1; there is no server to operate.
browser beacon ─┐ ┌─ GET /api/stats/* ──► Dashboard (SPA, served by Worker)
POST /api/collect├─► Worker ─► privacy hash │
server events ─┘ (ingest) + validate └─ GET /.well-known/* + /api/attestation/* (signed provenance)
POST /api/event │
▼
D1 (raw events, salts)
│
hourly cron ────┤ sessionize · roll up · detect anomalies · purge past retention
▼
D1 (sessions, event_rollups) ──► fast, aggregate-only reads
Ingest hashes and validates in-memory (raw IP never stored), writes raw events to D1, and an hourly cron folds them into sessions and durable rollups; the stats API and dashboard read only aggregates.
| Path | Published as | Purpose |
|---|---|---|
apps/server |
— | Cloudflare Worker: ingest + stats API + admin + cron rollups + D1 schema |
apps/dashboard |
— | React 19 + Vite dashboard, served as static assets by the Worker |
packages/client |
@writerslogic/facet |
Browser tracking snippet (zero deps, umami shim) |
packages/cli |
@writerslogic/facet-cli (npx @writerslogic/facet-cli) |
Setup, admin, reporting, offline verification, key generation & selective disclosure CLI |
packages/shared |
— | Shared TypeScript types + valibot wire schemas |
packages/trust |
— | Workers-native trust & provenance primitives (keys/JWKS, JWS/COSE, VC, DID, MMR, SCITT, RATS) |
Add tracking to a site — drop in the standalone script (umami-compatible):
<script defer src="https://your-deployment.example.com/script.js" data-site-id="YOUR_SITE_ID"></script>Or use it programmatically:
npm install @writerslogic/facetimport { init, track, variant } from '@writerslogic/facet';
init({ host: 'https://your-deployment.example.com', siteId: 'YOUR_SITE_ID' });
track('signup', { plan: 'pro' });
const cta = variant('homepage_cta'); // privacy-first A/B assignmentCreate a site & API key (admin, against your deployment):
curl -X POST https://your-deployment.example.com/api/sites \
-H "Authorization: Bearer $ADMIN_TOKEN" -H "content-type: application/json" \
-d '{"name":"My Site","domain":"example.com"}'
curl -X POST https://your-deployment.example.com/api/keys \
-H "Authorization: Bearer $ADMIN_TOKEN" -H "content-type: application/json" \
-d '{"site_id":"<the site id from above>"}'Build & test locally:
pnpm install
pnpm typecheck && pnpm lint && pnpm testThe dashboard is a React SPA served by the Worker at the root path. Enter an API key + site id to view Overview (KPIs, traffic chart, top-lists, channels, realtime), Funnels & conversions, Experiments, and Anomalies, plus an "Ask" tab for natural-language queries. Custom date ranges with period-over-period comparison and CSV/JSON export are available throughout. A Settings tab (admin token) manages sites and API keys, with one-click multi-site switching.
Every published release carries two independent, Sigstore-signed provenance attestations (recorded
in the public Rekor transparency log), so you can verify that an @writerslogic/* package was built
from this repo by its GitHub Actions workflow — currently SLSA Build Level 2:
# npm provenance (source commit + build workflow)
npm audit signatures
# GitHub build-provenance attestation over the exact tarball
gh attestation verify "$(npm pack @writerslogic/facet-cli --silent)" --repo writerslogic/facetBeyond the packages, a deployment signs machine-readable statements about itself (keys, privacy processing, build/config evidence) — see Trust & provenance. Security policy and reporting: SECURITY.md.
- Usage — the tracking snippet, npm API, UTM & form tracking, umami migration
- Self-hosting — one-command deploy on Cloudflare Workers + D1
- Privacy model — the hashing design, salt rotation, and retention
- Trust & provenance — signed deployment attestations, verification, hardware-rooted keys
- Standards & conformance — the open standards Facet implements, and where
- API reference — every endpoint, auth, and error code
- CHANGELOG · Contributing · Security
Apache-2.0 © 2026 WritersLogic, Inc.

