# 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-funnel
```

Frontend 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

```python
@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

1. **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.
2. **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.
3. **Caching is easier.** Add `Cache-Control: max-age=60` on this
   endpoint → 60 seconds of CDN cache for the entire dashboard. Per-
   endpoint caching requires a TTL per surface area + invalidation
   logic per metric.
4. **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, one `datetime.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_PREFIXES` extension 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_at` field**. 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.
