Story 1779: one-trip page bootstrap for the team and record pages
Why this exists. A workspace page that fires five or six parallel API calls on load is five or six chances to wake a cold Lambda. Each concurrent round-trip can force a new instance, each new instance pays a cold start, and the user gets the transient 503 class that Bug-1775 had to mop up reactively. The durable fix is to stop fanning out: let the browser ask once, and let the server do the fan-out internally over a warm pooled connection. Think of it as a waiter taking the whole table's order on one trip to the kitchen instead of one trip per dish.
/app. No new tables, no new business rules, no
duplicated query logic — each endpoint calls the SAME service functions the
per-resource routes already call. The per-resource endpoints stay exactly as
they are; this only adds an aggregate door.Satellite view: two new doors
GET /app/firm/team/bootstrap
-> { members, seatsAndBilling, firmRoles }
GET /app/matters/{matter_id}/record/bootstrap
-> { matter, participants, documents, savedViews, savedViewsAvailable }
The team page collapses three round-trips into one; the record page collapses four into one. Field names are intentional and frozen — pramaan-app (Story-1780) mirrors this wire shape via codegen.
Airplane view: compose, don't reimplement
PageBootstrapService.get_record_bootstrap(session, matter_id) | |-- MatterService.get_matter(...) <- anchor; 404 stops here |-- MatterIdentityService.list_matter_participants(...) |-- DocumentService.list_documents(..., limit=50) '-- DocumentSavedViewService.list_saved_views(...) <- best-effort, runs LAST
Every field is the verbatim response of its per-resource endpoint. The aggregate fans out to a fixed number of existing service queries (3 for team, 4 for record) — never one query per row — so it adds no N+1 (pramaan-docs ADR-130 DR-1). The underlying services already do their own set-based queries; this layer only sequences and assembles their typed responses.
Microscopic view: the three load-bearing details
1. Authz is identical by construction. Every call runs on
the firm-scoped AppDbDep session, so RLS firm-scope is the same
wall the per-resource endpoints stand behind. For the record page the matter
is the anchor: a caller in the wrong firm gets a 404 from
get_matter and never reaches any sub-resource query. A door a
caller cannot open per-resource cannot be opened via the bootstrap.
2. Best-effort sub-parts degrade, never fail. Saved views
run last, wrapped so any failure becomes an empty list plus
savedViewsAvailable: false rather than a 500 for the whole page.
Last-place ordering means a saved-view failure never discards the
already-collected anchor payloads.
3. Team carries three blocks, not five — on purpose. The
team page also shows invitations and
invitationReconciliation, but the app-api Lambda has no VPC egress
to api.clerk.com, PRAMAAN does not persist pending Clerk invitations
(organizationInvitation.accepted is an ignored webhook), and
reconciliation needs a live Clerk snapshot. Those two are browser→Clerk
calls that never touch this gateway, so they are outside the SSR Lambda
fan-out this work targets and stay client-side Clerk by design.
Observability (pramaan-docs ADR-124)
These are read-only composition handlers over already-instrumented service
queries. They are covered by the auto-instrumented HTTP route span plus the
auto DB spans, and there is no registry-listed ManualSpanName for
a page-bootstrap read. No manual span is added — instrumentation-exempt under
the rule that small composition over instrumented callees rides the caller
span, mirroring the existing MatterConfigBootstrapService
precedent.
Verification
make test-db uv run ruff check src/app_api/page_bootstrap_dtos.py src/app_api/page_bootstrap_service.py \ src/app_api/routes/firm.py src/app_api/router_matters.py tests/integration/test_app_page_bootstrap.py uv run pytest tests/integration/test_app_page_bootstrap.py -v --tb=short make openapi # regenerates openapi/app-api.json + permission-map.json
Tests cover the composed shape for both endpoints (full DTO round-trip), the cross-firm denial path (wrong firm → 404), and the zero-rows first run (new firm with no roles, matter with no documents → empty arrays, not an error).
One-sentence recall: the bootstrap endpoints let the browser ask once and the server fan out over existing firm-scoped service queries, trading a cold-start-prone client fan-out for one warm round-trip without changing a single per-resource contract.