ExcelRewind — Whole-System Architecture

Top-level view across all 12 modules · monorepo C:\Users\TM\Documents\claude proj\fable pqrs · built from verified per-module interface edges, cross-checked against package.json deploy scripts and repo source

Contents
  1. 1. Full-system architecture diagram
  2. 2. .rewind file lifecycle
  3. 3. Auth & licensing sequence
  4. 4. Billing flow
  5. 5. Deployment & topology
  6. 6. Trust boundaries & data classification
  7. 7. Consolidated key facts

1. Full-system architecture diagram

Every arrow below is a real interface edge reported by a module's own documentation (see the per-module pages linked from index.html). Edges are deduplicated where two modules describe the same wire call from both sides (e.g. addin-excel’s companion loopback call and companion-win’s response). Dashed arrows are compile-time ESM imports of packages/format/packages/branding (no network I/O); solid arrows are runtime network/IPC calls, labelled with protocol and payload.

flowchart TB
  subgraph SHARED["Shared library (in-process import only)"]
    FMT["packages/format
+ packages/branding"] end subgraph DESKTOP["Excel / Google Sheets Desktop"] ADDIN["apps/addin-excel
(Office.js task pane, React+TS)"] OFFICE["Office.js / Excel JS API
(in-process)"] CWIN["apps/companion-win
(Python tray, COM via pywin32)"] CMAC["apps/companion-mac
(Python, AppleScript/osascript)"] SHEETS["apps/addon-sheets
(Apps Script sidebar)"] GAS["Apps Script server
(Code.js, poll-only)"] end subgraph WEB["Browser clients (static, Cloudflare Pages)"] VIEWER["apps/viewer
(zero-backend replay SPA)"] ADMINAPP["apps/admin
(superadmin console)"] TEAM["apps/team
(org/account console)"] WEBSITE["apps/website
(marketing site)"] end subgraph SUPA["Supabase backend — no application server"] BDATA["backend-data
Postgres (RLS + SECURITY DEFINER RPCs) + Storage"] BSVC["backend-services
8 Deno Edge Functions"] end subgraph EXT["External services"] STRIPE["Stripe"] RAZOR["Razorpay (written, not live)"] RESEND["Resend"] GOOG["Google / Apple OAuth"] DNS["Domain DNS (TXT records)"] end subgraph DEPLOY["Deployment"] CFP["Cloudflare Pages
(5 projects, --branch master)"] INSTALL["Windows NSIS installer +
Mac .pkg installer"] end %% ---- compile-time imports ---- FMT -.->|"ESM import: writeRewind/readRewind, crypto, replayTo, branch.ts"| ADDIN FMT -.->|"ESM import: readRewind(limited:true), replayTo, spotlightCues"| VIEWER FMT -.->|"ESM import: keyring.ts key-row lifecycle"| TEAM FMT -.->|"ESM import, bundled into dist/sidebar.html"| SHEETS FMT -.->|"BUILD_TAG grep post-deploy"| INSTALL FMT -.->|"KeyRowFields schema-shape mirror only"| BDATA %% ---- addin-excel runtime edges ---- ADDIN -->|"Office.js: onChanged/onCalculated/onFormatChanged, getFileAsync"| OFFICE ADDIN -->|"HTTPS supabase-js: signInWithOtp/verifyOtp/OAuth"| BSVC ADDIN -->|"PostgREST SELECT/INSERT (JWT, RLS): profiles/org_keys/user_keys/usage_events"| BDATA ADDIN -->|"RPC: register_device, effective_features, exam_*, claim_handoff"| BSVC ADDIN -->|"POST /functions/v1/exam-storage (Bearer access token)"| BSVC ADDIN -->|"Storage upload bug-reports/<uid>/"| BDATA ADDIN -->|"window.open google-signin.html (forced system browser)"| VIEWER ADDIN -->|"HTTP(S) loopback 127.0.0.1:38743-47, Bearer token"| CWIN ADDIN -->|"HTTPS loopback 127.0.0.1:38743-47, self-signed cert"| CMAC ADDIN -->|"window.open Stripe Payment Link (system browser)"| STRIPE ADDIN -->|"window.open /companion/ installer landing page"| WEBSITE CWIN -->|"JSON: workbook list, coalesced cell-change events, file bytes"| ADDIN CMAC -->|"JSON: workbook list, spotlight spans, file bytes"| ADDIN CWIN -->|"COM: GetActiveObject('Excel.Application'), attach-only"| OFFICE CMAC -->|"AppleScript/osascript Apple Events (TCC-gated)"| OFFICE %% ---- addon-sheets ---- SHEETS -->|"google.script.run.rewindPoll() every ~2s"| GAS SHEETS -->|"HTTPS supabase-js: OTP auth, profiles/app_config read, usage_events insert"| BDATA SHEETS -->|"out-of-band link to /privacy.html"| VIEWER %% ---- viewer ---- VIEWER -->|"PostgREST insert viewer_events (anon key, keepalive)"| BDATA VIEWER -->|"google-signin.html: PKCE OAuth exchange + rpc stash_handoff"| BSVC VIEWER -->|"OAuth2/PKCE redirect (google-signin.html only)"| GOOG VIEWER -->|"outbound <a href> install.html?utm_*"| ADDIN %% ---- admin ---- ADMINAPP -->|"GET /functions/v1/metrics?days=N, Bearer OWNER_METRICS_TOKEN"| BSVC ADMINAPP -->|"POST /functions/v1/admin-ops {action}, Bearer + x-superadmin-jwt"| BSVC ADMINAPP -->|"supabase-js: OTP auth (superadmin email) + app_config read"| BDATA %% ---- team ---- TEAM -->|"RPC: org_*, cohort_*, exam_*, effective_features"| BDATA TEAM -->|"POST send-invite / verify-domain / exam-storage (forwarded JWT)"| BSVC TEAM -->|"window.open exam .rewind handoff"| VIEWER TEAM -->|"Stripe Payment Links / billing portal"| STRIPE TEAM -.->|"reads entitlements admin approved (effective_features)"| ADMINAPP %% ---- website ---- WEBSITE -->|"REST POST rpc/public_tiers, public_promo_config, get_my_org (anon key)"| BDATA %% ---- backend internals ---- BSVC -->|"service-role supabase-js: RPC + table read/write, bypasses RLS"| BDATA BDATA -->|"RPC JSON responses (effective_features, exam config, etc.)"| ADDIN %% ---- billing ---- STRIPE -->|"HTTPS webhook, stripe-signature verified (constructEventAsync)"| BSVC RAZOR -->|"HTTPS webhook, x-razorpay-signature HMAC-SHA256"| BSVC BSVC -->|"POST api.resend.com/emails (send-invite)"| RESEND BSVC -->|"Deno.resolveDns TXT lookup (verify-domain)"| DNS %% ---- deployment ---- CFP -->|"serves static dist/"| ADDIN CFP -->|"serves static dist/"| VIEWER CFP -->|"serves static dist/"| ADMINAPP CFP -->|"serves static dist/"| TEAM CFP -->|"serves static dist/"| WEBSITE INSTALL -->|"registers WEF TrustedCatalogs (Flags=3) via SMB UNC share"| OFFICE INSTALL -->|"hidden PowerShell POST /shutdown, then taskkill"| CWIN INSTALL -->|"postinstall: sideload manifest, trust TLS cert, LaunchAgent"| CMAC INSTALL -->|"HTTPS POST uninstall-feedback (anonymous, best-effort)"| BSVC

2. .rewind file lifecycle

Record (Excel or Sheets) → optional zero-knowledge encryption → save/share → replay (viewer or in-Excel temp sheet). Same writeRewind/readRewind contract on both platforms (packages/format/src/container.ts).

flowchart LR
  subgraph RECORD["Record"]
    EXCEL["Excel Desktop
worksheets.onChanged (Office.js)"] SHEETSREC["Google Sheets
poll-diff every ~2s"] end EXCEL -->|"recorder.ts normalizes to CellChangeEvent[]"| BUF["addin-excel: IndexedDB crash buffer
(encrypted at rest, buffer-crypto.ts)"] SHEETSREC -->|"diff.ts shadow-map diff"| DRAFT["addon-sheets: in-memory SessionDraft"] BUF --> STOP1["User clicks Stop & Save"] DRAFT --> STOP2["User clicks Save .rewind"] STOP1 --> WRITE["writeRewind(doc, encSpec?)
@excelrewind/format container.ts"] STOP2 --> WRITE WRITE --> ENCGATE{"account encryption
policy ON?"} ENCGATE -- "no (free/starter or opt-out)" --> PLAIN["plaintext: ZIP (store) of
deflate(MessagePack(entry))
-- deterrence only, not encryption"] ENCGATE -- yes --> ENC["AES-256-GCM per entry (AAD=entry name),
CEK wrapped to passphrase/org/RSA-3072 recipients,
escrow entry embedded for offline unlock"] PLAIN --> FILE[(".rewind file
(ZIP archive)")] ENC --> FILE FILE -->|"local save"| LOCAL["addin-excel: File System Access / download"] FILE -->|"share via email / Slack / link"| SHARE["Recipient (no ExcelRewind account needed)"] LOCAL -->|"replay-excel.ts: disposable temp sheets
inside the SAME workbook"| REPLAYX["In-Excel replay"] SHARE -->|"drop/pick in browser, zero install"| VIEWERO["apps/viewer: peekRewind()
-> cached secret / UnlockDialog
-> readRewind(bytes,{limited:true})"] VIEWERO --> REPLAYV["Viewer.tsx: replayTo() drives grid
+ audio/webcam + Provenance Spotlight at playhead t"]

3. Auth & licensing sequence

Email OTP is the common path; Google/Apple must be handed off to the system browser because WebView2/Apps-Script iframes block embedded OAuth. Every authenticated call downstream is gated by the effective_features() global→tier→org→user cascade.

sequenceDiagram
  participant User
  participant Pane as "addin-excel auth.ts"
  participant SysBrowser as "System Browser"
  participant ViewerAuth as "viewer google-signin.html"
  participant GoTrue as "Supabase Auth (GoTrue)"
  participant PG as "Postgres (backend-data)"

  alt Email OTP (all client apps)
    User->>Pane: enter email
    Pane->>GoTrue: signInWithOtp(email)
    GoTrue-->>User: OTP code via email
    User->>Pane: enter code
    Pane->>GoTrue: verifyOtp(email, token)
    GoTrue-->>Pane: session (JWT access + refresh)
  else Google / Apple (addin-excel only, system-browser handoff)
    User->>Pane: click "Continue with Google"
    Pane->>SysBrowser: window.open viewer.excelrewind.com/google-signin.html#nonce
    Note over Pane,SysBrowser: viewer origin is deliberately excluded
from manifest AppDomains -- forces the OS
to open the real browser, not the pane webview SysBrowser->>ViewerAuth: load page (viewer origin) ViewerAuth->>GoTrue: signInWithOAuth(google|apple), PKCE redirect GoTrue-->>ViewerAuth: exchangeCodeForSession -> JWT ViewerAuth->>PG: rpc stash_handoff(nonce, access_token, refresh_token) loop poll every ~2s, expires after 10 min Pane->>PG: rpc claim_handoff(nonce) end PG-->>Pane: access + refresh tokens Pane->>Pane: setSession(tokens) end Pane->>PG: rpc effective_features() PG->>PG: resolve_feature_state(global, tier, org, user)
coalesce(user, org, tier, global),
global 'disabled' is a hard-kill floor no narrower scope re-enables PG-->>Pane: feature state map Pane->>Pane: gate UI (encryption, spotlight, exam mode, cohort features, etc.)

4. Billing flow

Stripe is live; Razorpay is written but undeployed (no live webhook secret yet). Both webhooks are the only code path allowed to write a paid profiles.tier value — security fix C-1 closed a self-granted-tier hole in org_create().

sequenceDiagram
  participant User
  participant Client as "addin-excel / team (window.open)"
  participant Stripe
  participant Razorpay
  participant Webhook as "backend-services: stripe-webhook / razorpay-webhook"
  participant PG as "Postgres: profiles.tier"

  User->>Client: click Upgrade
  Client->>Stripe: window.open Stripe Payment Link (system browser)
  Note over Client,Stripe: client NEVER writes profiles.tier directly
  Stripe-->>User: checkout complete
  Stripe->>Webhook: POST checkout.session.completed (stripe-signature header)
  Webhook->>Webhook: stripe.webhooks.constructEventAsync() + SubtleCryptoProvider
  Webhook->>PG: match profiles.email via ESCAPED ILIKE (%/_/\ escaped)
UPDATE tier, stripe_customer_id PG-->>Webhook: ok Webhook-->>Stripe: 200 (idempotent -- safe to retry) par Razorpay path (written, NOT yet deployed/tested) User->>Client: click Upgrade (India entity) Client->>Razorpay: window.open Razorpay Payment Link Razorpay->>Webhook: POST payment_link.paid (x-razorpay-signature HMAC-SHA256) Webhook->>Webhook: verify via Web Crypto (crypto.subtle, no SDK) Webhook->>PG: same escaped-ILIKE match -> UPDATE tier, razorpay_customer_id end Note right of PG: profiles has NO authenticated UPDATE policy in RLS --
only service-role webhooks or SECURITY DEFINER RPCs can change tier

5. Deployment & topology

Five Cloudflare Pages projects (prod branch master, not main), two Supabase projects (prod/dev), and two native installers (Windows NSIS, Mac .pkg) that also register the Excel add-in and start the companion helper. Publishing the Windows installer to the public website is a manual step, not part of npm run deploy.

flowchart TB
  subgraph CF["Cloudflare Pages projects (wrangler --branch master)"]
    P1["excelrewind-addin
addin.excelrewind.com"] P2["excelrewind-viewer
viewer.excelrewind.com"] P3["excelrewind-website
excelrewind.com"] P4["excelrewind-admin
admin.excelrewind.com"] P5["excelrewind-team
team.excelrewind.com / account.excelrewind.com"] end subgraph SB["Supabase projects"] PROD["prod: tohvepsjfeecjycspocg.supabase.co"] DEV["dev: ragrgchidtdpgkyjkznu.supabase.co"] end DEPLOY1["npm run deploy
manifest -> build -> deploy:addin -> deploy:viewer -> deploy:website -> smoke"] DEPLOY1 --> P1 DEPLOY1 --> P2 DEPLOY1 --> P3 DEPLOY2["npm run deploy:admin (separate script, not in the chain)"] --> P4 DEPLOY3["npm run deploy:team (separate script, not in the chain)"] --> P5 MANIFEST["scripts/make-manifest.mts"] -->|"writes manifest.prod.xml + install.html"| P1 SMOKE["npm run smoke -> scripts/smoke.mts"] -->|"GET + BUILD_TAG grep"| P1 SMOKE -->|"GET + BUILD_TAG grep + sample-session.rewind ZIP-magic check"| P2 SMOKE -->|"GET + BUILD_TAG grep"| P3 P1 -.->|"CSP connect-src"| PROD P2 -.->|"CSP connect-src"| PROD P3 -.->|"anon-key RPC"| PROD P4 -.->|"CSP connect-src"| PROD P5 -.->|"CSP connect-src"| PROD subgraph WININST["Windows install path"] NSIS["apps/installer/ExcelRewind-Setup.nsi
(built by build-installers.ps1, Nuitka companion payload)"] USERW["User's Windows machine"] OFFICEW["Excel Desktop (Windows)"] COMPWIN["ExcelRewindCompanion.exe (tray)"] end MANUAL["MANUAL step: copy Setup.exe into apps/website/,
rerun manifest, wrangler-deploy website"] NSIS -.-> MANUAL -.-> P3 P3 -->|"download ExcelRewind-Setup.exe"| USERW USERW -->|"WEF TrustedCatalogs (SMB UNC share, Flags=3, HKCU)"| OFFICEW USERW -->|"non-elevated launch via explorer.exe"| COMPWIN COMPWIN -->|"loopback :38743-47"| OFFICEW subgraph MACINST["Mac install path"] MACPKG["apps/companion-mac build-installer.sh (.pkg, py2app,
currently unsigned/unnotarized)"] USERM["User's Mac"] OFFICEM["Excel Desktop (Mac)"] COMPMAC["Companion.app (rumps menu-bar)"] end MACPKG -->|"pkg postinstall (root)"| USERM USERM -->|"sideloads rewind-manifest.xml into Excel's wef folder"| OFFICEM USERM -->|"trusts self-signed loopback TLS cert (Keychain)"| USERM USERM -->|"LaunchAgent RunAtLoad=true"| COMPMAC COMPMAC -->|"loopback HTTPS :38743-47"| OFFICEM

6. Trust boundaries & data classification

For auditors: what crosses each boundary, what is zero-knowledge encrypted (server structurally cannot read it), and what is architecturally incapable of ever leaving the client.

Boundary-by-boundary data classification
BoundaryWhat crosses itClassificationEnforcement mechanism
Excel/Sheets workbook → recording engineFull cell values/formulas the user editsStays in-process (pane / Apps Script sandbox) onlyRecorder normalizes to CellChangeEvent before anything leaves the process; no raw workbook snapshot is sent anywhere by default
Client add-ins → backend-data / backend-servicesSession metadata: CellChangeEvent coordinates + before/after values needed for replay, auth tokens, telemetrySession events only — never full spreadsheet content (root CLAUDE.md constraint)telemetry.ts sanitizeProps()/flattenSettings() caps every tracked payload to ≤10 primitive keys and redacts free text; OAuth scopes stay minimal (Sheets add-on: spreadsheets.currentonly only)
Encryption key materialPassphrase, recovery code, unwrapped org/user master key (OMK/UMK), RSA private keyZero-knowledge — never reaches any server in usable formLives only in module-level in-memory variables (keyring-client.ts, React state in team), wiped on sign-out (clearUnlockCache()); DB (org_keys/user_keys) stores only ciphertext, KDF salts/iterations, and SHA-256 fingerprints — public keys are the only plaintext by design
.rewind file at rest / in transit (shared via email, Slack, link)Encrypted files: AES-256-GCM ciphertext + wrapped CEK + escrow. Unencrypted files: deflate-obfuscated MessagePack (readable with effort)Encrypted = confidential (recipient needs passphrase/recovery code/org key); plaintext = deterrence only, explicitly documented as NOT encryptioncontainer.ts per-entry AES-256-GCM (AAD=entry name) + GCM auth tag catches tampering; viewer applies decompression-bomb caps ({limited:true}) since a shared file is untrusted input
Crash-recovery buffer (IndexedDB, add-in)In-progress session draft + audio/video chunksEncrypted at rest when the account's encryption policy is ON (closed a prior plaintext-buffer gap)buffer-crypto.ts: same AES-256-GCM/wrapCek/unwrapCek path as a real file, AAD='rewind.recovery-buffer'; never persisted to localStorage
Companion loopback (addin-excel ↔ companion-win/mac)Workbook lists, cell-change events, raw reference-workbook bytesLocal machine only — never crosses the network beyond 127.0.0.1Bearer token + CORS allowlist (addin.excelrewind.com, localhost:3000 only); Mac additionally requires a locally-trusted self-signed TLS cert since WKWebView blocks plain-HTTP loopback
Billing webhooks (Stripe/Razorpay → backend-services)Signed payment events → a single tier-flip writeServer-to-server, signature-verified; the only path allowed to grant a paid tierconstructEventAsync (Stripe) / HMAC-SHA256 via crypto.subtle (Razorpay); security fix C-1 (20260724_org_no_forged_paid_tier.sql) closed a self-granted-tier hole; profiles has no authenticated UPDATE RLS policy at all
Admin console (apps/admin → admin-ops)Tier/feature state, user PII (email, status), coupons, pricing, destructive data-resetOwner-only, highest-privilege write surface in the systemDual factor: static OWNER_METRICS_TOKEN bearer + live Supabase OTP session for one hardcoded superadmin email, both re-validated server-side; destructive reset additionally requires an exact confirm-phrase re-checked in SQL
Anonymous telemetry (viewer_events, usage_events, uninstall_feedback)Event names, UTM params, boolean/number/short-enum props onlyMetadata-only by schema, not by conventionExplicit code comment on viewer_events: "the SCHEMA is the defense" — CHECK constraints bound the event vocabulary and cap props at 1KB; no RLS needed because there's nothing sensitive to protect
AI training pipeline (SDD-scoped, not yet built)Would be: free-tier, non-"Personal"-tagged sessions only, after PII-stripping anonymizationDeferred post-MVP (root CLAUDE.md); no Claude/Anthropic API call exists anywhere in the current interface setSESSION_TAGS is append-only and 'Personal / other' must stay present & last — the future training-exclusion gate is already baked into the type, even though the processing layer itself is not implemented

7. Consolidated key facts

There is no application/API server anywhere in this system. Every client (add-in, viewer, admin, team, addon-sheets, website) talks directly to Postgres via supabase-js/PostgREST + SECURITY DEFINER RPCs, or to one of 8 Deno Edge Functions for privileged work. SDD Milestone 2 (a real API server) has not been started.
Only the Stripe/Razorpay webhook may ever write a paid profiles.tier value (security fix C-1). org_create() previously self-granted “enterprise” gated only by a free-email-domain check — any ~$1 custom domain bypassed it.
Encryption is additive and opt-in, not universal. A plaintext .rewind's deflate is deterrence only (documented in container.ts's own header comment), not confidentiality. AES-256-GCM zero-knowledge encryption (OPS-6) shipped to production in v3.23.0-.2; verify any doc/marketing security claim against the actual code path (per rewind-encryption-gap project memory).
Google/Apple OAuth cannot run inside an Office.js WebView2 pane or an Apps Script sandbox — both platforms block embedded-webview OAuth. The Excel add-in works around this by opening viewer.excelrewind.com/google-signin.html in the system browser (deliberately excluded from the manifest's AppDomains to force the handoff) and polling a nonce-keyed stash_handoff/claim_handoff RPC pair that expires after 10 minutes. The Google Sheets add-on sidesteps the problem entirely by using email-OTP only.
Feature entitlement resolves global → tier → org → user, most-specific wins, except a global disabled is a hard-kill floor no narrower scope can re-enable (resolve_feature_state(), effective_features() RPC). public.tiers is the single canonical source of the 4 tiers (free/starter/team/enterprise); tiers retire (active=false) rather than delete.
Companion helpers never launch Excel — attach-only via GetActiveObject (Windows COM) / AppleScript (Mac), and the Windows COM reference is released after every idle call, the root-cause fix for a historical ghost-EXCEL.EXE ownership bug. Both speak an identical loopback wire protocol (5 ports, same JSON shapes, Bearer auth) so apps/addin-excel/src/companion.ts is unmodified across OSes — Mac's difference is HTTPS-only (WKWebView blocks plain-HTTP loopback as mixed content) via a per-machine self-signed cert.
Charts are not in the Office.js shapes collection and have no native insert event. OBJECTS_MARKER/CHARTS_MARKER/PIVOTS_MARKER/TABLES_MARKER are synthetic cell addresses the recorder stamps after diffing per-sheet object counts — hard-won platform knowledge encoded directly in the type file.
Decompression-bomb caps only engage under opts.limited:true, which only apps/viewer sets (untrusted, possibly-shared file). The add-in reads its own local file unbounded by design. A future untrusted-upload surface must remember to pass this flag.
Stripe and Razorpay webhooks both match profiles.email via ILIKE and must escape %/_/\ first — documented inline as a real fixed vulnerability (an unescaped pattern like a_%@x.com could have upgraded another account's tier).
Cloudflare Pages prod branch is master, not main. Every wrangler pages deploy call must include --branch master or the custom domain silently keeps serving the previous build. deploy:admin and deploy:team are separate scripts NOT included in the top-level npm run deploy chain and must be run explicitly.
Publishing the Windows installer to the public website is a manual step, not automated anywhere in code: copy apps/installer/dist/ExcelRewind-Setup.exe into apps/website/, rerun npm run manifest, and manually wrangler pages deploy apps/website (documented in OPS.md §4b).
The CSP loopback allowance is load-bearing. Its absence in a v3.20.0 CSP tightening pass made every Companion install silently read as "not installed" because the browser blocked the /status probe as a CSP violation — apps/addin-excel/public/_headers must keep connect-src http(s)://127.0.0.1:*.
Both native installers are currently unsigned. Windows: ExcelRewindCompanion.exe triggers SmartScreen on first run. Mac: the .app/.pkg is unsigned and unnotarized (Apple Developer ID signing + notarization is a tracked owner-side TODO).
MVP sync precision is 2–3 seconds across the whole system (per root CLAUDE.md). No downstream logic anywhere should assume millisecond-accurate cross-cell ordering beyond the sort-by-t contract in packages/format/src/replay.ts.
A SOC 2 Type II readiness audit (2026-07-17) found 48 findings against the whole system (0 critical, 16 verified high); remediation had not started as of the last recorded project handoff. Treat this document's "shipped" claims as accurate as of source but check CHANGELOG.md / SOC2_AUDIT.md for the latest remediation status before repeating a security claim externally.