This guide covers the analytics infrastructure for AdCP, including SQL views and Metabase setup.
AdCP uses a two-layer analytics approach:
- SQL Views - Pre-computed aggregations in PostgreSQL for fast queries
- Metabase - Self-service BI tool for dashboards and visualizations
The following views are available in your database (created by migration 009_analytics_views.sql):
Monthly revenue aggregation with gross, refunds, and net revenue.
SELECT * FROM revenue_by_month ORDER BY month DESC LIMIT 12;Columns:
month- First day of month (timestamp)gross_revenue- Total positive revenue (USD)refunds- Total refunds as negative number (USD)net_revenue- Gross minus refunds (USD)paying_customers- Unique customers who paidsubscription_payments- Count of subscription paymentsrefund_count- Number of refund events
Daily revenue metrics for trend analysis.
SELECT * FROM daily_revenue WHERE date >= CURRENT_DATE - INTERVAL '30 days';Columns:
date- Calendar dategross_revenue,refunds,net_revenue- Same as monthlytransaction_count- Total payment eventsunique_customers- Daily unique payers
Revenue breakdown by product/SKU.
SELECT * FROM product_revenue ORDER BY total_revenue DESC;Columns:
product_name,product_idcustomer_count- Unique customerstotal_revenue- All-time revenue (USD)avg_revenue_per_customer- Average per customer (USD)first_sale,last_sale- Date range
Current health status of all customers with LTV and risk indicators.
SELECT * FROM customer_health
WHERE subscription_status = 'active'
ORDER BY lifetime_value DESC;Columns:
workos_organization_id,company_name,stripe_customer_idcustomer_since- Account creation datemrr- Monthly recurring revenue (USD)subscription_interval- 'month' or 'year'subscription_current_period_end- Next renewal datesubscription_canceled_at- Cancellation timestamp (if canceled)subscription_status- 'active', 'canceled', 'expired', 'none'renewal_at_risk- TRUE if renewing in next 7 daystotal_payments- Count of all payment eventslifetime_value- Total revenue from customer (USD)refund_count- Number of refunds issuedlast_payment_date- Most recent payment timestamp
Summary metrics for subscription business (MRR, churn, etc.).
SELECT * FROM subscription_metrics;Columns (all single-row summary):
active_subscriptions,canceled_subscriptions,expired_subscriptions,no_subscriptiontotal_mrr- Sum of all active MRR (USD)avg_mrr- Average MRR per active sub (USD)total_ltv- Sum of all customer LTV (USD)avg_ltv_active- Average LTV for active customers (USD)renewals_at_risk_7d- Count of renewals in next 7 days
Customer retention by signup cohort.
SELECT * FROM cohort_analysis ORDER BY cohort_month DESC;Columns:
cohort_month- Month customers signed upcohort_size- Total customers in cohortstill_active- Currently active subscriberschurned- Customers who canceledretention_rate- Percentage still active
Payment processing success rate over time.
SELECT * FROM payment_success_rate
WHERE date >= CURRENT_DATE - INTERVAL '30 days'
ORDER BY date DESC;Columns:
date- Calendar datesuccessful_payments,failed_payments,total_attemptssuccess_rate- Percentage of successful attempts
docker-compose -f docker-compose.metabase.yml up -dMetabase will be available at: http://localhost:3001
Run the setup script to automatically configure Metabase with signed embedding:
./scripts/setup-metabase.shThis script will:
- Create an admin user (
admin@adcp.local) - Enable embedded dashboard support
- Generate a secret key for signed embedding
- Output environment variables to add to
.env.local
Add the environment variables from the script output to your .env.local file:
METABASE_SITE_URL=http://localhost:3001
METABASE_SECRET_KEY=<generated-secret-key>Then restart your dev server to load the new environment variables.
Accessing Metabase:
- Admin Panel: Navigate to
/admin/analyticsto view embedded dashboards (after creating one - see step 4) - Direct Access: http://localhost:3001 (login with admin credentials from setup script)
If you prefer manual setup:
- Open http://localhost:3001
- Create admin account (first user)
- Go to Admin → Settings → Embedding
- Enable "Embedding in other applications"
- Generate an embedding secret key
- Add the secret key to
.env.localasMETABASE_SECRET_KEY - Click "Add a database"
Database type: PostgreSQL
Display name: AdCP Production
Connection settings:
- Host:
host.docker.internal(Mac/Windows) or172.17.0.1(Linux) - Port:
53198(or your PostgreSQL port from.env.local) - Database name:
adcp - Username:
adcp(from.env.local) - Password:
localdev(from.env.local)
Advanced options:
- SSL: No (for local development)
Click "Save" to test connection.
After creating dashboards, you need to enable embedding and configure the dashboard ID for the admin panel.
Create a new dashboard called "Revenue Overview" with these questions:
-
Total Revenue (This Month)
- SQL:
SELECT SUM(net_revenue) FROM revenue_by_month WHERE month >= DATE_TRUNC('month', CURRENT_DATE) - Visualization: Number
- Format as currency
- SQL:
-
MRR Trend (Last 12 Months)
- Table:
revenue_by_month - X-axis:
month - Y-axis:
net_revenue - Visualization: Line chart
- Limit: 12 rows
- Table:
-
Active Subscriptions
- SQL:
SELECT active_subscriptions FROM subscription_metrics - Visualization: Number
- SQL:
-
Revenue by Product
- Table:
product_revenue - Visualization: Bar chart or Table
- Sort by:
total_revenue DESC
- Table:
Once you've created your dashboard:
- Click the dashboard share icon (top right) → "Embedding"
- Click "Enable" to turn on embedding for this dashboard
- Note the dashboard ID from the URL:
http://localhost:3001/dashboard/1→ dashboard ID is1 - Add the dashboard ID to your
.env.local:
METABASE_DASHBOARD_ID=1- Restart your dev server to load the new environment variable
- Navigate to
/admin/analytics- your dashboard will now load automatically!
Note: The open-source version of Metabase supports signed embedding for dashboards. This provides secure, seamless integration without requiring Metabase Enterprise features.
-
Customers by Status
- Table:
customer_health - Group by:
subscription_status - Aggregate: Count
- Visualization: Pie chart
- Table:
-
Top Customers by LTV
- Table:
customer_health - Columns:
company_name,lifetime_value,subscription_status - Sort by:
lifetime_value DESC - Limit: 10
- Visualization: Table
- Table:
-
Renewals at Risk (Next 7 Days)
- SQL:
SELECT COUNT(*) FROM customer_health WHERE renewal_at_risk = TRUE - Visualization: Number (with alert styling)
- SQL:
-
Cohort Retention
- Table:
cohort_analysis - Columns:
cohort_month,cohort_size,retention_rate - Visualization: Table or Heatmap
- Limit: 12 rows
- Table:
-
Payment Success Rate (30 Days)
- Table:
payment_success_rate - Filter:
date >= CURRENT_DATE - INTERVAL '30 days' - X-axis:
date - Y-axis:
success_rate - Visualization: Line chart
- Table:
-
Daily Revenue Trend
- Table:
daily_revenue - Filter: Last 30 days
- X-axis:
date - Y-axes:
gross_revenue,refunds,net_revenue - Visualization: Multi-line chart
- Table:
-
Failed Payments (Last 7 Days)
- Table:
payment_success_rate - Filter:
date >= CURRENT_DATE - INTERVAL '7 days' - Sum:
failed_payments - Visualization: Number
- Table:
The analytics dashboard is embedded in your admin panel using Metabase's signed embedding feature!
To view analytics:
- Create a dashboard in Metabase (see step 4)
- Enable embedding and add the dashboard ID to
.env.local - Navigate to
/admin/analyticsin your browser - The embedded dashboard will load securely inside the admin panel
Benefits of Signed Embedding:
- ✅ Secure - tokens expire after 10 minutes and are regenerated automatically
- ✅ No Metabase login required - tokens authorize access directly
- ✅ Works with open-source Metabase (no Enterprise license needed)
- ✅ Seamless experience - dashboard feels like part of your admin panel
Implementation Details:
The integration works through:
/api/admin/metabase-tokenendpoint generates signed embedding URLsadmin-analytics.htmlpage loads the dashboard in an iframe- Metabase validates the signature and displays the dashboard
Multiple Dashboards:
To embed multiple dashboards, you can:
- Create additional dashboards in Metabase
- Enable embedding for each one
- Use different dashboard IDs in your application
- Either switch between them or create separate pages
SELECT
company_name,
mrr,
subscription_current_period_end,
last_payment_date,
refund_count
FROM customer_health
WHERE renewal_at_risk = TRUE
AND refund_count > 0
ORDER BY mrr DESC;WITH last_month AS (
SELECT cohort_month, retention_rate
FROM cohort_analysis
WHERE cohort_month = DATE_TRUNC('month', CURRENT_DATE - INTERVAL '1 month')
)
SELECT
cohort_month,
100 - retention_rate AS churn_rate
FROM last_month;WITH monthly AS (
SELECT
month,
net_revenue,
LAG(net_revenue) OVER (ORDER BY month) AS prev_month_revenue
FROM revenue_by_month
WHERE month >= DATE_TRUNC('month', CURRENT_DATE - INTERVAL '12 months')
)
SELECT
month,
net_revenue,
prev_month_revenue,
ROUND(((net_revenue - prev_month_revenue) / NULLIF(prev_month_revenue, 0) * 100), 2) AS growth_rate_pct
FROM monthly
WHERE prev_month_revenue IS NOT NULL
ORDER BY month DESC;SELECT
company_name,
mrr,
subscription_current_period_end,
EXTRACT(DAY FROM (subscription_current_period_end - NOW())) AS days_until_renewal
FROM customer_health
WHERE renewal_at_risk = TRUE
AND subscription_status = 'active'
ORDER BY subscription_current_period_end ASC;When you add new views or tables:
- Go to Admin → Databases → AdCP Production
- Click "Sync database schema now"
# Backup Metabase's own database (dashboards, settings)
docker-compose -f docker-compose.metabase.yml exec metabase sh -c \
'cp /metabase-data/metabase.db.mv.db /metabase-data/metabase-backup-$(date +%Y%m%d).db.mv.db'docker-compose -f docker-compose.metabase.yml downdocker-compose -f docker-compose.metabase.yml logs -f metabase- Use Views - The pre-computed views are much faster than querying raw tables
- Add Filters - Always filter by date ranges in Metabase questions
- Cache Results - Enable Metabase caching for dashboards (Admin → Settings → Caching)
- Scheduled Refresh - Set dashboards to refresh every 5-15 minutes, not real-time
- Alerts - Set up Metabase alerts for key metrics (failed payments, low MRR, etc.)
- Public Links - Share dashboards with stakeholders via public URLs
- API Access - Use Metabase API to fetch metrics programmatically
- Custom Reports - Build SQL queries for one-off reports using these views as a foundation
Can't connect to database:
- Check PostgreSQL is running:
lsof -i :53198 - Verify credentials in
.env.localmatch Metabase connection - On Linux, use
172.17.0.1instead ofhost.docker.internal
Views not showing up:
- Run:
docker-compose -f docker-compose.metabase.yml exec metabase sh -c 'curl -X POST http://localhost:3000/api/database/1/sync' - Or manually sync in Admin → Databases
Dashboard is slow:
- Add date filters (e.g., last 30 days)
- Enable caching in Metabase settings
- Consider adding indexes to base tables if needed