# PURE — Performance & Scale Plan

**Created:** 2026-06-20 · **Status:** measured + staged (code ready below)
**Goal (Mike):** load faster, handle a lot of concurrent users, follow best practices.

## 0. Measured baseline (live, 2026-06-20)

| Page | Over the wire (Brotli) | Load | Note |
|---|---|---|---|
| `/` splash | 4 KB | ~220 ms | optimal |
| `/landing.html` | 2 KB | ~250 ms | optimal |
| `/v5.html` | 980 KB | ~490 ms | heavy (dev React + Babel-in-browser) |
| `/v4.html` | 1.97 MB | ~580 ms | very heavy |

**Key truth:** static pages are served by the Netlify CDN edge and scale to effectively unlimited
concurrent users. An MCP server does NOT serve pages and must not — the CDN is faster and already in place.
Two real levers remain: (1) v4/v5 page weight, (2) the Supabase data layer under polling load.

---

## 1. v5/v4 slim — PROVEN (page-speed lever)

**Proof-of-concept run (esbuild on the real `nav-control-center` source, 58 modules):**
- App code minified: **726 KB** → minified+gzipped: **202 KB**
- + production React+ReactDOM (min+gz): ~45 KB
- **Slimmed v5 ≈ 247 KB over the wire vs 980 KB today — ~75% smaller**, and **zero runtime Babel compile**
  (today the browser downloads `@babel/standalone` and compiles JSX on every first load).

**Plan:**
1. Precompile JSX → JS once with esbuild (run off-Netlify; commit the built output — keeps "no build on Netlify").
2. Ship **production** React/ReactDOM, not the dev builds.
3. **Route-based code splitting**: load the shell + the active module only; lazy-load the other ~40 modules
   on demand → initial load drops to ~50–80 KB.
4. After this, tighten CSP (drop `unsafe-eval` — no more runtime JSX). Folds in `t-csp-tighten`.

**Prereqs (unchanged from PERF-SLIM-V4V5-SCOPE):** pick the canonical source tree
(`project/nav-control-center@main` vs `merge-v3@clever-meitner`), and a render-verify path (Netlify branch
preview). The PoC above proves the win; the remaining work is wiring it up + verifying it renders.

---

## 2. Data-layer caching — the real concurrency fix

**The ceiling:** 41 pages read Supabase directly from the browser; ~20 poll every 20–30 s. 1,000 people on
the Client Portal = ~33 queries/s forever from one page's poll; the Command Center fires 3 queries/tab every
25 s. Static scales; Postgres does not. Collapse N pollers into 1 DB read per interval with a cached endpoint.

**STATUS (2026-06-20): BUILT + LIVE + VERIFIED.** Shared Postgres cache is in production:
  - `public.pure_cache_kv(key text pk, body jsonb, updated_at)` + `public.refresh_pure_cache()`.
  - **pg_cron job `pure_cache_refresh` runs every 20s** (reusing Design's pg_cron) → upserts hot query
    results (`integrations`, `board`, `releases`) into the table. Sub-minute schedule confirmed working.
  - `pure-cache` Edge Function (v4, verify_jwt off, CORS) reads ONE indexed row by key and returns it
    (`X-Cache: KV`, `X-Cache-Age`), falling back to a live query only if the row is missing.
  - Verified: 5/5 consecutive requests served `KV` at ~9–10s freshness. Under load, N pollers = one indexed
    row read each + ONE refresh query per 20s, regardless of user count. (The earlier in-memory `Map`
    approach was a no-op on serverless and was replaced by this.)

**Next: page migration.** Point the lane-b hot pages' READS at `pure-cache?q=<key>`. Each page's query must
first be added to `refresh_pure_cache()` as a new key. Best for read-only view pages; write-heavy admin
pages (e.g. Integrations Hub toggles) should keep direct reads or accept ≤20s staleness after a write.

```ts
// supabase/functions/pure-cache/index.ts  — read-only, cached, CORS
const SB = "https://mbnakfrkfwxinhxlisyq.supabase.co/rest/v1/";
const ANON = Deno.env.get("SUPABASE_ANON_KEY")!;        // RLS still applies
const TTL = 20;                                          // seconds
const WHITELIST: Record<string,string> = {
  integrations: "pure_integrations?select=*&order=kind,name",
  board:        "pure_board?select=*&order=created_at.desc&limit=200",
  status:       "pure_releases?select=*&order=ts.desc&limit=10",
};
const cache = new Map<string,{t:number; body:string}>();
const CORS = { "Access-Control-Allow-Origin":"*", "Access-Control-Allow-Headers":"*" };
Deno.serve(async (req) => {
  if (req.method === "OPTIONS") return new Response("ok", { headers: CORS });
  const key = new URL(req.url).searchParams.get("q") || "";
  const path = WHITELIST[key];
  if (!path) return new Response(JSON.stringify({error:"unknown query"}), {status:400, headers:CORS});
  const hit = cache.get(key);
  if (hit && Date.now()-hit.t < TTL*1000)
    return new Response(hit.body, {headers:{...CORS,"Content-Type":"application/json","X-Cache":"HIT","Cache-Control":`public, max-age=${TTL}`}});
  const r = await fetch(SB+path, { headers:{ apikey:ANON, Authorization:`Bearer ${ANON}` }});
  const body = await r.text();
  cache.set(key, { t:Date.now(), body });
  return new Response(body, {headers:{...CORS,"Content-Type":"application/json","X-Cache":"MISS","Cache-Control":`public, max-age=${TTL}`}});
});
```
Deploy with `verify_jwt` OFF (public reads). Page migration is a one-liner: swap
`fetch(SB+"pure_integrations?...")` → `fetch(CACHE+"?q=integrations")`. Do the highest-traffic pages first
(Client Portal, Command Center, Integrations Hub, Notifications).

**Bigger win later:** replace polling with **Supabase Realtime** (websocket push) on the hot pages.

---

## 3. Polling backoff — quick idle-load cut

Pages poll even when the tab is hidden. Pause when hidden; resume on focus. Per-page pattern:
```js
let timer = setInterval(load, 30000);
document.addEventListener("visibilitychange", () => {
  clearInterval(timer);
  if (!document.hidden) { load(); timer = setInterval(load, 30000); }
});
```
~20 pages to update (`lane-b/*` + `lane-mail.html`). Alternative: one shared visibility-aware
`setInterval` guard in a script every lane-b page already loads (`lane-nav.js`) — one reviewed change,
applies everywhere. (Global interval patch — review before shipping.)

---

## 4. Already shipped (2026-06-20)
- Brotli compression confirmed on; cache headers + `stale-while-revalidate` on assets/JS.
- Entry pages (splash/landing) already 2–4 KB.

## 5. Tickets
| id | title | lever | risk | needs |
|---|---|---|---|---|
| `t-v5-slim` | Precompile+split v5 (PoC: 980→~247 KB, no Babel) | speed | med (live app) | source decision + preview verify |
| `t-v4-slim` | Same for v4 (2 MB) | speed | med | after v5 |
| `t-cache-fn` | Deploy `pure-cache` Edge Function | scale | low (additive) | deploy via Composio |
| `t-cache-migrate` | Point hot pages at `pure-cache` | scale | med (page edits) | after t-cache-fn |
| `t-poll-backoff` | Visibility-aware polling on ~20 pages | scale | low–med | — |
| `t-realtime` | Replace hot-page polling with Supabase Realtime | scale | med | after cache |
| `t-csp-tighten` | Drop `unsafe-eval` after v5 slim | security | low | after t-v5-slim |
