Stripe knows everything about your revenue. Every charge, subscription, refund, and payout is sitting in their system. The problem is that Stripe's default dashboard gives you a narrow window into that data good for a quick check, not for the deeper questions that actually drive decisions.
What was net revenue last month after refunds? Which plan is growing fastest? What's the average subscription lifetime by acquisition channel? How does MRR this quarter compare to last? These questions aren't hard to answer, but they require getting your Stripe data into a database and analyzing it there.
This guide walks through exactly how to do that and how to build a live revenue dashboard from it without writing a line of SQL.
Getting Your Stripe Data into a Database
Stripe gives you two paths to get your data out: their API and their data pipeline product (Stripe Data).
Option 1: Stripe Data (recommended)
Stripe Data syncs your Stripe data directly to a database. It supports Snowflake, Redshift, and BigQuery natively. If you're on one of those, this is the cleanest option Stripe handles the schema, the syncs, and the incremental updates.
Option 2: Sync via API to PostgreSQL or MySQL
If you're on PostgreSQL or MySQL, you can pull the key tables using Stripe's API and insert them yourself. The tables you care most about:
charges every payment attemptsubscriptions recurring billing recordsinvoices line items and totalscustomers your subscriber baserefunds reversalsA lightweight sync for PostgreSQL might look like:
import stripe
import psycopg2
stripe.api_key = "sk_live_..."
# Pull all charges from the last 30 days
charges = stripe.Charge.list(
created={"gte": int((datetime.now() - timedelta(days=30)).timestamp())},
limit=100
)
conn = psycopg2.connect("postgresql://...")
cur = conn.cursor()
for charge in charges.auto_paging_iter():
cur.execute("""
INSERT INTO stripe_charges
(id, amount, currency, status, customer_id, created_at, refunded)
VALUES (%s, %s, %s, %s, %s, to_timestamp(%s), %s)
ON CONFLICT (id) DO UPDATE SET
status = EXCLUDED.status,
refunded = EXCLUDED.refunded
""", (
charge.id,
charge.amount / 100, # Stripe stores in cents
charge.currency,
charge.status,
charge.customer,
charge.created,
charge.refunded
))
conn.commit()Option 3: Third-party connectors
Tools like Fivetran, Airbyte, or Stitch automate the Stripe-to-database sync entirely. If you're already using one of these for other sources, adding Stripe is a few clicks.
Once your Stripe data is in a database, you're ready to build.
The Core Revenue Metrics to Track
A revenue dashboard that actually gets used answers five questions at a glance:
1. MRR (Monthly Recurring Revenue) the sum of all active subscription amounts normalized to monthly. This is your north-star number.
2. Net Revenue total charges minus refunds in a given period. Stripe's gross revenue number can be misleading if refunds are significant.
3. Churn subscriptions that cancelled in a period, expressed as a percentage of total active subscriptions at the start of that period.
4. New vs. Expansion vs. Churned MRR breaking MRR movement into its components shows whether growth is coming from new customers or expansion of existing ones.
5. Revenue by plan which tier is generating the most revenue, and which is growing fastest.
Here's the SQL for MRR from an active subscriptions table:
SELECT
DATE_TRUNC('month', current_date) AS month,
SUM(
CASE
WHEN billing_interval = 'month' THEN amount
WHEN billing_interval = 'year' THEN amount / 12
END
) AS mrr
FROM stripe_subscriptions
WHERE status = 'active';And monthly net revenue (charges minus refunds):
SELECT
DATE_TRUNC('month', created_at) AS month,
SUM(amount) AS gross_revenue,
SUM(amount_refunded) AS refunds,
SUM(amount) - SUM(amount_refunded) AS net_revenue
FROM stripe_charges
WHERE status = 'succeeded'
GROUP BY 1
ORDER BY 1 DESC;Building the Dashboard Without Writing SQL
If you're a SaaS founder, a RevOps analyst, or a product manager, you probably don't want to maintain a library of SQL queries. You want to open a dashboard and see your numbers.
AI for Database connects directly to your database whether that's PostgreSQL, MySQL, BigQuery, Snowflake, or Supabase and lets you ask revenue questions in plain English. The system translates your question into SQL, runs it, and returns a chart or table.
For a Stripe revenue dashboard, you'd ask questions like:
Each question becomes a panel in your dashboard. You arrange them, give them titles, and set the refresh interval hourly, daily, weekly, whatever makes sense for your business cadence.
The result is a live revenue dashboard built from your actual Stripe data, not a static export or a screenshot from Stripe's interface.
Tracking MRR Movement: New, Expansion, and Churn
The most valuable revenue analysis isn't just the total MRR number it's understanding where it came from and where it went.
MRR movement breaks into four buckets:
To calculate this from a subscriptions table, you compare each customer's subscription amount this month to last month:
WITH monthly_sub AS (
SELECT
customer_id,
DATE_TRUNC('month', created_at) AS month,
amount,
LAG(amount) OVER (PARTITION BY customer_id ORDER BY DATE_TRUNC('month', created_at)) AS prev_amount,
LAG(status) OVER (PARTITION BY customer_id ORDER BY DATE_TRUNC('month', created_at)) AS prev_status
FROM stripe_subscriptions
WHERE status IN ('active', 'canceled')
)
SELECT
month,
SUM(CASE WHEN prev_amount IS NULL AND amount > 0 THEN amount ELSE 0 END) AS new_mrr,
SUM(CASE WHEN prev_amount IS NOT NULL AND amount > prev_amount THEN amount - prev_amount ELSE 0 END) AS expansion_mrr,
SUM(CASE WHEN prev_amount IS NOT NULL AND amount < prev_amount AND amount > 0 THEN amount - prev_amount ELSE 0 END) AS contraction_mrr,
SUM(CASE WHEN prev_status = 'active' AND status = 'canceled' THEN -prev_amount ELSE 0 END) AS churned_mrr
FROM monthly_sub
GROUP BY 1
ORDER BY 1 DESC;This query is the foundation of a proper MRR waterfall chart. In AI for Database, you can get this breakdown by just asking: "Show me new, expansion, and churned MRR by month for the last 12 months."
Monitoring Churn and Subscription Health
Churn is the metric that kills SaaS companies slowly. Tracking it at the aggregate level is table stakes. The more useful analysis is understanding who churns and when.
Time-to-churn by plan do annual subscribers churn at renewal? Do monthly subscribers leave after 2-3 months?
SELECT
plan_name,
ROUND(AVG(
EXTRACT(DAY FROM (cancelled_at - created_at))
), 0) AS avg_days_to_cancel
FROM stripe_subscriptions
WHERE status = 'canceled'
AND cancelled_at IS NOT NULL
GROUP BY plan_name
ORDER BY avg_days_to_cancel;Cohort retention for customers who started in a given month, what percentage are still active after 1, 3, 6, and 12 months?
This requires a cohort analysis query, which is more involved but extremely valuable. In AI for Database, you can ask "Show me 12-month subscription retention by signup cohort" and it generates the query automatically against your Stripe data.
Cancellation timing are customers cancelling at the end of a billing period or mid-cycle? Mid-cycle cancellations often indicate a specific pain point (a failed integration, a billing dispute, a competitive loss). End-of-period cancellations are more often about budget or product-market fit.
Segmenting Revenue for Strategic Decisions
Once your Stripe data is queryable, segmentation opens up. A few questions worth having in your dashboard:
Revenue by geography if Stripe collects billing country, you can see which markets are generating real revenue vs. which you're investing in speculatively.
Revenue by acquisition source if you're passing a metadata field on Stripe customers (utm_source, referral code, plan selected during signup), you can trace revenue back to marketing channels. This turns your Stripe data into a lightweight attribution model.
Failed payment recovery how much revenue is at risk from failed charges? How effective is your dunning sequence?
SELECT
DATE_TRUNC('month', created_at) AS month,
COUNT(*) FILTER (WHERE status = 'failed') AS failed_charges,
SUM(amount) FILTER (WHERE status = 'failed') AS failed_revenue,
COUNT(*) FILTER (WHERE status = 'succeeded' AND previous_status = 'failed') AS recovered_charges,
ROUND(
COUNT(*) FILTER (WHERE status = 'succeeded' AND previous_status = 'failed')::numeric /
NULLIF(COUNT(*) FILTER (WHERE status = 'failed'), 0) * 100, 1
) AS recovery_rate_pct
FROM stripe_charges
GROUP BY 1
ORDER BY 1 DESC;Setting Up Revenue Alerts
A dashboard you check once a week is useful. An alert that tells you when something important happens is better.
AI for Database's action workflows let you define conditions on your Stripe data and fire notifications when they're met. Examples:
These run on a schedule against your live database. No custom code, no cron jobs, no maintaining a separate alerting infrastructure.