SaaS revenue metrics with an AI analyst — a lightweight alternative to ChartMogul for indie founders.
Point your billing webhooks at PulseOps and it turns a raw stream of signups, payments and churn into live MRR, ARR, churn and conversion — plus a one-click AI analyst that reads the numbers and tells you what's moving and what to do next. In realtime.
Live demo: https://pulse-ops-ai-five.vercel.app/ Demo login:
demo@pulseops.app·Passkey123!(or sign in with Google / passkey)The demo runs on a sample workspace with generated data so you can explore without connecting a real Stripe account.
A solo founder or a small SaaS team (1–5 people) on Stripe has revenue coming in — but no clear read on it. Stripe's own dashboard shows transactions, not MRR, churn or conversion, and it never tells you what to do. The dedicated tools that do — ChartMogul, Baremetrics, ProfitWell — start at $100–500/mo, which is hard to justify pre–Series A. So founders end up back in spreadsheets, guessing whether growth is real and whether churn is creeping.
PulseOps ingests billing events (Stripe-style webhooks) through a simple API and derives the metrics that matter, in realtime:
- Live metrics — MRR, ARR, active users, churn and conversion, computed from the event stream (not stored and drifting — derived, so they're always consistent and auditable).
- Realtime event feed — new events appear the moment they land, over Supabase Realtime.
- AI analyst — the differentiator. ChartMogul shows you charts; PulseOps also explains them. One click produces a headline, highlights, anomalies and concrete next steps, grounded in your live numbers and a statistical anomaly pass (see below) — the model explains real signals instead of inventing them.
- Analytics — MRR-over-time, signups per day, event-mix, an MRR movement waterfall (new/expansion/reactivation vs. contraction/churn), weekly cohort retention, and statistically detected anomalies.
- Passwordless auth — passkeys (Face ID / Touch ID / Windows Hello), Google OAuth and hCaptcha, all via Supabase.
The public demo uses a sample workspace — click Load sample data to generate ~60 days of realistic history, or Simulate live events to watch the realtime feed update. Every workspace can also switch to Live mode and connect a real Stripe account (see below) — the same analytics engine, same AI analyst, running on real billing events instead of generated ones.
PulseOps can ingest real Stripe billing events instead of sample data. This is a genuine webhook integration — signature-verified, per-workspace — not a mock.
- Go to Settings in the app. Copy the webhook URL shown there (unique per account).
- In the Stripe Dashboard (Test mode is fine — no real money needed): Developers → Webhooks → Add endpoint, paste the URL, and subscribe to these events:
customer.created,customer.subscription.created,customer.subscription.updated,customer.subscription.deleted,invoice.payment_succeeded,invoice.payment_failed. - Stripe shows a signing secret (
whsec_...) after the endpoint is created — paste it back into the Settings page and save. - Switch the toggle to Live. Trigger a test event from Stripe (or use their test clocks) and watch it land in the event feed in real time.
Under the hood: each Stripe event is verified (HMAC-SHA256 over the raw body, timing-safe comparison, replay-window check — the same algorithm Stripe's own SDK uses) and mapped to PulseOps' internal event model — subscription prices are normalized to a monthly amount regardless of billing interval, and Stripe's opaque customer IDs are deterministically hashed into the UUIDs the schema expects. All of that mapping logic is pure and covered by unit tests.
Solo — product, design and full-stack engineering. I scoped the product, designed the brand and UI, modelled the data, built the ingestion pipeline, the analytics engine, the AI layer and the auth, and shipped it to production on Vercel.
Each choice was deliberate — the reasoning matters more than the stack itself:
- Event-sourced metrics. Events are the source of truth; metrics are derived by a pure function, never stored. This keeps numbers self-consistent and auditable, and makes the whole domain trivial to test.
- Drizzle over TypeORM. Started on TypeORM, hit friction unfit for serverless (decorators,
reflect-metadata, dynamic-requiredriver issues, a separate migration CLI). Swapped to Drizzle while the DB was still empty — lighter runtime, SQL-first migrations, no decorators. - Supabase for auth + realtime + Postgres. One managed source of truth for sessions (passkeys, OAuth, captcha) and live subscriptions — no second auth library, no websocket server to run.
- Groq for the AI layer. OpenAI-compatible, generous free tier, ~2s inference. A strict JSON contract (schema in the prompt +
response_format: json_object+ defensive parsing + zod validation) keeps model output safe without paid structured-outputs. - Feature-Sliced Design. A strict, dependency-directed layering that keeps the codebase scalable and the pure domain isolated from framework glue.
- Serverless-aware DB pooling. Capped
pgpool per lambda so the Supabase pooler isn't exhausted under fan-out. - No Stripe SDK for the webhook path. Signature verification is ~20 lines of
crypto(HMAC-SHA256 + timing-safe compare) — pulling in the full SDK for that alone isn't worth the weight. The mapping and verification logic is pure and unit-tested.
src/
├─ app/ Next.js App Router (routing, layouts, route handlers)
├─ views/ Composed page bodies (FSD "pages" layer)
├─ widgets/ Self-contained UI blocks (sidebar, topbar, charts, event feed)
├─ features/ User-facing capabilities (ai-analyst, auth, realtime, stripe-integration)
├─ entities/ Business domains (event, metric, user) + pure domain logic
└─ shared/ Framework-agnostic primitives (ui, lib, api clients, config)
The domain layer is pure and framework-free — analytics, time-series, the event generator, and formatters take plain data and return plain data. That's what makes it fast to test and reason about.
| Area | Choice |
|---|---|
| Framework | Next.js 16 (App Router, Turbopack), React 19 |
| Language | TypeScript (strict) |
| Styling | Tailwind CSS v4, shadcn/ui components on @base-ui/react |
| Charts | Recharts |
| Database | Postgres via Supabase, Drizzle ORM |
| Auth | Supabase Auth — passkeys, Google OAuth, hCaptcha |
| Realtime | Supabase Realtime |
| Validation | zod |
| AI | Groq (OpenAI-compatible, free tier) |
| Testing | Vitest (+ v8 coverage) |
| Architecture | Feature-Sliced Design |
The pure domain layer is covered to 100% (statements / branches / functions / lines) with Vitest. Framework glue (Server Components, route handlers, Supabase, Drizzle) is intentionally excluded — testing it would mostly test mocks, not logic.
npm run test # watch mode
npm run test:run # single run
npm run test:coverage # with coverage reportCovered: computeAnalytics, computeDailySeries / computeEventTypeCounts, computeMrrMovement, computeCohortRetention, detectAnomalies, the event generator, currency/percent/relative-time formatters, the AI response schema + JSON extraction, and the Stripe integration (customer-ID hashing, monthly-amount normalization, event mapping, signature verification).
- Node.js 20+
- A Supabase project (free tier)
- A Groq API key (free, optional — the app runs without AI)
npm install
cp .env.example .env.local # then fill in the values (see below)
npm run db:generate
npm run db:migrate
npm run devOpen http://localhost:3000, sign up, then click Load sample data to populate the workspace.
| Variable | Required | Purpose |
|---|---|---|
NEXT_PUBLIC_SUPABASE_URL |
✅ | Supabase project URL |
NEXT_PUBLIC_SUPABASE_ANON_KEY |
✅ | Supabase publishable/anon key |
SUPABASE_SECRET_KEY |
✅ | Supabase secret key (server-only) |
DATABASE_URL |
✅ | Postgres connection string |
NEXT_PUBLIC_SITE_URL |
✅ | Base URL for OAuth/passkey redirects |
NEXT_PUBLIC_HCAPTCHA_SITE_KEY |
— | hCaptcha site key (auth works without it) |
GROQ_API_KEY |
— | Enables the AI Analyst |
GROQ_MODEL |
— | Override the default model |
| Script | What it does |
|---|---|
npm run dev |
Start the dev server (Turbopack) |
npm run build |
Production build |
npm run start |
Serve the production build |
npm run lint |
Lint with ESLint |
npm run test / test:run / test:coverage |
Vitest |
npm run db:generate / db:migrate |
Drizzle schema & migrations |
A self-initiated product MVP. Data in the live demo is synthetic; not affiliated with Stripe, ChartMogul or any named company.