The Original Problem (Still Valid)
Google Analytics sends every user interaction to Google's servers. Every click, every session, every IP address — all of it leaves your infrastructure.
For developers who care about user trust, that's a problem.
The alternatives were either expensive (Mixpanel at $28/month), limited (Plausible gives you pageviews but no custom events or error tracking), or painful to self-host (Matomo).
So I built my own. The idea was solid. The execution needed work.
What Was Wrong With v1
Problem 1: API response time was 3-4 seconds
Every analytics event was being written to MongoDB synchronously, inside the request handler. Under any real load, this meant:
- The
/logendpoint was slow - Events were being dropped
- The tracked application felt the impact
This was unacceptable for an analytics tool. Analytics should be invisible.
Problem 2: Wrong database schema for time-series data
I was storing analytics events as regular MongoDB documents. That works, but it's not optimized for what analytics actually is: time-ordered data queried by date range.
Queries like "show me the last 7 days" were doing full collection scans.
Problem 3: No separation of concerns
User data, analytics events, settings, API keys — all mixed together in MongoDB. No relational integrity, no proper foreign keys, no ACID transactions where they were needed.
What I Rebuilt
Fix 1: MongoDB Time-Series Collections
MongoDB has a purpose-built collection type for time-ordered data. I migrated all analytics events to Time-Series collections.
The result:
- Automatic data bucketing by time — range queries are now index-native
- Columnar compression — storage costs dropped significantly
- Built-in TTL — data automatically expires after 6 months, no manual cleanup
- API response: 3-4 seconds → 400ms
That's not a typo. The same query that took 3-4 seconds now runs in under 400ms.
Fix 2: Async Processing With BullMQ + Redis
The /log endpoint no longer writes to MongoDB directly. Every incoming event goes into a BullMQ job queue backed by Redis. A background worker processes the queue and writes to MongoDB asynchronously.
Client SDK
│
▼
Express API ──→ BullMQ Queue (Redis)
│
▼
Worker Process
│
▼
MongoDB Time-Series
The API endpoint now just validates the request and pushes to the queue — it responds in milliseconds. Events are processed in the background. Your application is never affected.
Fix 3: Polyglot Persistence
Analytics events belong in MongoDB Time-Series. But user accounts, project settings, API keys, and billing records are relational data — they have foreign key relationships and need ACID transactions.
I added PostgreSQL via Supabase for all relational data. Supabase also handles Row Level Security.
Now the data layer is:
| Data Type | Database |
|---|---|
| Analytics events | MongoDB Time-Series |
| Users, projects, settings | PostgreSQL (Supabase) |
| Queue, caching, presence | Redis |
Each database does what it's best at. This is called polyglot persistence — and it's the right pattern for this kind of workload.
Fix 4: Real Infrastructure
v1 was running on a basic setup. v2 runs on:
- Microsoft Azure VPS — backend server
- Cloudflare CDN — global edge delivery, DDoS protection
- PM2 — process management, zero-downtime restarts
- Nginx — reverse proxy
New Features in v2
Beyond performance, I added features that were missing:
- Scroll depth tracking — how far users actually scroll (Pro)
- Core Web Vitals — LCP, FID, CLS from real users (Pro)
- Domain ownership verification — DNS TXT record or meta tag, so you can only track sites you own
- On-demand data deletion — delete individual events, date ranges, or everything from the dashboard
- Free forever plan — no credit card, no expiry
The Current Stack
| Layer | Technology |
|---|---|
| SDK | TypeScript, Beacon API, ~7.4KB gzipped |
| Frontend Dashboard | Next.js, React, TypeScript, Tailwind CSS, Shadcn UI |
| Backend | Node.js, Express, TypeScript |
| Analytics DB | MongoDB Time-Series |
| Relational DB | PostgreSQL via Supabase |
| Queue | BullMQ + Redis |
| Auth | Supabase Auth (OTP) |
| Infrastructure | Microsoft Azure + Cloudflare |
| SDK Distribution | npm + jsDelivr CDN |
What the Dashboard Looks Like Now
The dashboard gives you:
- Page views, sessions, bounce rate
- Custom events with properties
- JavaScript errors with stack traces
- Referrers, UTM params, device/browser breakdown
- User location (country/city only — IP is discarded immediately after geolocation)
- Scroll depth heatmap (Pro)
- Core Web Vitals from real users (Pro)
All without a single cookie. No consent banner required.
The Privacy Architecture (Unchanged, Still Core)
This didn't change between v1 and v2 because it was right from the start:
- No IP storage — IPs are used for geolocation then immediately discarded
- No cookies — session identity uses a daily-rotated hash, no persistent tracking
- No cross-site tracking — your data stays in your project
- No data selling — ever
- GDPR, CCPA compliant — no consent banner needed because no tracking cookies
What I Learned From 1 Upvote
The product wasn't the problem. The launch was.
I posted on Product Hunt with a brand new account, no audience, no prior community engagement. The algorithm doesn't surface products from cold accounts. That's not a conspiracy — it's just how it works.
The lesson: build an audience before you need it.
I'm doing that now. Writing here, sharing on Twitter, building in public. If you're reading this, you're part of that process.
Try It
npm install ucoder-insight
import { initUcoderInsight } from 'ucoder-insight';
initUcoderInsight("YOUR_PROJECT_ID");
That's the entire setup. It auto-tracks page views, SPA navigation, JavaScript errors, and Core Web Vitals from that single line.
- 🔗 Platform: insights.ucoder.in
- 📦 npm: npmjs.com/package/ucoder-insight
- 📖 Docs: insights.ucoder.in/docs
If you're building something and tired of sending your users' data to Google — give it a try.
And if you have feedback, I genuinely want to hear it. Drop a comment.
Built by Soumyadip Maity — Full Stack Developer, final year CS student, West Bengal, India.
Previously: I Was Tired of Sending My Users' Data to Google — So I Built My Own Analytics
Top comments (0)