DEV Community

Cover image for I Built a Stripe Analytics Dashboard That Never keep Your Data: Here's the Architecture Behind It
Jules
Jules

Posted on

I Built a Stripe Analytics Dashboard That Never keep Your Data: Here's the Architecture Behind It

For months, I tracked my SaaS metrics from Stripe in a spreadsheet.

Every month I'd export CSVs from Stripe, paste them into Google Sheets, and manually calculate MRR, churn, and net revenue retention. The formulas kept breaking. The numbers never matched what Stripe showed. And every month I told myself I'd automate it next time.

I looked at the existing tools. ChartMogul, Baremetrics, ProfitWell. They all do the job. But they all share one architectural decision that bothered me:
They copy your entire Stripe billing history onto their servers.

Every customer name, every payment amount, every subscription change, every refund — duplicated and stored indefinitely on infrastructure you don't control.

As a solo founder running a small SaaS on Stripe, giving a third-party tool permanent access to my most sensitive business data felt like a bigger trade-off than it needed to be.

So I built NoNoiseMetrics, and I made a different architectural choice.

The Core Idea: Session-Only Data
The central constraint I set for myself was simple:

Raw Stripe data should exist on my servers only while the user is actively looking at it.

When you connect NoNoiseMetrics to Stripe, here's what happens:

OAuth with read-only scope. You authorize via Stripe's OAuth flow with restricted permissions. NoNoiseMetrics can read subscription and payment data. It cannot create charges, modify subscriptions, issue refunds, or access bank account details.

Session-scoped data fetching. During your active session, the app calls the Stripe API, pulls the relevant objects (subscriptions, invoices, charges), and computes your metrics in real time.

Aggregates stored, raw data wiped. When your session ends, raw Stripe objects are discarded. Only computed metrics (MRR value, churn percentage, NRR, etc.) are persisted for historical tracking.

One-click disconnect. Revoke access anytime from your Stripe dashboard. There's nothing to export, nothing to migrate, nothing left behind.

This isn't a privacy policy. It's the architecture.

  • Why This Was Harder Than It Sounds

The obvious downside of not storing raw data is that you can't query it later. Every analytics tool in this space keeps a full copy specifically because it makes everything easier: historical recalculations, cohort comparisons, retroactive metric changes.

I had to solve several problems differently.

  • Problem 1: Historical Metrics Without Historical Data If I don't store raw Stripe objects, how do I show MRR trends over the past 6 months?

The answer: compute and snapshot on each session. Every time a user logs in, NoNoiseMetrics calculates the current state of all metrics and stores the computed values with a timestamp. Over time, these snapshots build up a historical series.

The trade-off is real. If a user doesn't log in for two months, there's a gap in their trend line. I decided that's acceptable for the target audience — indie hackers and solopreneurs who check their metrics at least weekly.

For the first session (when there's no history yet), the app does a deeper Stripe API pull to backfill key metrics from available invoice and subscription data, computes the aggregates, and then discards the raw objects.

  • Problem 2: Multi-Account Aggregation A surprising number of bootstrapped founders run 2–3 Stripe accounts (one per product). They want to see combined MRR across all of them.

This meant the session layer had to handle multiple concurrent Stripe API connections, normalize the data into a common schema, compute unified metrics, and then discard all raw data from every account when the session ends.

The key abstraction here was a StripeAccountContext that wraps each connected account's API calls and exposes a normalized interface. The aggregation layer doesn't care which account a subscription came from — it works with the normalized shape.

interface NormalizedSubscription {
id: string;
accountId: string;
status: SubscriptionStatus;
currency: string;
mrrCents: number;
startedAt: Date;
canceledAt: Date | null;
interval: "month" | "year";
}

Keeping this interface tight made it possible to write the metric calculation logic once and have it work across single and multi-account setups.

  • Problem 3: Stripe API Rate Limits Stripe's API has rate limits (25 read requests/second in live mode for most accounts). When computing metrics for an account with thousands of subscriptions, you can't just fire off requests in parallel and hope for the best.

I implemented a simple token-bucket rate limiter that sits in front of all Stripe API calls. It's not sophisticated — just a counter with a refill interval — but it prevents 429 errors and keeps the session-scoped fetch reliable.

class RateLimiter {
private tokens: number;
private maxTokens: number;
private refillRate: number;
private lastRefill: number;

async acquire(): Promise {
this.refill();
if (this.tokens <= 0) {
const waitTime = (1 / this.refillRate) * 1000;
await new Promise((r) => setTimeout(r, waitTime));
this.refill();
}
this.tokens--;
}

private refill(): void {
const now = Date.now();
const elapsed = (now - this.lastRefill) / 1000;
this.tokens = Math.min(this.maxTokens, this.tokens + elapsed * this.refillRate);
this.lastRefill = now;
}
}

Nothing fancy. But it was one of those things where getting it wrong would silently break the entire user experience.

The Metric Calculations

The metrics themselves are less trivial than they look. MRR sounds simple until you deal with:

  • Annual subscriptions that need to be normalized to monthly
  • Multi-currency accounts (a founder in Europe with USD and EUR customers)
  • Prorations from mid-cycle plan changes
  • Trial periods that shouldn't count as active MRR
  • Paused subscriptions that Stripe still reports as active

Each edge case is a small if branch that's easy to get wrong. I chose to handle these explicitly rather than building an abstract "subscription normalizer" — similar to the reasoning in this post about not over-abstracting scheduling algorithms. When the logic itself is the product, keeping it readable matters more than keeping it DRY.

MRR waterfall breakdown (new, expansion, contraction, churn, reactivation) was the trickiest piece. It requires comparing the current state of every subscription against its state in the previous period, classifying the delta, and summing by category. Getting this right took more iterations than any other part of the codebase.

Tech Stack

  • Frontend: React on Next.js, deployed on Vercel
  • Backend: Node.js on Railway
  • Database: Supabase (EU region — important for GDPR)
  • Payments: Stripe
  • Email automation: Loops.so
  • Styling: Tailwind CSS
  • Validation: Zod

Why This Stack
I'm a solo founder. Budget is close to zero while the product finds its market. Every choice was driven by "what can I ship and maintain alone without going broke?"

Vercel + Railway gives me separate scaling for frontend and backend without managing infrastructure.
Supabase is Postgres with auth and real-time features I might need later, and hosting in EU keeps GDPR simple.
Zod for runtime validation of Stripe API responses was a decision I'm particularly glad I made early. Stripe's API is well-typed, but parsing real-world webhook payloads and API responses revealed enough edge cases that having a validation layer caught bugs I wouldn't have found otherwise.

  • What I Didn't Build (On Purpose) One of the hardest parts of building a product alone is deciding what to leave out.

NoNoiseMetrics doesn't have:

  • A CRM. ChartMogul has one. My users don't need one inside their analytics tool.
  • Dunning / failed payment recovery. Baremetrics charges $129/mo extra for this. It's a separate product category.
  • Cancellation surveys. Same logic. Useful, but out of scope.
  • 47 chart types. The dashboard shows 8 core metrics. MRR, ARR, churn (revenue + customer), NRR, ARPU, LTV, and cohort analysis. That covers what a bootstrapped founder should check weekly.

Every feature I didn't build is one less thing to maintain, one less surface area for bugs, and one less reason for the interface to feel overwhelming.
The indie hackers I talk to don't want a "powerful analytics platform." They want to connect Stripe, see their numbers, and get back to building their product.

What I Actually Learned

  • Stripe's API is excellent but deep
    The documentation is some of the best in the industry. But the object model is more complex than it looks from the outside. A single subscription can have multiple items, each with different prices, coupons, tax rates, and billing anchors. Getting metric calculations right required reading the Stripe docs more carefully than I've read almost any technical documentation.

  • Session-only architecture forces better engineering
    When you can't fall back on "just query the database," you're forced to think carefully about what you compute, when you compute it, and what you persist. It's a constraint that made the codebase simpler, not more complex.

  • The privacy angle resonates more than expected
    -When I explain the architecture to other founders, the most common reaction is: "Wait, the other tools store all my data?" Most people don't read the fine print. Knowing that your financial data is never copied to a third-party server is a relief that's hard to quantify but easy to feel.

Solo founder ≠ no feedback loop

I can't afford a QA team, but I can afford to be obsessive about Zod validation, TypeScript strict mode, and testing metric calculations against known Stripe test data. The type system is the cheapest co-worker you'll ever have.

Try It

If you're running a SaaS on Stripe and you want clean metrics without handing over your billing data:
🔗 Nonoisemetrics

Free up to €10K MRR. €19/mo after that.

What's Next

  • AI-powered weekly digest (plain-language summary of what changed in your metrics — delivered via email, no data stored)
  • Improved first-session backfill for faster time-to-insight
  • Public blog with SaaS metric guides (MRR, churn, NRR explainers)
  • Automated tests for every metric calculation against Stripe's test clock

If you have questions about the architecture, the Stripe API edge cases, or building a SaaS analytics tool as a solo founder, happy to discuss in the comments.

The best side projects are the ones where the constraint teaches you more than the feature set.

Top comments (0)