One endpoint per screen
Applies to The Fabrica v0.1.2. Documentation source:
1ee1bc9cb661, including post-release documentation corrections.
GET /api/v1/admin/metrics/overview returns every number the admin
dashboard renders — revenue, mailing health, DAU/WAU/MAU, acquisition,
lead-magnet funnel — in a single response. Not five separate
endpoints. Single round trip.
What competitors ship
Often: one endpoint per “card” on the dashboard.
GET /admin/mrr
GET /admin/dau
GET /admin/wau
GET /admin/mau
GET /admin/signups
GET /admin/churn
GET /admin/mailing-funnelFrontend issues 7 fetches in useEffect. Network waterfall. Loading
spinners that pop in / out per card. Stuck cards if any one endpoint
slows down. SSR-friendly? No (7 parallel requests, all of which can
fail independently).
What we ship
@router.get("/metrics/overview", response_model=AdminMetricsOverviewResponse)
async def metrics_overview(...) -> AdminMetricsOverviewResponse:
revenue = await _compute_revenue_snapshot(db)
mailing = await _compute_mailing_health(db, thirty_days_ago=...)
activity = await _compute_activity_metrics(db, ...)
acquisition = await _compute_acquisition(db, ...)
lead_magnet = await _compute_lead_magnet_funnel(db, ...)
return AdminMetricsOverviewResponse(revenue=..., mailing=..., ...)One endpoint, five helpers. Each helper runs 2-4 small queries against indexed columns. P99 latency is dominated by the slowest sub-query, not their sum.
Why one, not five
- Frontend is simpler. One fetch, one loading state, one error boundary. The dashboard shows skeletons everywhere or all the real numbers — never the broken-middle state.
- Network is faster. TCP / TLS / Clerk JWT verification is a per-request cost. Five requests = 5× that overhead. One request amortises it across the full payload.
- Caching is easier. Add
Cache-Control: max-age=60on this endpoint → 60 seconds of CDN cache for the entire dashboard. Per- endpoint caching requires a TTL per surface area + invalidation logic per metric. - The metrics are correlated. If you’re showing them on the
same dashboard, you probably want them computed against the same
now(). Five endpoints means five different “now” calls — the user sees “30 signups last 30d / 31 active subs” because one card was a second behind the next. One endpoint, onedatetime.now().
When you’d split
Split when one of the metrics has fundamentally different update semantics:
- A real-time “currently-online users” counter that updates every 5s → don’t bundle it with the daily-revenue snapshot.
- A heavy aggregation (cohort retention over 12 months) that takes 10s to compute → don’t make the whole dashboard wait. Split that one out + show “Loading cohort…” while the rest is hydrated.
The factory’s metrics overview is bundled because all of them are “recent snapshot” queries — small, indexed, fast.
Implementation details worth pinning
USER_PROVENANCE_PREFIXESextension hook. Lead-magnet metrics are zero-safe when the prefix dict is empty (factory default). Buyers with a lead-magnet flow add{"lead_magnet": "lead-magnet:"}to project_config and the funnel populates.- Out-of-scope flags. Time-series MRR + CAC/LTV aren’t in this
endpoint. Documented as recipes (
admin-metrics-time-series.md) because they need additional storage (daily snapshots / ad-spend ingest) that isn’t factory boilerplate. generated_atfield. Lets the frontend show “Last updated: 2 minutes ago” instead of always rendering “live”. With a 60s cache, the freshness is bounded by the cache TTL.
Receipts
- Endpoint:
backend/src/api/routers/admin.py:metrics_overview - Helpers:
backend/src/api/routers/admin.py:_compute_* - Schema:
backend/src/api/schemas/admin.py:AdminMetricsOverviewResponse - Extension hook:
backend/src/config/project.py:user_provenance_prefixes
Provenance. Documentation source: 1ee1bc9cb6619c19f57766731e7884b37f515dc9 (main).
Extracted from docs/recipes/single-call-metrics-overview.md.
That commit is later than the release these pages describe: it carries documentation corrections made after thefabrica-v0.1.2 went out. Nothing that changes how the product behaves landed between the two, which is what makes naming that version honest.