1. Where the time goes
Like phoning each of 70 stores to ask how many coats they sold, when head office keeps the total.
How these numbers were measured
Stage: a read-only leg meter around the real loadLeasingInputs for the largest company (70 buildings), 5 runs each. Prod: CloudWatch http timeline lines for /api/leasing/stats, staff with no company picked, cache miss, 12:25Z–13:25Z on 2026-09-28 (n=15). Counts and timings only.
- Per-building reads, not an N+1 on leases or tours.
- Prod cold p75 2,504 ms, 513 calls per request.
- More concurrency barely helps: 12 wide saves ~0.15 s on stage.
Proves: stage leg meter (598 calls, 1.1 s) and prod http timeline lines, 2026-09-28.
2. Two company keys already exist
Two of the filing cabinets already have a drawer per company; four do not.
- Prospects: prospect-list-index, company partition, already used when nothing is picked.
- Occupancies: GSI3
OCC#<orgId>, stamped on every occupancy today. - Both need only a reader switch and a parity check.
Proves: prospect-list-gsi.ts; occupancies.ts key comment and occupancyEntityGSI3PK.
3. One new index, stamped by the writers
Every new invoice gets the company's name stamped on it at the till, not afterwards.
- One sparse GSI for four row types; entity rides in the partition key.
- Company comes from the building row, never the session.
- Both writer trees (src and the Clara Lambda) stamp, pinned by a drift test.
- No building or no company: stamp nothing, never guess.
Proves: precedent work-order-org-gsi.ts + scripts/backfill-work-order-org-gsi.ts, already shipped this way.
Deeper → Appendix: writer list and key shape
4. Backfill and rollout
Stamp the old invoices once, count them twice, then switch the till.
- Backfill prints counts only;
--applyis Gera's call, run in the slot. - Writes are UpdateItem SET key, conditional, idempotent, throttled.
- Parity: per-building and per-company reads must return identical rows.
- Totals stay true totals; nothing is truncated.
Proves: the work-order-org-index rollout (add → backfill → parity → read), same repo.
Deeper → Appendix: dry-run output shape, estimated read counts
A1 · Appendix — full record
Measurements, writer list, key shape and the backfill contract, for whoever builds this.
Per-reader calls, 70-building leg (stage, read-only, 2026-09-28)
| Reader | Shape today | Calls |
|---|---|---|
| prospects | 1 query per building | 70 |
| tours | 1 query per building | 70 |
| units | 1 query per building | 70 |
| tenants (occupancies) | org lookup + 1 query per building | 140 |
| building row | 1 GetItem per building (#13165 batches it) | 70 |
| leases | 1 query per building | 70 |
| renewals | per-building renewal-building-index reads + stages | ~110 |
| whole loadLeasingInputs (memo shared) | 598 |
Option B, fan-out concurrency (stage, on-demand table; NOT prod capacity)
| Fan-out | Calls | p50 ms | p75 ms | max ms | Throttles / SDK retries |
|---|---|---|---|---|---|
| 8 (today) | 598 | 1,097 | 1,098 | 1,352 | 0 / 0 |
| 12 | 598 | 944 | 998 | 1,222 | 0 / 0 |
| 16 | 598 | 1,055 | 1,058 | 1,095 | 0 / 0 |
5 runs each, from a machine outside the AWS region. Stage is on-demand with little other traffic, so 0 throttles here says nothing about prod, which showed read throttles earlier on 2026-09-28. Renewals' own inner fan-out (16) was not varied.
Writers that stamp the new key
| Row | Base key today | Writers (both trees) |
|---|---|---|
| Unit | PROP#<id> / UNIT#<number> | saveUnits (src property.ts, clara property.ts) |
| Lease | PROP#<id> / LEASE#<unit>#<id> | saveLeases (src lease.ts + property.ts, clara lease.ts + property.ts) |
| Tour | PROP#<id> / TOUR#… | _unsafe_saveTour (src leasing.ts, clara tree) |
| Renewal row | renewal-building-index rows | the renewal writer that stamps renewal-building keys |
Key: ORGROWPK = ORGROW#<orgId>#<entity>, ORGROWSK = <propertyId>#<rowId>, projection ALL. One pure module derives it, like work-order-org-gsi.ts. The org comes from the building's own row, read in the write path; a missing building or org stamps nothing.
Backfill dry-run output (counts only)
backfill-org-row-index --entity=unit|lease|tour|renewal (dry-run unless --apply) entity scanned already would_stamp no_building no_org unit … … … … …
Scan by entityType index, page by page, throttled. --apply does UpdateItem SET ORGROWPK/SK with the condition attribute_not_exists(ORGROWPK), so it is idempotent. Run in the serial slot with a throttle check before and after. Apply is Gera's call.
Estimated read-count win (70-building leg)
| Reader | Today | Company read (est.) |
|---|---|---|
| prospects | 70 | 1–3 pages |
| occupancies | 140 | 1–3 pages |
| units / leases / tours | 210 | 3–8 pages |
| renewal rows | ~110 | 2–5 pages |
| building rows | 70 | 1 (batch, #13165) |
Estimates assume 1 MB query pages at today's row counts. To be measured on stage after a stage backfill, before any prod apply.