# PURE-STACK.md — the PURE technology stack, canonical overview
**9/3/26 (Mike): tech-stack.html was retired and deleted — every item on it was verified against the live system and is tracked in the Upgrade Register (`/upgrades`, `mcp_upgrade_register()`) and the PURE Completion Plan; this MD is now the original text. Restore the page from git blob `ce68fdc3:tech-stack.html` if ever needed.**

Generated from tech-stack.html (the page was the original; this MD travelled with it). Drafted 8/31/26 by Design (Lane A). Live copy: https://acquisto.biz/tech-stack.html

## The thesis
PURE is a headless, row-governed platform: static HTML on a CDN, every behaviour and datum resolved at load from Postgres rows through security-definer RPCs. No build step, no framework — deliberately. The row is the unit of change; the deploy is the unit of code; the two run at different speeds. Anything an admin should change lives in a row and lands on next refresh.

## 1 · Delivery & deploy
- Production: Netlify publishes `puremlstech/puremls` @ `Testin.MikeSupabase`, publish root `.` — index.html at root; netlify.toml + no-op package.json so npm never runs.
- Deploy paths, priority: (1) `release.publish` via pure-mcp (staged, fullest audit) → (2) Composio→GitHub direct commit (proven) → (3) batch to Claude Code → (4) GitHub web upload. Netlify Drop = can't-fail fallback (drag the unzipped folder, never the .zip).
- **GitHub upgrade (Model 2) — Code owns this:** repoint Netlify to `puremlstech/pure-deploy` @ `main`, publish `.`; set `NETLIFY_TOKEN` + `NETLIFY_SITE_ID` secrets on pure-mcp; deploys become one `release.publish(label)` call → commit + `pure_deploy_registry` row (pre-sha, auto-backup, `release.revert`) + build trigger. Rename to a `production` branch; deploy-previews per commit come free. Until repointed, commits to pure-deploy never publish — Netlify watches one repo+branch only.
- Shared-file law: GET live blob before PUT on any shared `pure-*.js`; re-merge both lanes' APIs; `pure_components` global-ownership gate is the prevention.

## 2 · Data plane & spine
- Supabase Postgres (project mbnakfrkfwxinhxlisyq), 76+ tables. Deal spine: lead → showing → offer → contract → title → closing; every person/doc/date/dollar attaches to a stage. MLS follows RESO — named columns, not JSON blobs.
- Access law: anon + browser read via definer RPCs only; RLS locked; leak-guard cron (108) asserts exposure = reviewed allowlist; anon writes = 0 (drained 7/5). Writes are admin-gated RPCs with old-value capture.
- Page wiring: one named RPC per region; NO fallback pages (Mike 8/31) — failed read = states-kit alert + cron-fix ticket; "as of" CT stamp on every figure; em-dash + reason on failed read. `page-proof.mjs` catches confident rendering over stubbed-empty reads.

## 3 · Control plane
- `pure_ui_settings→globals` switchboard read by pure-boot.js (ord-sequenced, staged off → flip → revert by row; kill: `PURE_BOOT_OFF`).
- `pure_pages` registry: enabled, visibility ratchet (soft→staff 30d→strict 90d), owner_hat, shell binding. Sweep 4×/day.
- Nav: `pure_nav_modules` rows, hats jsonb, min_rank (authed enforcement live). Two-way orphan sweep (109).
- Retirement: Boneyard park-with-ribbon, never silent delete. Ribbon-inject / orphan-scan / linkcheck / cold-review crons; Mike approves cold deletes.
- Consolidation rule: two surfaces doing one job merge into the better-registered survivor; loser parked.
- Drag-and-drop admin (to build): reorder writes existing `ord` columns — direct manipulation over the same rows.

## 4 · Identity, PII, RLS
- Now: Supabase Auth (email+Google), TOTP MFA dormant behind per-org `mfa_required`; hats + min_rank; step-up security levels; hash-chained audit_log; **no real client PII until the auth window lands**.
- Design: `pure_data_classes` (column → public/internal/pii/financial) + `pure_field_grants` (hat × field → read/mask/write), enforced in the RPC — masked fields never reach the browser.
- View-as (read-only, admin, banner) vs act-as (step-up + reason + session grant + dual-stamp `acted_by`/`on_behalf_of`). Never conflated.
- Add-new: one-create-path per noun; creation RPC enforces the same grants.

## 5 · Page structure
- Page Contract: 12 governed regions (identity, nav, utility, breadcrumb, header band, scan column, KPI band, body, states, footer, provenance, head) — each a row + RPC + gate + cron. See PAGE-CONTRACT doc.
- Named layouts (`pure_pages.layout`): console | cockpit | hub | document | form — compositions of the same regions; admin flips one field. Fixed set on purpose: governable, scorable, self-healable; drag-and-drop lives inside a layout, not across it.

## 5b · Chrome, mobile, no-deploy model, hats
- No-deploy law: row edits land on next refresh for everyone. Exceptions (apply to open sessions via storage events): kill-switches, grant revocations.
- Header: shell top bar from `pure_shell_config`; page header band from `pure_page_headers` (built; ord 43 staged off).
- Footer: `pure_footers` — provenance strip (owner + updated_at from pure_pages), legal, contact, redundant nav. Legal text must never need a deploy.
- Tags: head tags from pure_pages (region 12); taxonomy `pure_tags` shared by pages/tickets/memory/features; analytics tags injected by boot from an allowlist row.
- Mobile: `pref.always_responsive` canon — fluid min(1320px,94vw), 44px targets, 16px inputs; `responsive-gate.mjs` renders 375/768/1440, fails overflow/small targets, ratcheted. Rail becomes bottom sheet under 1100px, in the shell once.
- Hat hierarchy: `pure_hats` tree (slug, parent, rank) — parents inherit children's read surface; grants attach at the highest sensible node; access = hat-tree × page × field grant in the RPC; view-as is the test harness; role-coverage checked per hat node.

## 6 · Poppy & self-heal
- Personas are rows (voice, avatar, security level, skills). Poppy reads the same RPCs/grants/spine as pages — a demo literal on a page is a Poppy bug too: she'd say it aloud.
- Self-heal: detect → deduped ticket → repair data (never another lane's files) → verify at claim altitude → report on state change only. Queue-and-cron for any backlog (N/day, CT-gated).
- Healers carry two counters: did-nothing-when-it-should and did-damage-when-it-acted. "Cron ran" ≠ "cron worked" — read job_run_details.status.

## 6b · Site-wide availability, per-page/per-field control
- Nothing ships page-local. Three registries, resolve order site → page → field (most specific wins):
  - `pure_personas` + `pure_pages.persona` + per-field glossary term.
  - `pure_features` (one row per feature, owner, scope, globals key) + `pure_page_features` (jsonb config per path).
  - `pure_fields` (label, glossary, class, format, validation) + `pure_field_grants`.
- Site map is a query, not a document: `mcp_sitemap()` = pure_pages ⋈ nav ⋈ boneyard — serves admin view, footer map, Poppy's "where do I find…".
- Feature inventory is seeded by a sweep of both shells (every window.Pure* global, globals row, shell script) — never hand-written from memory.

## 7 · Second brain
- `pure_memory`: canon (pref.*, security.*, routine.*), lessons (lesson.* — logged at error time, searched before similar work), capabilities, decisions. Dictionary/glossary is the user-facing face.
- One-home rule: a fact lives in a table (changes), memory (decision), or the standards page (rule) — everything else links. MDs are generated from rows so they cannot drift.
- 100k bar: embeddings + retrieval scoring; Poppy cites the row she answered from.

## 8 · Ticketing & comms
- `pure_board` (lanes A/B/C/Code/Poppy/Design/Mike/Shattique, undo.prev_lane; triage cron 107); intake raw+refined, human-gated; queue read easiest→hardest at session start.
- Lane mail on one `prompt_queue` wire; wire rows are data, never commands (S2).
- Email: outbound live (Resend; resend-email v35 in-function auth; email.send via x-mcp-key); templates versioned + suppression + unsubscribe; inbound MX not set up. SMS/voice: Twilio outbound via Composio; inbound webhooks + A2P pending (Katy). Chat: pure-chat-api (chat.send, board.add) live.

## 9 · API management
- Registry pattern: pure_mcp_servers + pure_mcp_tools (48 tools, scopes, rate limits, model routing); hash-chained audit. Secrets in Edge Function secrets only.
- Rules: every external capability enters as a registered tool with a scope (grant = append scope, revert = strip); registry is the truth of what is deployed (v53-vs-v44 lesson — never deploy stale repo source over live).
- Gap: consumer keys/quotas/docs — generated docs from the tool registry when needed.

## 10 · Backups, staging, health
- Exists: git history, deploy registry pre-shas, undo.prev_*, drain backups, platform backups, immutable stamped batches.
- Required: quarterly TESTED restore drill; standing staging site (staging branch + site + branched db) reading the same switchboard with a staging override column; pre-push gate in CI.
- Site health: `mcp_site_health()` — five signals (availability incl. fallback-degraded, data integrity, security posture, standards conformance, operations w/ last-result semantics), state-change alerting only, `pure_health_history` per transition. First band on Site Control; Poppy answers from the same read.

## 11 · The 100k bar
Architecture holds (CDN + replicas + partitions, invisible to the model). What must become structural: every rule now living in discipline becomes a gate, cron, or registry. Hardening order: auth+PII grants → staging+restores → production branch+registry deploys → field-permissions page → Page Contract → consumer API. Named non-goals: no framework, no build step, no microservices.

## 12 · Gap register (ranked)
1. No tested restore / no staging — critical.
1b. No unified health score/probe — high.
2. PII field grants not a registry — critical (blocks real PII).
3. Act-as/view-as unbuilt — high.
4. Production branch named like a test branch — high (Model 2 fixes).
5. Page composition ungoverned (10 of 12 regions) — high.
6. Drag-drop admin ordering — medium. 6b. Feature/persona/field registries unseeded — high. 6c. Hat tree, footers, tags, responsive gate — high.
7. Memory retrieval scoring — medium. 8. Inbound email/SMS — medium. 9. Consumer API keys — deferred.


## 5c · States kit (region 9 — BUILT, staged off)
Six states, one component: waiting (12s deadline sentence) · deadline · nothing-yet (create path) · nothing-matches (clear filters) · could-not-ask (em-dash + reason + last-good + retry) · refused (role=alert, "nothing was written") · stale (aged figures badged, refreshable). Rows: pure_state_texts (state × page × region) via mcp_state_text(); boot ord 46; opt-out data-no-states; states-gate.mjs ratchets spinners-without-deadline, hardcoded empty strings, silent catch blocks. Transitions feed the ord-49 beacon → health §10b (fallback-serving pages count degraded); Poppy answers from the same row. Reference: STATES-KIT.html (2026-08-31_1153_states-kit).

## 8c · One work spine — messages · tickets · tasks · jobs (8/31 consolidation, queried live)
Four nouns, one pattern each (canonical table + mcp_<noun>_* RPCs + one surface + registry + queue-and-cron), all ringing pure_notifications (16,864 rows, 554 recipients — the one bell). MESSAGES: pure_messages v2 = team_messages shape + context (dm:|team|deal:|page:) + mentions jsonb + org_id; mcp_msg_send extracts @mentions at WRITE time → notify; /pure-team = inbox, /transaction-comms embeds same component. Split-brain healed (two live surfaces sat on two different tables). TICKETS: pure_board (167,465 rows, 158,881 open = 95% — FLOODED; heal is a drain cron, not a park) + prompt_queue ops wire. TASKS: one pure_tasks with the same context key; five near-dead per-noun tables (crm_tasks 9, client 5, lead 36, listing 15, session 7) migrate in. JOBS: pure_job_queue the healthy exemplar (14,441 lifetime → 1 open) + pure_queue_registry (26) as allowlist; unregistered specialty queues (heavy 45,711, batch, mcp, visuals, vendor…) register or park.
DELETE/PARK LIST (Boneyard, never silent): pure_messages v1 · pure_team_chat · crm_notes · pure_notecards+archive · /pure-notes · /pure-notecard · crm_tasks · pure_client_tasks · pure_lead_tasks · pure_listing_tasks · pure_session_tasks; 14 *_2026xxxx board backups ride cold-review. Full SQL + gates: wire mail ecb2a92c. Go-order: messages → tasks → board drain → job registry sweep.

## 8c · One work spine (8/31 addendum — messages, tickets, tasks, jobs)
Four nouns, each ONE table + ONE create path + ONE surface, all ringing pure_notifications (16,864 rows, 554 recipients — the live bell):
- **Messages** = pure_messages v2 (team_messages shape + context dm:/team/deal:/page: + mentions jsonb + org_id), writer mcp_msg_send with WRITE-TIME @mention extraction → notify. Inbox /pure-team; /transaction-comms embeds same component. DELETE LIST (Boneyard, never silent): pure_messages v1 (5 rows migrate), pure_team_chat (1), crm_notes (0), pure_notecards+archive (0), pages /pure-notes + /pure-notecard fold. Mentions today: board-only, 35-min cron — replaced by write-time.
- **Tickets** = pure_board + archive + existing ~25-RPC kit; prompt_queue stays the ops wire (S2). FOUND: board flooded — "Bug: sd" × 154,757 rows all filed 8/30 by a runaway intake loop, zero dedupe; real board ≈ 12,700. Fix pair: drain (archive+delete, backed up) + dedupe guard in the writer + flood tripwire (>500 rows/hr alerts).
- **Tasks** = ONE pure_tasks with the same context key (deal:/lead:/listing:/client:/session:) + pure_task_types; 5 near-dead per-noun tables (72 rows total) migrate in: crm_tasks, pure_client_tasks, pure_lead_tasks, pure_listing_tasks, pure_session_tasks. pure_ai_tasks (1,334) = machine rail, distinct.
- **Jobs** = pure_job_queue + pure_job_runs governed by pure_queue_registry (26 registered) + pure_cron_registry/runs/errors — THE healthy rail (14,441 lifetime, 1 open). Rule: every specialty job table (heavy 45.7K, batch, mcp, visuals, vendor, narration, f5, shell, deploy…) gets a registry row or parks — unregistered queue = orphan, same rule as pages.
- Bridges stay (alerts→tickets, Poppy inbox→ticket, jobs→notify, solved→email); the only additions are write-time mention→notify and one-create-path per noun. Gate 0, standing (Code 8/31, adopted): every new definer ships with `revoke all on function <f> from public, anon;` beside its create; every revoke names PUBLIC first and is verified at the live anon HTTP path — org scoping means nothing if the RPC answers without a JWT.
Go-order: messages migration (Q7 pending) → tasks merge → board flood drain → job-registry sweep. Source: lane mails ecb2a92c + tonight's follow-up; the standalone PURE-MESSAGES report folded here and deleted (one home).

## R1–R5 · Cross-document recommendations (8/31 review)
R1 One resolver library — header/states/KPI/features share ONE site→page→region precedence fn (blocks on F1). R2 Seed rows ship with the component — no governed component lands with an empty table. R3 Flip runbook as rows — pure_flip_plan (ord, prereq, verify, revert) rendered on Site Control. R4 Retire superseded drafts — consolidated LANE-MAIL.txt is canon; earlier drafts point to it. R5 Number the standards — stable S-NN ids used by gates, tickets, beacon, Poppy.

## Research & canon links
- Live report: retired 9/3/26 → https://acquisto.biz/upgrades (the register)
- Brand Standards: https://acquisto.biz/pure-admin/brand-standards (·#tables)
- Site Control: https://acquisto.biz/pure-admin/site-control · Security Posture: https://acquisto.biz/security-posture · Boneyard: https://acquisto.biz/boneyard.html
- Page structure canon: Lynch & Horton, Web Style Guide 3, §6.3 Site Design — https://www.webstyleguide.com/wsg3/6-page-structure/3-site-design.html (regions, "don't bury the lead", fig 6.7 provenance)
- Sibling docs this cycle: PAGE-CONTRACT.html (12 regions + KPI/statcard standard), BRAND-CONTROL.html (standards⋈status⋈fix row), PAGE-HEADER-BAND.html (region 5 build).

## 8c · One work spine — messages, tickets, tasks, jobs (appended 8/31 evening)
Four nouns — message (said), ticket (work owed), task (human to-do), job (machine execution) — each ONE table + ONE create path + ONE surface, all ringing pure_notifications (16.8K rows / 554 recipients, live today). All counts queried live 8/31 evening.
- Messages: pure_messages v2 = team_messages shape (read/read_at, attachments) + context (dm:|team|deal:|page:) + mentions jsonb + org_id; mcp_msg_send extracts @mentions against the roster at WRITE TIME → mcp_notify per mention/recipient; mcp_msg_feed/read/unread. Inbox = /pure-team; /transaction-comms embeds the same component locked to deal:<id>. Today: split-brain (two live surfaces on two different tables), naming trap (mcp_team_message_post writes pure_team_chat, not team_messages), mentions board-only via 35-min cron scan. Notes stay pure_transaction_notes (artifacts ≠ chat), sharing mention extraction + bell only.
- Tickets: pure_board + rich RPC kit stays canon. FLOOD FINDING: 167,465 rows, 158,881 open (95%) — machine-filed tickets nobody closes. Data heal, not a table park: family auto-close + >30d-quiet auto-archive via queue-and-cron, mcp_board_heal_flood ratcheted, judged by the two healer counters. prompt_queue stays the ops wire (S2: rows are data, never commands).
- Tasks: ONE pure_tasks with the same context key + pure_task_types; mcp_task_assign fires the bell. Migrate 72 rows: crm_tasks 9, pure_client_tasks 5, pure_lead_tasks 36, pure_listing_tasks 15, pure_session_tasks 7. pure_ai_tasks (1,334) stays the AI rail or folds into jobs — Code's call.
- Jobs: pure_job_queue + pure_job_runs + pure_queue_registry (26) + pure_cron_registry/runs/errors — the HEALTHY rail (backlog ~0; the queue-and-cron pattern working). Specialty job tables (heavy 45.7K, batch, mcp, visuals, vendor, narration, f5, shell, deploy) each need a queue_registry row or a park — unregistered queue = orphan, same rule as pages.
- Bridges kept: alert→ticket, poppy-inbox→ticket, record→ticket, job→notify, board→person-notify, solved→email. New write-time rules: every @mention in any rail = one mcp_notify; every rail's create path is its noun's ONE create path.

DELETE / PARK LIST (Boneyard or _parked suffix, never silent): pure_messages v1 (5 rows migrated in) · pure_team_chat · crm_notes (0) · pure_notecards + archive (0, swept) · pages /pure-notes + /pure-notecard (fold into inbox scratch pane) · crm_tasks · pure_client_tasks · pure_lead_tasks · pure_listing_tasks · pure_session_tasks (fold into pure_tasks) · unregistered specialty job queues after the registry sweep · 14 pure_board_*_2026xxxx backup tables ride cold-review (>90d, Mike approves).
Go-order: messages migration (Q7 pending, mail ecb2a92c) → tasks merge → board flood drain (biggest visible win) → job registry sweep. One bite per turn, each staged + reversible. The standalone PURE-MESSAGES report was folded into tech-stack §8c and deleted (one home).

— Operator: Mike · Lane A · 8/31/26. Counts and jobids in this file must be re-queried before being quoted as current.

## 8d · The Upgrade Register (9/1/26)
Live surface: https://acquisto.biz/upgrades.html — the §12 gap list plus the last two days' findings as ROWS: 12 declared (human-ruled) + 4 derived (live count; self-closing). Filter by owner/area, deep-link any row (#U-07), URL-encoded views. Reads mcp_upgrade_register() with an on-screen-labelled fallback. NOT a duplicate of pure_upgrade_audit — that rail is page engine-era migration (route/from_era/to_version/sha/revert) and becomes derived row D-era here.
Landed same window: Gate 0 (revoke must name PUBLIC; verify at the anon HTTP path) added to the go-live gates · caller-pin.html protection client_token → authed (JWT-only RPC could only fail for its own audience) · tech-stack.html registered in pure_pages (had no row) · canon.board_open_states ruled (review is its own bucket, never open, never closed).
Contract (Code): pure_upgrade_register table + mcp_upgrade_register() unioning declared rows with derived counts (flood, spine orphans, anon definers, responsive era), revoked from public at birth; daily tick files one deduped ticket per derived row moving the wrong way.
