Web analytics you ask — not a dashboard you maintain.
Simply ask your analytics questions in Claude or Cursor and get an answer — not a dashboard filter you have to build first. Self-hosted, MIT licensed, no cookies by default.
You: Did anything unusual happen on the site last week?
Agent: Interaction events were up about 35% last Tuesday — almost entirely outbound_link_click on your GitHub link, not more page views. Worth checking what pointed there that day.
Flow
Events in, answers out, one process in between.
One process and one volume: the database runs inside that process, so there is no second service to keep alive and no analytics platform underneath it. Put it behind the reverse proxy you already run, and that is the whole of it.
Your website sends events to Genug, a single Node process, over its /events endpoint, and also serves a cockpit at /cockpit. SQLite is embedded in that same process rather than run as a separate database, and everything it keeps sits on one volume at /data: genug.db holding every event as a row, an events directory holding your schemas as one JSON file each, and a context directory holding the ground rules and history log the agent reads before it answers. Two things read that data back, and both go through Genug: your AI agent, over /mcp, and you, in the cockpit the same process serves at /cockpit.
Features
Every question is one call, not a filter you build.
Ask, don't read
Every tool takes a segment, so “where did the buyers of product X come from” is a single call rather than a dashboard filter you have to build. Thirty-one MCP tools in total, each answering one question a site owner actually asks — top pages, where traffic comes from, which campaign converted, whether last week's visitors came back. If you want a chart, ask for one.
- How much traffic, and when?
- Summary, per day / weekday / hour, one event's trend
- Which pages, and where from?
- Top, entry, exit and bounce pages; referrers; campaign params
- What happened?
- Events ranked, broken down by any prop, summed, funnelled
- Who is visiting?
- Devices and browsers, languages, new vs returning, cohorts
- Is tracking working?
- Recent raw events, rejections with reasons, bot traffic
- … for one set of sessions?
- The segment argument on every tool above
What leaves your server when you ask
Genug stores everything on your own machine and sends it nowhere on its own. But an MCP client is something else's client: the tool results your agent fetches go into that model's context, which for a hosted assistant means they are sent to that vendor's API. Most tools return aggregates — counts, rankings, breakdowns. get_recent_events is the exception and returns raw rows, including URLs and whatever your events put in props (your own custom fields). If your deployment holds data you would not hand to a third party, that is the tool to think about.
There is deliberately no raw-SQL tool and no row export. An agent cannot write its own SQL — it calls a tool that runs a fixed query.
Query strings are filtered to campaign parameters before anything is stored, so a newsletter link's ?email=, a password reset's token and a site search's typed query stay out of the database — but ref and source survive filtering, so whatever you put in those is stored. Your own event props are yours to keep clean.
Running this for a client in the EU makes the model vendor a processor: you need a data-processing agreement, a valid basis for the transfer, and the recipient named in the tracked site's privacy policy. An EU-hosted or locally-run model avoids the transfer question entirely. None of this is legal advice — if the data is a client's, get it checked. What leaves your server when you ask.
Your own events, validated on arrival
Three events come built in — page views, outbound link clicks and file downloads — each behind its own opt-in flag. Anything else is yours to define, as one JSON file or through the cockpit's own form, live the moment you save. Props are checked against your schema when they arrive, so a mistyped one shows up in the rejected counter instead of vanishing.
From JavaScript
window.genugAnalytics.track("order_completed", {
order_total: 49.9,
order_currency: "EUR",
});Or straight from the markup
<button
data-genug-on-click="cta_click"
data-genug-props='{"plan":"pro"}'
>
Upgrade
</button>A cockpit, for when you'd rather look than ask
No agent required: an Overview band counting sessions, events, rejections and filtered bots, a traffic trend, and cards ranking top pages, referrers and devices. The schema registry and the tool list above live in it too.
Radically small
One Node process, one SQLite file, and no analytics platform to run. Measured, not guessed: about 2,300 events a second in Docker behind a reverse proxy, 0.4 KB stored per event — a 2 vCPU / 2 GB VPS is enough. Your own events are one JSON file each, live the moment you save.
Built to operate
Retention defaults to 425 days, a real period rather than forever, and yours to change with one setting. Daily backups on by default, seven days kept. Erasure through a write tool that previews before it deletes. Health on /healthz.
Ground rules and a history log
A markdown file you write tells the agent how to answer — ask instead of guessing, name the unit, go careful on small samples. Beside it, dated notes on what happened to the site, so a drop in the chart gets explained rather than guessed at.
Yours to change
MIT, and written to be modified: three recipes cover adding an event, adding a tool, and changing the cockpit end to end. An AI assistant pointed at the repo picks up its rules and follows them.
Every tool's description says what its number means, what it excludes and where it misleads — the documentation the agent reads.
Quick start
From nothing to answers in 7 steps.
- 01
Pick a hostname on a subdomain of the tracked site
Such as
analytics.your-domain.com. - 02
Run it — three secrets and the tracked site's origin
docker run --rm -p 3000:3000 \ -e ALLOWED_ORIGIN=https://your-domain.com \ -e SALT_SECRET=<openssl rand -hex 32> \ -e MCP_API_KEY=<openssl rand -hex 32> \ -e COCKPIT_PASSWORD=<openssl rand -hex 32> \ -v genug-data:/data \ ghcr.io/datapip/genug-analytics:v0.5.0 - 03
Put it behind a reverse proxy that terminates TLS
Caddy, nginx, Traefik or your platform's own — the cockpit's session cookie is Secure, so it needs https. Set TRUST_PROXY=1 for a single proxy, and keep the collector's DNS record unproxied: a second hop in front collapses every consentless visitor into one shared identity, and TRUST_PROXY=2 is only safe if the origin cannot be reached directly.
- 04
Put the script on your site
<script> window.genugAnalyticsConfig = { enableAutoPageTracking: true }; </script> <script defer src="https://analytics.your-domain.com/client.js"></script> - 05
Watch the first event arrive at /cockpit
If nothing shows, the rejected counter says why.
- 06
Connect your agent
Any MCP client works — Claude Code, Claude Desktop, Cursor, or a local model on your own machine. Skip this step and the cockpit still has everything the tools would answer.
claude mcp add --transport http genug https://analytics.your-domain.com/mcp \ --header "Authorization: Bearer YOUR_MCP_API_KEY" - 07
Ask questions
Let the AI agent answer based on your data.
Privacy
Nothing on your visitors' devices, by default.
In its default consentless mode Genug writes nothing to the visitor's device: no cookie, no localStorage, no sessionStorage, and no fingerprinting — no canvas, no screen or font enumeration, no hardware probing. No profile is built on the side and there is no cross-site identifier. Consent selects how a visitor is identified, not whether events are recorded — consentless events are still stored, under a daily-rotating pseudonym.
Pseudonymous, not anonymous
The hash is a truncated address plus the User-Agent plus a salt derived from today's date, so it cannot be recomputed tomorrow, and the address going in is a network block rather than a connection. But your server holds the secret that salt comes from, so a known address and User-Agent can still be re-identified for a given day. These are personal data.
A working opt-out
genugAnalytics.optOut() writes a first-party flag and tracking stops for that browser; optIn() resumes and isOptedOut() renders your own toggle. Put it behind a link in your privacy policy and you have the objection route Art. 21 needs.
Consent, if you run a banner
Consent upgrades the visitor to a first-party genug_vid cookie — HttpOnly, Secure, SameSite=Lax, holding the id and nothing else. It expires 13 months after their last event and is refreshed on each visit, is only ever set for a visitor who consented, and is cleared when one withdraws.
Because you host it yourself there is no analytics vendor to sign an Art. 28 DPA with — though your hosting provider is still a processor, and so is the model vendor behind whatever agent you point at the MCP endpoint.
Whether this lets you drop the banner is a decision about your deployment, not one the software can make: § 25 TDDDG's storage half is clear in the default consentless mode, its access half is not settled for cookieless analytics — the product's privacy document sets out what is settled and what is not. What stays yours either way: name what you collect in your notice, name a legal basis — normally Art. 6(1)(f), with the balancing test done and kept — and set a retention period. None of this is legal advice.
Every field written, and nothing else
12 fields
event- The event name, e.g. page_view
visitor_id- Derived server-side: the daily hash, or the cookie value once consented
session_id- Server-assigned; a new one after 30 minutes idle
ts- The server's clock, UTC — never the visitor's
url- The page's own URL, query strings filtered — see below
referrer- Where the visit came from, filtered the same way
device_type- What the User-Agent classified to: mobile, tablet, desktop, other
browser- Likewise: Chrome, Safari, Firefox, Edge or Other
visitor_language- First Accept-Language entry, if it's a locale tag
consent_mode- consentful or consentless
props- Your schema — page_view declares page_title and document_language by default
idempotency_key- Only when you pass one yourself, e.g. an order id
And nothing else. No raw address and no User-Agent string is written to the database — only the hash derived from both. (The full address does reach the server log, once a minute, when the rate limiter refuses traffic.)
Query strings are stripped to campaign and click-id parameters — utm_*, gclid, fbclid, msclkid, ttclid, ref, source — and the fragment is dropped entirely. Note that ref and source survive, so a link written ?ref=a-person's-name does store that name.
Everything above describes the software you would run. For this website itself, see its privacy policy.
Genug — German for enough
One process, thirty-one tools, no cookie banner to maintain.


