Skip to content

Facet logo

Facet logo

Facet

Privacy-first, cookieless web analytics & experimentation

Runs entirely on the Cloudflare edge — no cookies, no external database,
and no cross-session identity to leak.


Deploy to Cloudflare CI OpenSSF Scorecard SLSA Build Level 2 TypeScript Cloudflare Workers License: AGPL-3.0 + commercial ORCID

Facet dashboard — live demo

The drop-in Umami alternative that's private by math, not policy.
Cookieless, verifiable, and one click on your own Cloudflare account — free tier, no database to run.
Live demo → (no login — fabricated data)

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.

Facet is software you run, not a service you sign up for. There is no Facet cloud, no account to create, and no data of yours held anywhere but your own Cloudflare account.

Install

Facet deploys to your Cloudflare account. You need Node ≥ 22, pnpm 11, and an authenticated wrangler. One command does the rest:

git clone https://github.com/writerslogic/facet.git
cd facet && pnpm install
npx @writerslogic/facet-cli init

init creates the D1 database, generates an ADMIN_TOKEN and stores it as a Worker secret, applies migrations, builds the dashboard, deploys, then creates your first site and issues its API key. It asks only three things — the hostname, the site domain, and the site name — each with a default you can accept by pressing Enter, and it prints the full plan for confirmation before creating anything.

Re-run it any time: every step detects whether it is already done, so a failed install resumes rather than duplicating resources. facet doctor diagnoses an existing install and is safe to paste into a bug report. facet init --dry-run prints the plan and changes nothing.

Deploying without a custom hostname gives you a *.workers.dev URL that works immediately; putting it on your own hostname is an optional step. No deployment-specific value in this repo points at anyone else's domain, account, or mailbox — the hostname, the database, the admin token, and the security contact are all yours to set, and Facet publishes nothing about your deployment that you did not configure. Full walkthrough: docs/install.md. Manual path, environment variables and operations: docs/self-hosting.md.

What it costs

Facet charges nothing: there is no hosted plan, no per-event pricing, and no seat count. Your only bill is Cloudflare's, for one Worker and one D1 database. Small and mid-size sites generally fit inside Cloudflare's free tier; beyond it, the Workers paid plan starts at $5/month at time of writing. The optional "Ask" tab uses Workers AI, which is metered separately — every other feature works without it. Check Cloudflare's current Workers and D1 pricing for the authoritative numbers.

Why Facet

  • 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/event server-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. See docs/trust.md.
  • Free, self-issued API keys and unlimited, first-class multi-site.

How privacy works

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.

Architecture at a glance

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.

Packages

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)

Quick start

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/facet
import { 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 assignment

Create 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 test

Dashboard

The 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.

Supply chain & provenance

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/facet

Beyond 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.

Documentation

License & attribution

Open source with a commercial option. Facet is written and copyrighted by WritersLogic, Inc. (© 2026); that is authorship, not a dependency — a Facet you deploy talks to nothing of ours.

Can you use it? In short: yes, for free, including commercially, as long as you don't offer a modified Facet to other people as a service without publishing your changes.

What you're doing License What you owe
Self-hosting Facet for your own sites or organization — including a commercial business AGPL-3.0-only Nothing. Run it, modify it internally, free forever.
Embedding the browser SDK in your site, or using the CLI MIT (packages/client, packages/cli, packages/shared) Nothing. Your site stays closed-source.
Building on the trust/provenance library Apache-2.0 (packages/trust) Nothing (includes a patent grant).
Offering a modified Facet to others as a hosted service AGPL-3.0-only Publish your modified source under the AGPL — or buy a commercial license.
Removing the "Powered by Facet" attribution either Publish your source per the AGPL, or hold a commercial white-label license.
Rebranding a fork under the Facet name or logo Not granted by any code license. Use your own name — see TRADEMARK.md.

The dashboard shows a small "Powered by Facet" link. It is a plain, unobfuscated element with no phone-home and no tamper check: you may remove it by complying with the AGPL (publish your corresponding source, which the license already requires of a modified network service) or under a commercial white-label license, which sets VITE_FACET_WHITE_LABEL=1 without the source obligation. Full terms: TRADEMARK.md.

Commercial licensing (hosted/OEM use without AGPL obligations, white-label, warranty and support): LICENSING.mdlicensing@writerslogic.com.

About

Privacy-first, cookieless web analytics & experimentation on Cloudflare Workers + D1. Self-hosted, one-command deploy: page views, sessions, funnels, goals, channels, A/B experiments, anomaly detection & ain-English queries. Runs on Cloudflare's free tier. No cross-session identity, raw IP never stored. Umami-compatible.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

Watchers

Forks

Releases

Used by

Contributors

Languages