The idea
A single-page dashboard you deploy on your own server that connects to your payment processors via read-only API keys and computes the metrics that matter for an indie SaaS: monthly recurring revenue, active subscriptions, churn rate, and customer lifetime value. You get one URL to bookmark instead of six browser tabs spread across three vendor dashboards.
Everything runs in a Docker container with a DuckDB cache. The app polls each processor API hourly, normalizes the data into a unified model, and serves a static dashboard. No external analytics service, no per-seat pricing, no vendor lock-in.
Why build this
Stripe's own dashboard is fine if you only use Stripe. Indie developers who split revenue across Stripe (subscriptions), Gumroad (digital products), and Paddle (international payments) have no single view of their business. Tools like Baremetrics, ChartMogul, and ProfitWell solve this well but cost $50–150 per month — often more than a developer's net profit in the early months.
The read-only nature of payment API keys makes this low-risk to self-host. None of the processors restrict this kind of integration. What does not exist is a well-packaged open-source version that handles multi-processor normalization, runs in a single container, and produces a clean dashboard without requiring a data engineering background.
Stack sketch
- Backend: FastAPI polling Stripe (
stripe-python), Gumroad (REST), and Paddle (paddle-python) on a configurable schedule; normalized event rows stored in DuckDB for fast analytical queries - Scheduler: APScheduler embedded in the same process — lighter than Celery for a workload that is one hourly batch job
- Dashboard: Observable Framework static site built at container startup and served as static HTML; charts are Plot.js; page auto-refreshes every 10 minutes via a
<meta>tag - Storage: DuckDB for aggregated metrics history; API keys stored in SQLite, encrypted at rest with AES-GCM via the
cryptographypackage - Auth: a single admin password (bcrypt-hashed, set via
ADMIN_PASSWORDenv var) protecting the dashboard and config pages - Config UI: a minimal HTMX + Jinja2 form for connecting processors and setting the poll interval; no separate frontend build pipeline
Scope for v1
- Connect up to three processor accounts: Stripe, Gumroad, Paddle
- Compute and display MRR, new MRR, churned MRR, active subscriber count, and average LTV
- 12-month MRR history line chart and a 30-day new-vs-churned subscriber waterfall
- Hourly background sync with a "sync now" button in the UI
- Single Docker image; only
ADMIN_PASSWORDis required to get started - Deliberately out of scope for v1: multi-user access, revenue attribution by acquisition channel, email digests, trial-to-paid conversion tracking, multi-currency normalization beyond USD
Where it could go
The most natural follow-on is a weekly email digest: a plain-text email sent every Monday with the week's MRR delta, new subscribers, cancellations, and one flagged metric that moved notably. This requires no new data collection — it is a scheduled query over existing DuckDB tables and an SMTP send. For a solo founder who opens their laptop over coffee, this is worth more than any always-on dashboard.
A second expansion worth building is cohort retention: group subscribers by the month they first converted and show the percentage still active at 1, 3, 6, and 12 months. That single heatmap tells you more about product-market fit than any aggregate MRR number. DuckDB handles the window function queries trivially, and Observable Framework renders the chart in about 15 lines of Plot.js.
Watch out for
Stripe's API is generous but Paddle's Billing API limits requests to 100 per minute. Implement exponential backoff on every processor client from the start and log every rate-limit response explicitly — a stalled sync that fails silently will look like accurate data until it obviously isn't.