Vibe code your own Plausible
Vibe code your own Plausible as a pageview script and visitor counts. Ingest at scale and the public dashboard people trust are the other product.
You can vibe code your own Plausible. Start with a first-party pageview script and visitor counts. The catalog calls Plausible that loop, not an ingest platform.
You pay $9–$19 a month. The verdict is “Lovely focused build”.
Earlier today I wrote about how to scope a SaaS clone. That was a regular backlog post. This one is a teardown.
Vibe code your own Plausible as a pageview script
The Plausible page splits the work into three rows.
The first row is 8 hours. Pageview script, visitor counts, top pages. That is a small fetch from your origin, a daily visitor hash, and a table of paths. You can vibe-code this.
The second row is 3 days. Goals, referrers, geo, device breakdown, CSV export. Still your own sites. More screens, a country from a GeoIP lookup, an export button.
The third row is 1 month and up. Billions of events, funnels, imports, SaaS hardening. Skip that row.
If you tell an agent to “clone Plausible”, it aims at the third row. The useful v1 is the first one.
Here is the shape of the data. Nothing else has to exist at 8 hours.
const pageview = {
id: 'pv-home',
siteId: 'flaviocopes.com',
path: '/blog/how-to-scope-a-saas-clone/',
referrerHost: 'news.ycombinator.com',
name: 'pageview',
}
const day = {
siteId: 'flaviocopes.com',
date: '2026-10-23',
visitors: 1,
views: 1,
}
A site, a pageview, and one day row. Goals can wait until Tuesday.
The audience line on the page is a site owner who wants first-party traffic numbers. If that is you, a script and five cards are enough.
The same v1 also replaces Fathom, Simple Analytics, Umami, and Matomo. DataFast is the revenue cousin at $9–$49 a month. Hotjar is the click map at $39–$99. When the question is “did anyone see the launch tweet”, that is Buffer at $18–$36.
How does Plausible work
Plausible publishes the code. I am summarizing those docs and two engineering posts, not reconstructing their cluster.
The public stack on their GitHub readme is Elixir and Phoenix, PostgreSQL for app data, ClickHouse for events, React for the dashboard.
A browser posts JSON to POST /api/event. The Events API always answers 202 Accepted, even when the event is dropped. The body is small.
const event = {
name: 'pageview',
domain: 'flaviocopes.com',
url: 'https://flaviocopes.com/blog/how-to-scope-a-saas-clone/',
referrer: 'https://news.ycombinator.com/',
}
name is pageview or a custom event. url is the page. UTM query keys stay. Other query keys are stripped. The script options list the keepers: ref, source, utm_source, utm_medium, utm_campaign, utm_content, utm_term.
The ingest module at lib/plausible/ingestion/event.ex is the public map. It checks the site, drops bots and spam referrers, parses the user agent, hashes a visitor id, then buffers a ClickHouse row. The Events API notes say the write is async. Treat today’s exact buffer size as an educated guess.
This site already loads Plausible from b.flaviocopes.com. That is their first-party proxy. Blocklists often miss a script on your own domain. They say a default plausible.io install can miss 5% to 25% of visits, depending on the audience.
The tracker itself is one compiled source. Their tracker architecture builds many variants from the same files, then drops unused branches. Their lightweight page puts the gzipped script at 2.5KB. Google Analytics is 135KB on that same page.
A v1 does not need Phoenix. One Worker, one Analytics Engine dataset, one Astro dashboard. That is the stack on the card. Use Rails if that is your weekday language.
How Plausible counts unique visitors
Plausible refuses cookies for unique visitors.
Their data policy publishes this formula:
hash(daily_salt + website_domain + ip_address + user_agent)
The salt rotates every 24 hours. The old salt is deleted. Raw IP and user agent never land on disk.
The ingest source is a bit more specific. generate_user_id SipHashes the salt with user agent, IP, domain, and the registrable root of the hostname. Same idea. One extra hostname piece so app.flaviocopes.com and flaviocopes.com do not collapse by accident.
Plausible.Session.Salts keeps two salts in ETS. The current one, plus the previous one. Salts older than 48 hours are deleted. The previous salt exists so a visit that crosses midnight can still attach to the same session for a while. Treat “48 hours is still the 2026 window” as an educated guess. After the salt dies, yesterday’s visitor cannot be recognized.
Their metrics docs are honest about the tradeoff. The same person on two devices is two visitors. The same person tomorrow is a new visitor. One person can have more than one session in a day. A session ends after 30 minutes of silence.
The catalog’s first hard rule matches that public one. No cookies. No localStorage. No identifier that still works on Thursday.
function visitorId(site, request, salt) {
const userAgent = request.headers['user-agent'] || ''
return hash([
salt,
site.domain,
request.ip,
userAgent,
].join('|'))
}
function countView(day, visitorId, seenToday) {
day.views += 1
if (!seenToday.has(visitorId)) {
seenToday.add(visitorId)
day.visitors += 1
}
}
Hash, then throw the IP away. Persist the hash with a TTL that dies with the salt. Three same-day views are one visitor and three pageviews.
A v1 can store the day row in D1 and the “seen today” set in KV. Cloudflare Analytics Engine is the catalog pick if you want the time series closer to ClickHouse than to a table you roll up yourself.
Their docs call a visit a bounce when it is a single pageview. Put that formula in one module. The card, the chart, and the CSV should call it. The mini-prompt on the page is that module.
How Plausible event ingest works
The catalog’s first rule is the same as theirs. Authenticate the site, then answer fast.
The Events API wants a real User-Agent and, when you proxy, a real X-Forwarded-For. If the header is your Worker’s IP, their bot filter drops the event. The status is still 202. The debug header is x-plausible-dropped: 1.
async function acceptEvent(request, sites) {
const event = await request.json()
const site = sites.find((item) => item.domain === event.domain)
const originHost = origin(request)
if (!site || !site.allowlist.includes(originHost)) {
if (site) await site.bumpDropped()
return { status: 202, dropped: true }
}
if (isBot(request) || overCap(site)) {
await site.bumpDropped()
return { status: 202, dropped: true }
}
const salt = await currentSalt(site.timezone)
const id = visitorId(site, request, salt)
await queue.send({ ...scrub(event), visitorId: id })
return { status: 202 }
}
Respond before you aggregate. A slow collector makes the page you are measuring feel slower.
Scrub the path before you store it. Strip query strings except the campaign keys you decided to keep. /login?token=abc and /login are one page. /Pricing and /pricing/ are one page too if you normalize.
Rate-limit per site. A render loop in a single-page app will happily post a few thousand pageviews a minute. Drop the flood. Show the dropped count on the site settings page so you know the cap fired.
Plausible Cloud goes further on bots. The self-hosting page says Cloud excludes about 32,000 datacenter IP ranges, plus user-agent bots and spam referrers. Community Edition gets the basic user-agent and referrer lists. A v1 can start with a short bot list and a datacenter check later.
Ingest at scale and the public dashboard
The skip row is Plausible the platform.
They started on PostgreSQL. Uku Taht’s April 2020 recap is the public fork in the road. Pre-aggregate every breakdown, or keep raw events and scan them. Pre-aggregation explodes once someone asks for bounce rate from the UK on /blog, on mobile, last Tuesday. They picked ClickHouse so those questions stay possible.
The May 2020 recap said the live-demo graph went from 5 to 6 seconds down to about 500ms after the move. A later funding post said ClickHouse let them count more than a billion pageviews a month. Treat 2026 cluster size as an educated guess. The product rule is still: raw events in a column store, aggregate on read.
Funnels, user journeys, ecommerce revenue, and a Sites API sit on the Cloud side of their Community Edition comparison. CE ships twice a year. Cloud ships all week. Imports from Google Analytics, a shared public dashboard your readers trust, and the bot mesh that watches every tenant at once are the other product.
A public stats URL gets bookmarked and pasted into launch threads. If that number is wrong on a Monday, you hear about it. Some teams should keep paying for that trust.
Hotjar is the replay cousin. The catalog’s non-goal already names it: do not build attribution models, ad integrations, user journeys, or raw event replay. DataFast is the first-touch revenue report if the question is “which click paid”.
A Plausible alternative for one site
I would rebuild Plausible for a couple of sites I already deploy, where the current “analytics” is a hosting panel I open once a month. One site row, a first-party script on the same origin, a daily salt, and cards for visitors, views, and top pages.
Goals come after the counts look true for a week. Geo can wait until a country list would change a decision. I would not build a funnel builder.
Host it next to the product, on Astro, Workers, and Analytics Engine, because that is the stack on the card. Use Rails if that is your weekday language. Add referrers once the top pages are boring.
If the rent is “which tweet became a customer”, that is DataFast, 10 hours for pageviews, UTM, and a payment webhook. If the rent is “queue the tweet”, that is Buffer.
Start with the first row
Go to Build your own Plausible. Copy the AHA prompt, or the stack you already write.
Tell the agent the first estimate only: pageview script, visitor counts, top pages. Eight hours.
If the company already lives in funnels and a public live demo people quote, keep paying.