About a year ago we were looking at Plausible and Stripe at the same time, in separate tabs, trying to manually correlate the two.
Plausible told us we'd had a good week — organic search was up, a Hacker News post had sent a few hundred visitors, Twitter was doing its usual low-engagement trickle. Stripe told us we'd had four new subscriptions.
But which four people converted? Did the HN visitors convert, or did they just read the pricing page and leave? Was the organic traffic sending buyers or browsers? We genuinely had no idea. We'd been running the product for six months and couldn't answer the most basic question in marketing: which channel is actually working?
This isn't a Plausible problem specifically — it's a category problem. Privacy-first analytics tools track sessions. Payment processors track money. Nobody connects them. We spent a weekend trying to bolt something together manually (UTM parameters passed through Stripe metadata, a cron job, a spreadsheet) and realised the correct solution was a tool that did this properly.
That's why we built EngageTrack. This post is about the technical decisions we made and why, what was harder than expected, and where the thing is still rough.
The Core Technical Problem: Matching Sessions to Payments
The fundamental challenge in revenue attribution is linking a payment event (which happens in Stripe, outside your website) to a visitor session (which happened on your website, potentially days earlier).
The naive approach — pass a session ID in the checkout URL — breaks down immediately for subscription products where users sign up for a trial, close the browser, and come back two weeks later to complete payment. The session is long gone.
Our approach uses a four-level attribution hierarchy, applied in priority order:
Level 1: Visitor ID in payment metadata. When you pass EngageTrack's visitor ID into Stripe's payment metadata before checkout, the webhook carries a direct reference to the originating session. This works across devices and any time gap. The visitor ID is a random UUID generated per browser that persists in a short-lived local store — not a tracking cookie, because it's single-site and doesn't follow the user anywhere.
Level 2: Customer email match. If a visitor identifies themselves via engagetrack.identify() and later converts with the same email, the sessions are linked. Useful for products where users register before paying.
Level 3: Same-session attribution. If payment happens in the same session as the visit (common for products with fast conversion cycles), the referrer from that session is used.
Level 4: First-touch fallback. Last resort — the oldest session we have for that visitor's browser. Imperfect, but better than nothing.
In practice, for most SaaS products with a free trial, Level 1 + Level 3 handles the majority of cases correctly.
Stack Choices and Why
Go + Fiber for the backend. We considered Node (familiar) and Rust (tempting). We chose Go because we wanted a statically-typed, compiled language that would stay readable as the codebase grew, and Fiber's Express-like API meant we could move fast without fighting the framework. The webhook handlers in particular benefit from Go's concurrency model — processing simultaneous webhook events from multiple payment providers without blocking.
PostgreSQL + TimescaleDB. Raw PostgreSQL is the obvious choice for a transactional data layer, but analytics queries — "give me daily sessions grouped by source for the last 90 days" — are where plain Postgres starts to hurt. TimescaleDB's hypertables give you automatic time-based partitioning and time-series aggregate functions. The time_bucket() function alone justified the dependency. We're running on a single TimescaleDB instance right now, which handles the current load fine. When that stops being true, the partitioning model means sharding is relatively clean.
Redis for pub/sub and realtime. The realtime dashboard (the 3D globe with live visitor markers) needs sub-second event propagation from the analytics ingest endpoint to connected WebSocket clients. Redis pub/sub is the obvious fit — ingest handler publishes an event, WebSocket handler subscribes and pushes to the browser. The alternative was a message queue which felt like over-engineering for this traffic volume.
Next.js 16 + React 19. Pragmatic choice: App Router with Server Components means we can render analytics data server-side with full TypeScript type safety across the stack, and Vercel's edge network handles the CDN layer without running additional infrastructure. The frontend is a dashboard app — lots of charts, tables, filters, realtime updates — which React handles well.
Sub-3KB tracking SDK. We spent more time on the SDK than expected. The target was under 3KB minified, which meant every byte had to earn its place. The key decision was using sendBeacon for event dispatch rather than fetch — sendBeacon fires reliably on page unload without holding up navigation, which matters a lot for accurate session duration and exit page tracking. The SDK auto-tracks outbound clicks, file downloads, form submissions, and scroll depth with no configuration. You get all of that for 3KB and one script tag.
The Privacy Model
Worth being precise about this because "cookie-free" gets used loosely.
EngageTrack sets no cookies. It does not fingerprint browsers (no canvas, no WebGL, no font enumeration). It does not store IP addresses.
For session continuity within a single day, it generates a hash from: the anonymized IP prefix (first two octets only), the user-agent string, and a daily salt that rotates at midnight UTC. The hash is stored only for the duration of the current analytics processing window — it is never written to the database. Tomorrow's salt produces a completely different hash for the same visitor.
This means: accurate session grouping within a day, no cross-day user tracking, no personal data processed. The implication is that "returning visitor" metrics are slightly less accurate than tools that use persistent identifiers — someone who visits Monday and Friday counts as two separate visitors. That's an intentional trade-off. The alternative is persistent identification, which means personal data, which means GDPR basis, which means cookie consent banners.
All data is stored in Frankfurt (EU). The CDN for the tracking script is also EU-based.
What Was Harder Than Expected
Webhook replay protection. Payment webhooks get retried by providers on failure — Stripe retries for up to 72 hours, LemonSqueezy retries three times with delays. This creates a real attack surface: a captured valid webhook payload can be replayed to corrupt revenue data. We implemented timestamp-based replay protection for all four providers, rejecting webhooks with timestamps outside a 5-minute window using time.Since().Abs() (the .Abs() matters — servers with clock drift can be slightly ahead of the webhook provider's timestamp). Each handler validates the timestamp before performing the HMAC computation, so replay attempts fail fast without doing any work.
Attribution across payment provider boundaries. Different payment providers have very different webhook payload structures. Stripe's payment_intent.succeeded is clean and consistent. Polar's webhook format was underdocumented and we had to test against live events to confirm the exact timestamp field. LemonSqueezy embeds the relevant attribution data in nested metadata. Building a unified attribution model that works across all four providers with a single internal event schema was a week of work we hadn't budgeted for.
The realtime globe. This was an indulgence. We wanted the realtime dashboard to feel like something — not just a number incrementing in the corner. A 3D globe showing live visitor locations with animated avatar markers and a scrolling event feed. It uses Mapbox GL for the globe, DiceBear for the visitor avatars, and a WebSocket connection for the event stream. It's entirely unnecessary for the core product. But when we showed it to people, it was the thing they screenshot and share. Sometimes the delightful feature is worth building.
What's Still Rough
No mobile app. The dashboard is responsive but it's a web app. There's no iOS/Android native app. This matters less than we expected for the core use case (checking analytics on a laptop) but we hear about it occasionally.
Blog and content pages aren't in the product yet. The comparison pages (vs Plausible, vs Fathom, etc.) and this blog exist, but there's no integrated content layer inside the app itself. Docs are there, the rest is sparse.
Realtime data doesn't persist. Events appear in the live feed but the realtime panel only shows the current 5-minute window. Historical realtime data (what happened at 2pm three days ago, second by second) isn't stored — the database holds aggregate buckets, not the full event stream. This is a cost/architecture decision we may revisit.
No data export UI. There's an API, and Startup+ plan users can query it, but there's no one-click CSV export in the dashboard yet. It's on the roadmap and we know it's annoying.
Pricing
Starting at €5/month for 10,000 events. Startup at €20/month for 200,000 events. Agency at €50/month for 2,000,000 events and unlimited sites. 14-day free trial, no credit card required.
We priced it below Plausible deliberately — not as a race to the bottom, but because we think the market of founders who want privacy analytics is price-sensitive at the low end, and we'd rather have more users on the product giving feedback than maximise ARPU at launch.
What We'd Like Feedback On
If you try it: the attribution setup is the part we're least confident about in terms of UX. Is passing the visitor ID to Stripe metadata too much friction for most setups? Is there a better default for products where the Stripe integration isn't feasible?
If you've built something similar: we'd genuinely like to hear how you handled attribution across long trial periods (30–60 day trials where the original session is completely gone).
If you use Plausible or Fathom and are curious: the comparison page at engagetrack.net/vs-plausible is honest — we didn't cherry-pick the table to make Plausible look bad. They win on script size and community size. EngageTrack wins on revenue attribution and price.
The site is engagetrack.net. Trial is free, no card required.
EngageTrack is built by Asuna Labs, based in Cluj-Napoca, Romania. It's currently pre-launch v0.1.0 — the core analytics and revenue attribution are production-ready; some secondary features are still in progress.