How to Build a RevenueCat Dashboard (Without Building a Frontend)
What you'll build
A live RevenueCat dashboard with five widgets:
MRR (KPI) — current monthly recurring revenue
Active subscribers (KPI) — current count
MRR by platform (bar chart) — iOS vs. Android revenue breakdown
Subscription events (line chart) — new vs. cancelled per day, last 30 days
Top revenue products (table) — your highest-grossing SKUs
You'll write a few endpoints in your existing backend that wrap RevenueCat's REST API, then point dashboardbase at them. Total time: ~20 minutes if you already have RevenueCat webhooks set up.
Why not just use RevenueCat's own dashboard?
RevenueCat's dashboard is genuinely good — far better than App Store Connect's own analytics. So why build another one?
Reasons mobile founders build a custom dashboard on top of RevenueCat:
Combine RevenueCat data with your own product data — e.g. "MRR vs. weekly active users vs. crash-free sessions"
Mobile-first viewing — RevenueCat's dashboard is web-only; you want this on your phone with one tap
Share with co-founders or investors — without giving them RevenueCat seat access
Custom MRR definitions — exclude trials, exclude specific SKUs, weight by lifecycle stage
Combine iOS, Android, web subscriptions in one view with custom grouping
If none of those apply, just use RevenueCat's dashboard. If two or more apply, keep reading.
Prerequisites
A RevenueCat account with active subscriptions
A RevenueCat REST API v2 secret key (Project Settings → API keys)
A backend in any language (examples in Node.js)
A dashboardbase account
Step 1: The MRR endpoint
The KPI widget contract:
{ "title": "MRR", "actions": [ { "title": "View Details", "type": "link", "url": "https://example.com/mrr-details" } ], "data": { "header": { "title": "$8,420", "subtitle": "vs last month", "badge": { "text": "+8.2%", "icon": "ArrowUp", "color": "Success" } } } }
The endpoint:
// GET /dashboards/revenuecat/mrr app.get('/dashboards/revenuecat/mrr', async (req, res) => { const response = await fetch( `https://api.revenuecat.com/v2/projects/${PROJECT_ID}/metrics/overview`, { headers: { Authorization: `Bearer ${process.env.REVENUECAT_SECRET}` } } ); const data = await response.json(); const currentMrr = data.metrics.mrr.current; const previousMrr = data.metrics.mrr.previous; // 30 days ago const trend = previousMrr ? ((currentMrr - previousMrr) / previousMrr * 100).toFixed(1) : 0; const up = trend >= 0; res.json({ title: 'MRR', actions: [ { title: 'View Details', type: 'link', url: 'https://example.com/mrr-details' }, ], data: { header: { title: `$${Math.round(currentMrr).toLocaleString('en-US')}`, subtitle: 'vs last month', badge: { text: `${up ? '+' : ''}${trend}%`, icon: up ? 'ArrowUp' : 'ArrowDown', color: up ? 'Success' : 'Danger', }, }, }, }); });
Note: RevenueCat's REST API v2 returns MRR aggregated and pre-computed. You don't need to do the math yourself. If you want a custom MRR definition (excluding trials, etc.), compute it from raw subscriber data instead.
You don't have to hand-write the remaining four. Two things make this faster:
Generating the endpoint. The free dashboardbase Skill teaches Claude Code, Cursor or any skills-capable agent the full JSON contract, so "wrap RevenueCat's MRR breakdown as a bar chart endpoint" comes back in the right shape rather than an invented one. No Skill install? The widget editor generates a copy-pasteable prompt — per widget, or one covering the whole dashboard — for whatever AI tool you use. It's a draft you review and run yourself.
Checking it. Paste the response into the endpoint validator and it says whether that widget type can render your JSON, with a live preview. It's public — no account required — so it's also the fastest way to sanity-check a shape before you deploy.
Step 2: Active subscribers KPI
app.get('/dashboards/revenuecat/active-subscribers', async (req, res) => { const response = await fetch( `https://api.revenuecat.com/v2/projects/${PROJECT_ID}/metrics/overview`, { headers: { Authorization: `Bearer ${process.env.REVENUECAT_SECRET}` } } ); const data = await response.json(); const active = data.metrics.active_subscriptions.current; const delta = data.metrics.active_subscriptions.delta || 0; const up = delta >= 0; res.json({ title: 'Active subscribers', data: { header: { title: active.toLocaleString('en-US'), subtitle: 'vs last month', badge: { text: `${up ? '+' : ''}${delta}`, icon: up ? 'ArrowUp' : 'ArrowDown', color: up ? 'Success' : 'Danger', }, }, }, }); });
Step 3: MRR by platform (bar chart)
The bar chart contract:
{ "title": "MRR by platform", "actions": [ { "title": "View Details", "type": "link", "url": "https://example.com/mrr-by-platform" } ], "data": { "header": { "title": "$8,420", "subtitle": "Last 30 days", "badge": { "text": "+$640", "icon": "ArrowUp", "color": "Success" } }, "labels": [ "iOS", "Android", "Web" ], "datasets": [ { "data": [ { "value": 5240 }, { "value": 2890 }, { "value": 290 } ], "label": "MRR" } ] } }
The platform names go in labels; the numbers go in datasets[].data as objects with a value. Add "indexAxis": "y" inside data if you'd rather render the bars horizontally.
app.get('/dashboards/revenuecat/mrr-by-platform', async (req, res) => { const response = await fetch( `https://api.revenuecat.com/v2/projects/${PROJECT_ID}/metrics/mrr/breakdown?dimension=store`, { headers: { Authorization: `Bearer ${process.env.REVENUECAT_SECRET}` } } ); const data = await response.json(); const platform = store => store === 'app_store' ? 'iOS' : store === 'play_store' ? 'Android' : store; const total = data.breakdown.reduce((sum, row) => sum + row.mrr, 0); res.json({ title: 'MRR by platform', actions: [ { title: 'View Details', type: 'link', url: 'https://example.com/mrr-by-platform' }, ], data: { header: { title: `$${Math.round(total).toLocaleString('en-US')}`, subtitle: 'Last 30 days', }, labels: data.breakdown.map(row => platform(row.store)), datasets: [{ data: data.breakdown.map(row => ({ value: Math.round(row.mrr) })), label: 'MRR', }], }, }); });
This is the kind of view RevenueCat's own dashboard makes you click through several screens for. Worth having as a one-glance widget.
Step 4: Subscription events over time
The line chart needs new vs. cancelled subscriptions per day. The cleanest source for this is your own database, populated by RevenueCat webhooks. If you're not consuming RevenueCat webhooks yet, set that up first — it's the foundation for almost any custom analytics work on top of RevenueCat.
app.get('/dashboards/revenuecat/subscription-events', async (req, res) => { const days = parseInt(req.query.days) || 30; const since = subDays(new Date(), days); const events = await db.revenuecatEvents.findMany({ where: { type: { in: ['INITIAL_PURCHASE', 'CANCELLATION'] }, createdAt: { gte: since }, }, }); // Bucket by day const buckets = {}; for (const event of events) { const day = event.createdAt.toISOString().slice(0, 10); if (!buckets[day]) buckets[day] = { new: 0, cancelled: 0 }; if (event.type === 'INITIAL_PURCHASE') buckets[day].new++; else buckets[day].cancelled++; } const days_sorted = Object.keys(buckets).sort(); const totalNew = days_sorted.reduce((sum, d) => sum + buckets[d].new, 0); res.json({ title: 'Subscription events', data: { header: { title: totalNew.toLocaleString('en-US'), subtitle: `New subscriptions, last ${days} days`, }, labels: days_sorted, datasets: [ { data: days_sorted.map(d => ({ value: buckets[d].new })), label: 'New', }, { data: days_sorted.map(d => ({ value: buckets[d].cancelled })), label: 'Cancelled', }, ], }, }); });
This endpoint accepts a days query param so it works with dashboardbase's date range selector.
Step 5: Top revenue products (table)
app.get('/dashboards/revenuecat/top-products', async (req, res) => { const response = await fetch( `https://api.revenuecat.com/v2/projects/${PROJECT_ID}/metrics/revenue/breakdown?dimension=product`, { headers: { Authorization: `Bearer ${process.env.REVENUECAT_SECRET}` } } ); const data = await response.json(); const rows = data.breakdown .sort((a, b) => b.revenue - a.revenue) .slice(0, 10) .map(row => { const ios = row.store === 'app_store'; return [ { text: row.product_id }, { text: ios ? 'iOS' : 'Android', badge: { text: ios ? 'iOS' : 'Android', color: ios ? 'Dark' : 'Success' }, }, { text: `$${Math.round(row.revenue).toLocaleString('en-US')}` }, { text: row.active_subscribers.toString() }, ]; }); res.json({ title: 'Top revenue products', actions: [ { title: 'View All', type: 'link', url: 'https://example.com/products' }, ], data: { headers: [ { text: 'Product', width: 40 }, { text: 'Platform', width: 20 }, { text: 'Revenue (30d)', width: 25 }, { text: 'Active', width: 15 }, ], rows, }, }); });
Step 6: Wire it up in dashboardbase
Sign in, create a new dashboard, name it "RevenueCat."
Click Add datasource and paste the URL of your first endpoint. Your workspace's endpoint secret is already attached — it shows as a read-only row above your own headers. Your RevenueCat key never goes in here; it stays in your backend.
Test the datasource — JSON response shows up.
Drop a KPI widget on the grid, point it at the MRR datasource.
Repeat for the other four widgets.
Set refresh interval to 5 minutes. Publish.
Open it on your phone via the dashboardbase iOS or Android app.
That's the dashboard you'd otherwise have built a separate React Native app to view.
The mobile founder angle
If you're shipping a mobile app, you check your numbers from your phone. That's not a nice-to-have — it's how you actually run the business between meetings, on the train, in line at coffee.
The combination here genuinely matters:
RevenueCat owns the source-of-truth subscription data
Your backend wraps it into the exact shape you want
dashboardbase renders it natively on your phone with push notifications
You can get something close to this with RevenueCat's own dashboard on mobile web, but it'll never feel as good as a native widget you can deep-link into from a notification.
Securing the endpoints
These endpoints expose subscription revenue. Two secrets are in play and it's worth keeping them straight:
Your RevenueCat key authenticates you to RevenueCat. It lives in your backend and never leaves it.
Your dashboardbase endpoint secret authenticates dashboardbase to your endpoint.
That second one is the one you configure here, and it needs almost no work. Every workspace has a single endpoint secret, and dashboardbase sends it as an x-dashboardbase-secret header on every request. You compare one string:
const SECRET = process.env.DASHBOARDBASE_ENDPOINT_SECRET // Fail at boot rather than serving subscriber data unprotected. if (!SECRET) throw new Error('DASHBOARDBASE_ENDPOINT_SECRET is not set') app.use('/dashboard', (req, res, next) => { if (req.get('x-dashboardbase-secret') !== SECRET) { return res.status(401).json({ error: 'unauthorized' }) } next() })
Find the value under Settings → Secrets → Endpoint Secret (or /workspace), masked with a reveal toggle and the environment variable name beside it. Rotating it keeps the old value valid for 24 hours so nothing breaks mid-deploy.
Prefer something you already have? Custom headers and Basic Auth are both supported and stack on top of the secret rather than replacing it.
And the validator checks reality, not intent. Testing a datasource fires one extra request with every credential stripped, then reports what came back — a green "Protected — returns 401 without the secret", or a yellow warning naming the header to look at. It won't stop you saving; it just won't let an open endpoint look closed.
Otherwise: no tokens in the URL, HTTPS only.
Common gotchas
Trial users. RevenueCat counts trials as "active subscribers" by default. Decide whether you want them in your dashboard's MRR.
Refunds and chargebacks. RevenueCat's MRR endpoint already accounts for these — but if you compute MRR yourself, don't forget.
Currency conversion. Multi-currency apps need to pick a reporting currency. Pick one in your endpoint and stay consistent.
iOS vs. Android revenue isn't apples-to-apples. Apple takes 30% (or 15% for small developers), Google's similar. Decide whether your dashboard shows gross or net.
API rate limits. RevenueCat's REST API v2 has rate limits. Cache responses for 60–300 seconds in your endpoint; dashboardbase polls at your refresh interval.
Next steps
Add a churn rate gauge widget.
Add a "trial-to-paid conversion" KPI.
Add a cohort retention table (a heavier endpoint, but doable).
Add a webhook trigger so your dashboard pings you when MRR crosses a milestone.
Each one is another small endpoint. The dashboard grows; the architecture stays simple.
How to Build a RevenueCat Dashboard (Without Building a Frontend)
What you'll build
A live RevenueCat dashboard with five widgets:
MRR (KPI) — current monthly recurring revenue
Active subscribers (KPI) — current count
MRR by platform (bar chart) — iOS vs. Android revenue breakdown
Subscription events (line chart) — new vs. cancelled per day, last 30 days
Top revenue products (table) — your highest-grossing SKUs
You'll write a few endpoints in your existing backend that wrap RevenueCat's REST API, then point dashboardbase at them. Total time: ~20 minutes if you already have RevenueCat webhooks set up.
Why not just use RevenueCat's own dashboard?
RevenueCat's dashboard is genuinely good — far better than App Store Connect's own analytics. So why build another one?
Reasons mobile founders build a custom dashboard on top of RevenueCat:
Combine RevenueCat data with your own product data — e.g. "MRR vs. weekly active users vs. crash-free sessions"
Mobile-first viewing — RevenueCat's dashboard is web-only; you want this on your phone with one tap
Share with co-founders or investors — without giving them RevenueCat seat access
Custom MRR definitions — exclude trials, exclude specific SKUs, weight by lifecycle stage
Combine iOS, Android, web subscriptions in one view with custom grouping
If none of those apply, just use RevenueCat's dashboard. If two or more apply, keep reading.
Prerequisites
A RevenueCat account with active subscriptions
A RevenueCat REST API v2 secret key (Project Settings → API keys)
A backend in any language (examples in Node.js)
A dashboardbase account
Step 1: The MRR endpoint
The KPI widget contract:
{ "title": "MRR", "actions": [ { "title": "View Details", "type": "link", "url": "https://example.com/mrr-details" } ], "data": { "header": { "title": "$8,420", "subtitle": "vs last month", "badge": { "text": "+8.2%", "icon": "ArrowUp", "color": "Success" } } } }
The endpoint:
// GET /dashboards/revenuecat/mrr app.get('/dashboards/revenuecat/mrr', async (req, res) => { const response = await fetch( `https://api.revenuecat.com/v2/projects/${PROJECT_ID}/metrics/overview`, { headers: { Authorization: `Bearer ${process.env.REVENUECAT_SECRET}` } } ); const data = await response.json(); const currentMrr = data.metrics.mrr.current; const previousMrr = data.metrics.mrr.previous; // 30 days ago const trend = previousMrr ? ((currentMrr - previousMrr) / previousMrr * 100).toFixed(1) : 0; const up = trend >= 0; res.json({ title: 'MRR', actions: [ { title: 'View Details', type: 'link', url: 'https://example.com/mrr-details' }, ], data: { header: { title: `$${Math.round(currentMrr).toLocaleString('en-US')}`, subtitle: 'vs last month', badge: { text: `${up ? '+' : ''}${trend}%`, icon: up ? 'ArrowUp' : 'ArrowDown', color: up ? 'Success' : 'Danger', }, }, }, }); });
Note: RevenueCat's REST API v2 returns MRR aggregated and pre-computed. You don't need to do the math yourself. If you want a custom MRR definition (excluding trials, etc.), compute it from raw subscriber data instead.
You don't have to hand-write the remaining four. Two things make this faster:
Generating the endpoint. The free dashboardbase Skill teaches Claude Code, Cursor or any skills-capable agent the full JSON contract, so "wrap RevenueCat's MRR breakdown as a bar chart endpoint" comes back in the right shape rather than an invented one. No Skill install? The widget editor generates a copy-pasteable prompt — per widget, or one covering the whole dashboard — for whatever AI tool you use. It's a draft you review and run yourself.
Checking it. Paste the response into the endpoint validator and it says whether that widget type can render your JSON, with a live preview. It's public — no account required — so it's also the fastest way to sanity-check a shape before you deploy.
Step 2: Active subscribers KPI
app.get('/dashboards/revenuecat/active-subscribers', async (req, res) => { const response = await fetch( `https://api.revenuecat.com/v2/projects/${PROJECT_ID}/metrics/overview`, { headers: { Authorization: `Bearer ${process.env.REVENUECAT_SECRET}` } } ); const data = await response.json(); const active = data.metrics.active_subscriptions.current; const delta = data.metrics.active_subscriptions.delta || 0; const up = delta >= 0; res.json({ title: 'Active subscribers', data: { header: { title: active.toLocaleString('en-US'), subtitle: 'vs last month', badge: { text: `${up ? '+' : ''}${delta}`, icon: up ? 'ArrowUp' : 'ArrowDown', color: up ? 'Success' : 'Danger', }, }, }, }); });
Step 3: MRR by platform (bar chart)
The bar chart contract:
{ "title": "MRR by platform", "actions": [ { "title": "View Details", "type": "link", "url": "https://example.com/mrr-by-platform" } ], "data": { "header": { "title": "$8,420", "subtitle": "Last 30 days", "badge": { "text": "+$640", "icon": "ArrowUp", "color": "Success" } }, "labels": [ "iOS", "Android", "Web" ], "datasets": [ { "data": [ { "value": 5240 }, { "value": 2890 }, { "value": 290 } ], "label": "MRR" } ] } }
The platform names go in labels; the numbers go in datasets[].data as objects with a value. Add "indexAxis": "y" inside data if you'd rather render the bars horizontally.
app.get('/dashboards/revenuecat/mrr-by-platform', async (req, res) => { const response = await fetch( `https://api.revenuecat.com/v2/projects/${PROJECT_ID}/metrics/mrr/breakdown?dimension=store`, { headers: { Authorization: `Bearer ${process.env.REVENUECAT_SECRET}` } } ); const data = await response.json(); const platform = store => store === 'app_store' ? 'iOS' : store === 'play_store' ? 'Android' : store; const total = data.breakdown.reduce((sum, row) => sum + row.mrr, 0); res.json({ title: 'MRR by platform', actions: [ { title: 'View Details', type: 'link', url: 'https://example.com/mrr-by-platform' }, ], data: { header: { title: `$${Math.round(total).toLocaleString('en-US')}`, subtitle: 'Last 30 days', }, labels: data.breakdown.map(row => platform(row.store)), datasets: [{ data: data.breakdown.map(row => ({ value: Math.round(row.mrr) })), label: 'MRR', }], }, }); });
This is the kind of view RevenueCat's own dashboard makes you click through several screens for. Worth having as a one-glance widget.
Step 4: Subscription events over time
The line chart needs new vs. cancelled subscriptions per day. The cleanest source for this is your own database, populated by RevenueCat webhooks. If you're not consuming RevenueCat webhooks yet, set that up first — it's the foundation for almost any custom analytics work on top of RevenueCat.
app.get('/dashboards/revenuecat/subscription-events', async (req, res) => { const days = parseInt(req.query.days) || 30; const since = subDays(new Date(), days); const events = await db.revenuecatEvents.findMany({ where: { type: { in: ['INITIAL_PURCHASE', 'CANCELLATION'] }, createdAt: { gte: since }, }, }); // Bucket by day const buckets = {}; for (const event of events) { const day = event.createdAt.toISOString().slice(0, 10); if (!buckets[day]) buckets[day] = { new: 0, cancelled: 0 }; if (event.type === 'INITIAL_PURCHASE') buckets[day].new++; else buckets[day].cancelled++; } const days_sorted = Object.keys(buckets).sort(); const totalNew = days_sorted.reduce((sum, d) => sum + buckets[d].new, 0); res.json({ title: 'Subscription events', data: { header: { title: totalNew.toLocaleString('en-US'), subtitle: `New subscriptions, last ${days} days`, }, labels: days_sorted, datasets: [ { data: days_sorted.map(d => ({ value: buckets[d].new })), label: 'New', }, { data: days_sorted.map(d => ({ value: buckets[d].cancelled })), label: 'Cancelled', }, ], }, }); });
This endpoint accepts a days query param so it works with dashboardbase's date range selector.
Step 5: Top revenue products (table)
app.get('/dashboards/revenuecat/top-products', async (req, res) => { const response = await fetch( `https://api.revenuecat.com/v2/projects/${PROJECT_ID}/metrics/revenue/breakdown?dimension=product`, { headers: { Authorization: `Bearer ${process.env.REVENUECAT_SECRET}` } } ); const data = await response.json(); const rows = data.breakdown .sort((a, b) => b.revenue - a.revenue) .slice(0, 10) .map(row => { const ios = row.store === 'app_store'; return [ { text: row.product_id }, { text: ios ? 'iOS' : 'Android', badge: { text: ios ? 'iOS' : 'Android', color: ios ? 'Dark' : 'Success' }, }, { text: `$${Math.round(row.revenue).toLocaleString('en-US')}` }, { text: row.active_subscribers.toString() }, ]; }); res.json({ title: 'Top revenue products', actions: [ { title: 'View All', type: 'link', url: 'https://example.com/products' }, ], data: { headers: [ { text: 'Product', width: 40 }, { text: 'Platform', width: 20 }, { text: 'Revenue (30d)', width: 25 }, { text: 'Active', width: 15 }, ], rows, }, }); });
Step 6: Wire it up in dashboardbase
Sign in, create a new dashboard, name it "RevenueCat."
Click Add datasource and paste the URL of your first endpoint. Your workspace's endpoint secret is already attached — it shows as a read-only row above your own headers. Your RevenueCat key never goes in here; it stays in your backend.
Test the datasource — JSON response shows up.
Drop a KPI widget on the grid, point it at the MRR datasource.
Repeat for the other four widgets.
Set refresh interval to 5 minutes. Publish.
Open it on your phone via the dashboardbase iOS or Android app.
That's the dashboard you'd otherwise have built a separate React Native app to view.
The mobile founder angle
If you're shipping a mobile app, you check your numbers from your phone. That's not a nice-to-have — it's how you actually run the business between meetings, on the train, in line at coffee.
The combination here genuinely matters:
RevenueCat owns the source-of-truth subscription data
Your backend wraps it into the exact shape you want
dashboardbase renders it natively on your phone with push notifications
You can get something close to this with RevenueCat's own dashboard on mobile web, but it'll never feel as good as a native widget you can deep-link into from a notification.
Securing the endpoints
These endpoints expose subscription revenue. Two secrets are in play and it's worth keeping them straight:
Your RevenueCat key authenticates you to RevenueCat. It lives in your backend and never leaves it.
Your dashboardbase endpoint secret authenticates dashboardbase to your endpoint.
That second one is the one you configure here, and it needs almost no work. Every workspace has a single endpoint secret, and dashboardbase sends it as an x-dashboardbase-secret header on every request. You compare one string:
const SECRET = process.env.DASHBOARDBASE_ENDPOINT_SECRET // Fail at boot rather than serving subscriber data unprotected. if (!SECRET) throw new Error('DASHBOARDBASE_ENDPOINT_SECRET is not set') app.use('/dashboard', (req, res, next) => { if (req.get('x-dashboardbase-secret') !== SECRET) { return res.status(401).json({ error: 'unauthorized' }) } next() })
Find the value under Settings → Secrets → Endpoint Secret (or /workspace), masked with a reveal toggle and the environment variable name beside it. Rotating it keeps the old value valid for 24 hours so nothing breaks mid-deploy.
Prefer something you already have? Custom headers and Basic Auth are both supported and stack on top of the secret rather than replacing it.
And the validator checks reality, not intent. Testing a datasource fires one extra request with every credential stripped, then reports what came back — a green "Protected — returns 401 without the secret", or a yellow warning naming the header to look at. It won't stop you saving; it just won't let an open endpoint look closed.
Otherwise: no tokens in the URL, HTTPS only.
Common gotchas
Trial users. RevenueCat counts trials as "active subscribers" by default. Decide whether you want them in your dashboard's MRR.
Refunds and chargebacks. RevenueCat's MRR endpoint already accounts for these — but if you compute MRR yourself, don't forget.
Currency conversion. Multi-currency apps need to pick a reporting currency. Pick one in your endpoint and stay consistent.
iOS vs. Android revenue isn't apples-to-apples. Apple takes 30% (or 15% for small developers), Google's similar. Decide whether your dashboard shows gross or net.
API rate limits. RevenueCat's REST API v2 has rate limits. Cache responses for 60–300 seconds in your endpoint; dashboardbase polls at your refresh interval.
Next steps
Add a churn rate gauge widget.
Add a "trial-to-paid conversion" KPI.
Add a cohort retention table (a heavier endpoint, but doable).
Add a webhook trigger so your dashboard pings you when MRR crosses a milestone.
Each one is another small endpoint. The dashboard grows; the architecture stays simple.
How to Build a RevenueCat Dashboard (Without Building a Frontend)
What you'll build
A live RevenueCat dashboard with five widgets:
MRR (KPI) — current monthly recurring revenue
Active subscribers (KPI) — current count
MRR by platform (bar chart) — iOS vs. Android revenue breakdown
Subscription events (line chart) — new vs. cancelled per day, last 30 days
Top revenue products (table) — your highest-grossing SKUs
You'll write a few endpoints in your existing backend that wrap RevenueCat's REST API, then point dashboardbase at them. Total time: ~20 minutes if you already have RevenueCat webhooks set up.
Why not just use RevenueCat's own dashboard?
RevenueCat's dashboard is genuinely good — far better than App Store Connect's own analytics. So why build another one?
Reasons mobile founders build a custom dashboard on top of RevenueCat:
Combine RevenueCat data with your own product data — e.g. "MRR vs. weekly active users vs. crash-free sessions"
Mobile-first viewing — RevenueCat's dashboard is web-only; you want this on your phone with one tap
Share with co-founders or investors — without giving them RevenueCat seat access
Custom MRR definitions — exclude trials, exclude specific SKUs, weight by lifecycle stage
Combine iOS, Android, web subscriptions in one view with custom grouping
If none of those apply, just use RevenueCat's dashboard. If two or more apply, keep reading.
Prerequisites
A RevenueCat account with active subscriptions
A RevenueCat REST API v2 secret key (Project Settings → API keys)
A backend in any language (examples in Node.js)
A dashboardbase account
Step 1: The MRR endpoint
The KPI widget contract:
{ "title": "MRR", "actions": [ { "title": "View Details", "type": "link", "url": "https://example.com/mrr-details" } ], "data": { "header": { "title": "$8,420", "subtitle": "vs last month", "badge": { "text": "+8.2%", "icon": "ArrowUp", "color": "Success" } } } }
The endpoint:
// GET /dashboards/revenuecat/mrr app.get('/dashboards/revenuecat/mrr', async (req, res) => { const response = await fetch( `https://api.revenuecat.com/v2/projects/${PROJECT_ID}/metrics/overview`, { headers: { Authorization: `Bearer ${process.env.REVENUECAT_SECRET}` } } ); const data = await response.json(); const currentMrr = data.metrics.mrr.current; const previousMrr = data.metrics.mrr.previous; // 30 days ago const trend = previousMrr ? ((currentMrr - previousMrr) / previousMrr * 100).toFixed(1) : 0; const up = trend >= 0; res.json({ title: 'MRR', actions: [ { title: 'View Details', type: 'link', url: 'https://example.com/mrr-details' }, ], data: { header: { title: `$${Math.round(currentMrr).toLocaleString('en-US')}`, subtitle: 'vs last month', badge: { text: `${up ? '+' : ''}${trend}%`, icon: up ? 'ArrowUp' : 'ArrowDown', color: up ? 'Success' : 'Danger', }, }, }, }); });
Note: RevenueCat's REST API v2 returns MRR aggregated and pre-computed. You don't need to do the math yourself. If you want a custom MRR definition (excluding trials, etc.), compute it from raw subscriber data instead.
You don't have to hand-write the remaining four. Two things make this faster:
Generating the endpoint. The free dashboardbase Skill teaches Claude Code, Cursor or any skills-capable agent the full JSON contract, so "wrap RevenueCat's MRR breakdown as a bar chart endpoint" comes back in the right shape rather than an invented one. No Skill install? The widget editor generates a copy-pasteable prompt — per widget, or one covering the whole dashboard — for whatever AI tool you use. It's a draft you review and run yourself.
Checking it. Paste the response into the endpoint validator and it says whether that widget type can render your JSON, with a live preview. It's public — no account required — so it's also the fastest way to sanity-check a shape before you deploy.
Step 2: Active subscribers KPI
app.get('/dashboards/revenuecat/active-subscribers', async (req, res) => { const response = await fetch( `https://api.revenuecat.com/v2/projects/${PROJECT_ID}/metrics/overview`, { headers: { Authorization: `Bearer ${process.env.REVENUECAT_SECRET}` } } ); const data = await response.json(); const active = data.metrics.active_subscriptions.current; const delta = data.metrics.active_subscriptions.delta || 0; const up = delta >= 0; res.json({ title: 'Active subscribers', data: { header: { title: active.toLocaleString('en-US'), subtitle: 'vs last month', badge: { text: `${up ? '+' : ''}${delta}`, icon: up ? 'ArrowUp' : 'ArrowDown', color: up ? 'Success' : 'Danger', }, }, }, }); });
Step 3: MRR by platform (bar chart)
The bar chart contract:
{ "title": "MRR by platform", "actions": [ { "title": "View Details", "type": "link", "url": "https://example.com/mrr-by-platform" } ], "data": { "header": { "title": "$8,420", "subtitle": "Last 30 days", "badge": { "text": "+$640", "icon": "ArrowUp", "color": "Success" } }, "labels": [ "iOS", "Android", "Web" ], "datasets": [ { "data": [ { "value": 5240 }, { "value": 2890 }, { "value": 290 } ], "label": "MRR" } ] } }
The platform names go in labels; the numbers go in datasets[].data as objects with a value. Add "indexAxis": "y" inside data if you'd rather render the bars horizontally.
app.get('/dashboards/revenuecat/mrr-by-platform', async (req, res) => { const response = await fetch( `https://api.revenuecat.com/v2/projects/${PROJECT_ID}/metrics/mrr/breakdown?dimension=store`, { headers: { Authorization: `Bearer ${process.env.REVENUECAT_SECRET}` } } ); const data = await response.json(); const platform = store => store === 'app_store' ? 'iOS' : store === 'play_store' ? 'Android' : store; const total = data.breakdown.reduce((sum, row) => sum + row.mrr, 0); res.json({ title: 'MRR by platform', actions: [ { title: 'View Details', type: 'link', url: 'https://example.com/mrr-by-platform' }, ], data: { header: { title: `$${Math.round(total).toLocaleString('en-US')}`, subtitle: 'Last 30 days', }, labels: data.breakdown.map(row => platform(row.store)), datasets: [{ data: data.breakdown.map(row => ({ value: Math.round(row.mrr) })), label: 'MRR', }], }, }); });
This is the kind of view RevenueCat's own dashboard makes you click through several screens for. Worth having as a one-glance widget.
Step 4: Subscription events over time
The line chart needs new vs. cancelled subscriptions per day. The cleanest source for this is your own database, populated by RevenueCat webhooks. If you're not consuming RevenueCat webhooks yet, set that up first — it's the foundation for almost any custom analytics work on top of RevenueCat.
app.get('/dashboards/revenuecat/subscription-events', async (req, res) => { const days = parseInt(req.query.days) || 30; const since = subDays(new Date(), days); const events = await db.revenuecatEvents.findMany({ where: { type: { in: ['INITIAL_PURCHASE', 'CANCELLATION'] }, createdAt: { gte: since }, }, }); // Bucket by day const buckets = {}; for (const event of events) { const day = event.createdAt.toISOString().slice(0, 10); if (!buckets[day]) buckets[day] = { new: 0, cancelled: 0 }; if (event.type === 'INITIAL_PURCHASE') buckets[day].new++; else buckets[day].cancelled++; } const days_sorted = Object.keys(buckets).sort(); const totalNew = days_sorted.reduce((sum, d) => sum + buckets[d].new, 0); res.json({ title: 'Subscription events', data: { header: { title: totalNew.toLocaleString('en-US'), subtitle: `New subscriptions, last ${days} days`, }, labels: days_sorted, datasets: [ { data: days_sorted.map(d => ({ value: buckets[d].new })), label: 'New', }, { data: days_sorted.map(d => ({ value: buckets[d].cancelled })), label: 'Cancelled', }, ], }, }); });
This endpoint accepts a days query param so it works with dashboardbase's date range selector.
Step 5: Top revenue products (table)
app.get('/dashboards/revenuecat/top-products', async (req, res) => { const response = await fetch( `https://api.revenuecat.com/v2/projects/${PROJECT_ID}/metrics/revenue/breakdown?dimension=product`, { headers: { Authorization: `Bearer ${process.env.REVENUECAT_SECRET}` } } ); const data = await response.json(); const rows = data.breakdown .sort((a, b) => b.revenue - a.revenue) .slice(0, 10) .map(row => { const ios = row.store === 'app_store'; return [ { text: row.product_id }, { text: ios ? 'iOS' : 'Android', badge: { text: ios ? 'iOS' : 'Android', color: ios ? 'Dark' : 'Success' }, }, { text: `$${Math.round(row.revenue).toLocaleString('en-US')}` }, { text: row.active_subscribers.toString() }, ]; }); res.json({ title: 'Top revenue products', actions: [ { title: 'View All', type: 'link', url: 'https://example.com/products' }, ], data: { headers: [ { text: 'Product', width: 40 }, { text: 'Platform', width: 20 }, { text: 'Revenue (30d)', width: 25 }, { text: 'Active', width: 15 }, ], rows, }, }); });
Step 6: Wire it up in dashboardbase
Sign in, create a new dashboard, name it "RevenueCat."
Click Add datasource and paste the URL of your first endpoint. Your workspace's endpoint secret is already attached — it shows as a read-only row above your own headers. Your RevenueCat key never goes in here; it stays in your backend.
Test the datasource — JSON response shows up.
Drop a KPI widget on the grid, point it at the MRR datasource.
Repeat for the other four widgets.
Set refresh interval to 5 minutes. Publish.
Open it on your phone via the dashboardbase iOS or Android app.
That's the dashboard you'd otherwise have built a separate React Native app to view.
The mobile founder angle
If you're shipping a mobile app, you check your numbers from your phone. That's not a nice-to-have — it's how you actually run the business between meetings, on the train, in line at coffee.
The combination here genuinely matters:
RevenueCat owns the source-of-truth subscription data
Your backend wraps it into the exact shape you want
dashboardbase renders it natively on your phone with push notifications
You can get something close to this with RevenueCat's own dashboard on mobile web, but it'll never feel as good as a native widget you can deep-link into from a notification.
Securing the endpoints
These endpoints expose subscription revenue. Two secrets are in play and it's worth keeping them straight:
Your RevenueCat key authenticates you to RevenueCat. It lives in your backend and never leaves it.
Your dashboardbase endpoint secret authenticates dashboardbase to your endpoint.
That second one is the one you configure here, and it needs almost no work. Every workspace has a single endpoint secret, and dashboardbase sends it as an x-dashboardbase-secret header on every request. You compare one string:
const SECRET = process.env.DASHBOARDBASE_ENDPOINT_SECRET // Fail at boot rather than serving subscriber data unprotected. if (!SECRET) throw new Error('DASHBOARDBASE_ENDPOINT_SECRET is not set') app.use('/dashboard', (req, res, next) => { if (req.get('x-dashboardbase-secret') !== SECRET) { return res.status(401).json({ error: 'unauthorized' }) } next() })
Find the value under Settings → Secrets → Endpoint Secret (or /workspace), masked with a reveal toggle and the environment variable name beside it. Rotating it keeps the old value valid for 24 hours so nothing breaks mid-deploy.
Prefer something you already have? Custom headers and Basic Auth are both supported and stack on top of the secret rather than replacing it.
And the validator checks reality, not intent. Testing a datasource fires one extra request with every credential stripped, then reports what came back — a green "Protected — returns 401 without the secret", or a yellow warning naming the header to look at. It won't stop you saving; it just won't let an open endpoint look closed.
Otherwise: no tokens in the URL, HTTPS only.
Common gotchas
Trial users. RevenueCat counts trials as "active subscribers" by default. Decide whether you want them in your dashboard's MRR.
Refunds and chargebacks. RevenueCat's MRR endpoint already accounts for these — but if you compute MRR yourself, don't forget.
Currency conversion. Multi-currency apps need to pick a reporting currency. Pick one in your endpoint and stay consistent.
iOS vs. Android revenue isn't apples-to-apples. Apple takes 30% (or 15% for small developers), Google's similar. Decide whether your dashboard shows gross or net.
API rate limits. RevenueCat's REST API v2 has rate limits. Cache responses for 60–300 seconds in your endpoint; dashboardbase polls at your refresh interval.
Next steps
Add a churn rate gauge widget.
Add a "trial-to-paid conversion" KPI.
Add a cohort retention table (a heavier endpoint, but doable).
Add a webhook trigger so your dashboard pings you when MRR crosses a milestone.
Each one is another small endpoint. The dashboard grows; the architecture stays simple.