apps/admin · ExcelRewind monorepo · doc generated for the architecture documentation suite
apps/admin is the owner-only superadmin console for ExcelRewind. It is a small
zero-router React 18 + TypeScript single-page app (Vite, port 5175 in dev) that gives the
product owner a UI over metrics and every destructive/administrative action in the system:
tier design, feature entitlements, user tier changes and revocation/deletion, coupons,
referral/promo configuration, pricing, cohort (group-licence) management, and a global
maintenance kill switch plus a one-shot pre-launch data wipe.
It is deliberately not a normal Supabase-auth application. Instead of RLS-scoped
user sessions, the admin app talks to two purpose-built Supabase Edge Functions —
metrics (GET, read-only analytics) and admin-ops (POST, all mutations) —
authenticated with a static bearer token (OWNER_METRICS_TOKEN, stored in the
browser's localStorage) plus a second factor: a live Supabase session for one
hardcoded superadmin email, obtained via email OTP and carried on every admin-ops
call as the x-superadmin-jwt header. Both edge functions run under the Supabase
service role (bypasses RLS) — admin-ops is explicitly documented as
"the ONLY write path to these [tables] from outside SQL".
Deployment: a static Cloudflare Pages site at admin.excelrewind.com, built with
vite build from this workspace. apps/admin/public/_headers sets
X-Robots-Tag: noindex on every route (the console must never appear in search) and a
strict CSP whose connect-src is pinned to the Supabase project origin only — no
third-party script/analytics origins are allowed. The app has no server component of its own;
all business logic and authorization checks live in the edge function
(supabase/functions/admin-ops/index.ts) and the Postgres functions/RPCs it calls
under the service role. A ?demo=1 query flag skips the gate entirely and renders
hardcoded sample data (src/demo.ts) with every mutating call short-circuited — it
touches no real database and is deliberately left open (documented in src/api.ts) as
a backend-independent preview mode.
flowchart TB
subgraph ENTRY["Entry / bootstrap"]
main["main.tsx"] --> App["App.tsx<br/>top-level auth gate + tab router"]
end
subgraph AUTHUI["Auth UI"]
Gate["Gate.tsx<br/>2-factor sign-in: owner token + email OTP"]
end
subgraph SHELL["Shell / chrome"]
ShellC["Shell.tsx<br/>sidebar nav (11 tabs) + topbar + demo banner"]
UI["ui.tsx<br/>Icons, Avatar, TierBadge, StatusBadge,<br/>ToastHost/useToast, Modal, DemoFallbackBanner, ThemeToggle"]
end
subgraph SCREENS["screens/ (11 tabs)"]
Overview["Overview.tsx"]
Growth["Growth.tsx"]
Revenue["Revenue.tsx"]
Cohorts["Cohorts.tsx"]
Users["Users.tsx"]
Coupons["Coupons.tsx"]
Referrals["Referrals.tsx"]
Pricing["Pricing.tsx"]
Tiers["Tiers.tsx"]
Features["Features.tsx"]
System["System.tsx"]
end
subgraph SHARED["Shared client modules"]
Api["api.ts<br/>store (token/url), adminOps.*, fetchMetrics,<br/>ApiError, friendly(), isMissing(), types"]
Supa["supabase.ts<br/>supabase-js client (OTP-only),<br/>SUPERADMIN_EMAIL, superadminJwt()"]
Demo["demo.ts<br/>demoUsers/demoTiers/demoFeatures/<br/>demoPromo/demoAccountLookup"]
Hook["useTierOptions.ts<br/>shared tier-picker hook (calls tierList)"]
Util["util.ts<br/>fmtNum, fmtDate"]
Charts["charts.tsx<br/>sparkline / bar chart primitives"]
end
main --> AUTHUI
App -->|"authed=false"| Gate
App -->|"authed=true"| ShellC
ShellC --> SCREENS
ShellC --> UI
Gate --> Api
Gate --> Supa
SCREENS --> Api
SCREENS --> UI
SCREENS --> Demo
SCREENS --> Hook
SCREENS --> Util
Overview --> Charts
Growth --> Charts
Revenue --> Charts
Hook --> Api
Api --> Supa
System -->|"direct read only:<br/>app_config select"| Supa
Notes on the diagram: there is no client-side router library — Tab is a plain string
union persisted to localStorage (rewind.admin.tab) and switched with a
ternary chain in App.tsx. There is no global state manager (Redux/Zustand); each
screen owns its own useState/useEffect data-loading, following one
repeated shape: load() → try adminOps.X() → on 404 fall back
to demo.ts data + a toast, on other errors show a toast and set an empty/last-good
state. System.tsx is the one screen that reads Postgres directly via
supabase-js (app_config, RLS-readable by any signed-in session) instead of
going through admin-ops, purely to reflect real maintenance state on load.
flowchart LR
subgraph BROWSER["Admin browser (owner only)"]
LS["localStorage<br/>rewind.metrics-url, rewind.metrics-token"]
SS["sessionStorage<br/>Supabase OTP session (via supabase-js)"]
UIState["React component state<br/>(no persistence beyond active tab)"]
end
subgraph EDGE["Supabase Edge Functions (Deno, service role)"]
Metrics["metrics<br/>GET ?days=N"]
Ops["admin-ops<br/>POST {action, ...args}"]
end
subgraph DB["Postgres (service role — bypasses RLS)"]
Tables["profiles, tiers, feature_flags,<br/>coupons, coupon_redemptions, referrals,<br/>promo_config, pricing_config, orgs,<br/>app_config, exams, ..."]
RPCs["RPCs: user_delete, tier_create/set/delete,<br/>account_lookup, admin_data_reset,<br/>feature_set/reset, cohort_* "]
end
LS -->|"Authorization: Bearer OWNER_METRICS_TOKEN"| Metrics
LS -->|"Authorization: Bearer OWNER_METRICS_TOKEN"| Ops
SS -->|"x-superadmin-jwt: <access_token> (2nd factor)"| Ops
UIState -->|"JSON body {action, user_id, tier, ...}"| Ops
Metrics -->|"JSON: {overview, addin, feedback, bugs,<br/>revenue, growth}"| UIState
Ops -->|"JSON: {ok} / {users:[...]} / {tiers:[...]} / {error}"| UIState
Metrics -.->|"read-only SELECT / views"| Tables
Ops --> RPCs
Ops -->|"direct table read/write<br/>(app_config, coupons, promo_config, ...)"| Tables
RPCs --> Tables
UIState -.->|"direct: supabase.from('app_config').select(...)"| Tables
The admin app never receives or stores .rewind file bytes, session events, or
spreadsheet content — its payloads are exclusively administrative JSON (tier keys, feature
states, user rows, coupon config, pricing numbers). Nothing here handles the
timeline.json/annotations.json/meta.json components of the
.rewind format; that lives entirely in packages/format and the
add-in/viewer.
sequenceDiagram
participant Owner
participant GateUI as "Gate.tsx"
participant Api as "api.ts (store/verifyToken)"
participant Metrics as "metrics fn"
participant SupaJS as "supabase.ts (supabase-js)"
participant Auth as "Supabase Auth"
participant Ops as "admin-ops fn"
Owner->>GateUI: enter metrics URL + owner token
GateUI->>Api: store.url = url, store.token = token
GateUI->>Api: verifyToken()
Api->>Metrics: GET ?days=1 (Authorization: Bearer token)
alt token rejected (401)
Metrics-->>Api: 401
Api-->>GateUI: false
GateUI-->>Owner: "Token rejected." (stays on step 1)
else network/other error
Metrics-->>Api: error (non-401)
Api-->>GateUI: true (treated as "maybe fine")
else ok
Metrics-->>Api: 200 JSON
Api-->>GateUI: true
end
GateUI->>GateUI: setStep('otp')
GateUI->>SupaJS: supabase.auth.signInWithOtp({ email: SUPERADMIN_EMAIL })
SupaJS->>Auth: send OTP email
Owner->>GateUI: enter 6-digit code
GateUI->>SupaJS: supabase.auth.verifyOtp({ email, token: code, type:'email' })
SupaJS->>Auth: verify
alt bad code
Auth-->>GateUI: error
GateUI-->>Owner: "Code rejected"
else ok
Auth-->>SupaJS: session (sessionStorage)
GateUI->>App: onAuthed()
end
Note over Api,Ops: Every later admin-ops call carries BOTH the owner token AND<br/>x-superadmin-jwt = current session access_token (superadminJwt()).<br/>admin-ops independently re-validates the JWT against Supabase Auth<br/>and rejects any email other than SUPERADMIN_EMAIL — the client UI<br/>gate is convenience only, not the real security boundary.
sequenceDiagram
participant Admin as "Superadmin"
participant FeaturesUI as "Features.tsx"
participant Ops as "admin-ops fn"
participant RPC as "feature_set() / feature_reset() (Postgres)"
participant Flags as "feature_flags table"
Admin->>FeaturesUI: search email in "Account exception" card
FeaturesUI->>Ops: POST {action:'account_lookup', email}
Ops->>RPC: account_lookup(p_email)
RPC-->>Ops: {user_id, email, tier, org_id, member_count, scope, subject}
Ops-->>FeaturesUI: {account}
FeaturesUI->>Ops: POST {action:'features_get', scope, subject}
Ops-->>FeaturesUI: {features:[{feature, global, own}]}
Admin->>FeaturesUI: toggle a feature row to "Disabled"
alt global state for that feature is already 'disabled'
FeaturesUI-->>Admin: control rendered disabled client-side (lockedByGlobal)
else
FeaturesUI->>Ops: POST {action:'feature_set', scope, subject, feature, state}
Ops->>RPC: feature_set(...) [service role, no RLS]
RPC->>Flags: upsert override row at (scope, subject, feature)
RPC-->>Ops: ok / error (bad_scope, bad_state, subject_required, bad_subject)
alt success
Ops-->>FeaturesUI: {ok:true}
FeaturesUI-->>Admin: toast "ok", optimistic row update
else error
Ops-->>FeaturesUI: {error} (400/500)
FeaturesUI-->>Admin: toast "err" via friendly(e)
end
end
Note over RPC,Flags: Resolution order enforced server-side too: account > tier > global,<br/>EXCEPT a global 'disabled' is a hard-kill floor no narrower scope can lift.
sequenceDiagram
participant Admin as "Superadmin"
participant UsersUI as "Users.tsx"
participant Ops as "admin-ops fn"
participant DB as "Postgres (service role)"
Admin->>UsersUI: click "Change tier" -> pick tier + optional free months
UsersUI->>Ops: POST {action:'user_set_tier', user_id, tier, months?}
Ops->>DB: UPDATE profiles SET tier=..., tier_expires_at = months ? now()+months : null
DB-->>Ops: ok
Ops-->>UsersUI: {ok:true}
UsersUI-->>Admin: toast + optimistic row update
Admin->>UsersUI: click "Revoke"
UsersUI->>Ops: POST {action:'user_set_status', user_id, status:'revoked'}
Ops->>DB: UPDATE profiles SET status='revoked'
Ops-->>UsersUI: {ok:true}
Admin->>UsersUI: click "Delete" -> type exact email to confirm
UsersUI->>Ops: POST {action:'user_delete', user_id, email}
Ops->>DB: rpc user_delete(p_user, p_email)
alt email mismatch or user owns an org
DB-->>Ops: error (user_owns_org / mismatch)
Ops-->>UsersUI: {error} (400)
UsersUI-->>Admin: toast "This account owns an org - reassign or delete that org first."
else ok
DB-->>Ops: {deleted:true, email}
Ops-->>UsersUI: {deleted:true, email}
UsersUI-->>Admin: row removed from table, toast "ok"
end
Note over UsersUI: Bulk actions (multi-select) run this same setTier/setStatus call<br/>once per selected user, sequentially, client-side (no batch RPC) —<br/>see runBulk() in Users.tsx.
sequenceDiagram
participant Admin as "Superadmin"
participant SystemUI as "System.tsx"
participant SupaJS as "supabase-js (session-authed read)"
participant Ops as "admin-ops fn"
participant Cfg as "app_config (id=1)"
SystemUI->>SupaJS: on mount: select enabled,message from app_config where id=1
SupaJS->>Cfg: SELECT (RLS: any signed-in session may read)
Cfg-->>SystemUI: {enabled, message}
SystemUI->>SystemUI: on = !enabled (enabled=true means the app is UP)
Admin->>SystemUI: flip the Maintenance Mode toggle ON
SystemUI->>Ops: POST {action:'maintenance_set', enabled:false, message}
Ops->>Cfg: UPDATE app_config SET enabled=false, message=... WHERE id=1
Ops-->>SystemUI: {ok:true}
SystemUI-->>Admin: banner "Maintenance mode is live" + toast
Note over Cfg: Only the Excel add-in and the Google Sheets add-on read<br/>app_config.enabled to gate/block usage and show the message — the viewer<br/>and team console do not check it (verified: no app_config reference in either app).
sequenceDiagram
participant Admin as "Superadmin"
participant SystemUI as "System.tsx (DangerZone)"
participant Browser as "window.confirm"
participant Ops as "admin-ops fn"
participant RPC as "admin_data_reset() (Postgres)"
Admin->>SystemUI: type exact phrase "RESET DATA"
SystemUI->>SystemUI: armed = (phrase === 'RESET DATA')
Admin->>SystemUI: click "Reset all data"
SystemUI->>Browser: confirm("FINAL CONFIRM: permanently wipe ... Proceed?")
alt cancelled
Browser-->>SystemUI: false
SystemUI-->>Admin: no-op
else confirmed
SystemUI->>Ops: POST {action:'data_reset', confirm:'RESET DATA'}
Ops->>Ops: re-check body.confirm === 'RESET DATA' (guard #3, server-side)
Ops->>RPC: admin_data_reset(p_confirm:'RESET DATA')
RPC->>RPC: wipes users + telemetry, KEEPS tiers/flags/pricing/promo/app_config
RPC-->>Ops: {ok, kept:{tiers,global_flags,...}, cleared:{...}}
Ops-->>SystemUI: {ok, kept, cleared}
SystemUI-->>Admin: "Done. ... Sign in again to recreate your superadmin account."
end
flowchart LR Admin["apps/admin<br/>(admin.excelrewind.com)"] Metrics["Edge fn: metrics"] Ops["Edge fn: admin-ops"] Auth["Supabase Auth<br/>(email OTP)"] PG["Supabase Postgres<br/>(service role, RLS bypassed by admin-ops;<br/>RLS-read for direct app_config read)"] CFPages["Cloudflare Pages<br/>(static hosting)"] Admin -->|"HTTPS GET, Bearer OWNER_METRICS_TOKEN"| Metrics Admin -->|"HTTPS POST, Bearer OWNER_METRICS_TOKEN + x-superadmin-jwt"| Ops Admin -->|"supabase-js: signInWithOtp / verifyOtp / getSession"| Auth Admin -->|"supabase-js: select on app_config (session-RLS)"| PG Metrics --> PG Ops --> PG Ops -->|"validates JWT signature/expiry + email"| Auth CFPages -->|"serves static build"| Admin
| Target | Protocol / call | Purpose |
|---|---|---|
metrics edge function | HTTPS GET /functions/v1/metrics?days=N, header Authorization: Bearer OWNER_METRICS_TOKEN | Read-only analytics contract (Metrics type) rendered by Overview, Growth, Revenue tabs. Also used as the cheap token-validity probe in the Gate (verifyToken(), days=1). |
admin-ops edge function | HTTPS POST /functions/v1/admin-ops, body {action, ...args}, headers Authorization: Bearer OWNER_METRICS_TOKEN + x-superadmin-jwt | Every mutation and most reads in the console: users, tiers, features, coupons, promo/referrals, pricing, cohorts, maintenance, data reset. 27 actions (see BACKEND_CONTRACT.md and §6). |
| Supabase Auth | supabase-js: auth.signInWithOtp, auth.verifyOtp, auth.getSession, auth.signOut | Second-factor login as the fixed SUPERADMIN_EMAIL (email OTP). Session persisted to sessionStorage only (fresh browser session ⇒ re-OTP). The resulting access token is what admin-ops validates as x-superadmin-jwt. |
Postgres table app_config | supabase-js: .from('app_config').select('enabled,message').eq('id',1).maybeSingle() | The only direct-to-DB read in the app (System.tsx) — loads real maintenance state on mount via the OTP session's RLS-scoped read, so the toggle reflects reality before any write. |
Postgres (via admin-ops, service role) | direct table reads/writes + RPC calls (user_delete, tier_create/set/delete, account_lookup, admin_data_reset, feature_set/feature_reset, cohort RPCs) | All administrative state: profiles, tiers, feature_flags, coupons, coupon_redemptions, referrals, promo_config, pricing_config, orgs, app_config. |
| Cloudflare Pages | static hosting, deployed via npm run deploy:admin (wrangler) — a separate script from the root npm run deploy, which only covers the add-in/viewer/website | Serves the built SPA at admin.excelrewind.com; public/_headers sets CSP, HSTS, and X-Robots-Tag: noindex. |
| None: no Office.js / Apps Script host | — | The admin console is a standalone web app; it never runs inside Excel or Google Sheets and has no relationship to the add-in/add-on hosts. |
| File | Function / export | What it does | Why it matters |
|---|---|---|---|
App.tsx | App() | Top-level gate: renders Gate until authed, then Shell + the tab whose id matches tab state. Re-checks a stored token against a live OTP session on mount via superadminJwt(). | A stored OWNER_METRICS_TOKEN alone is not enough to reach the console after a refresh — the effect forces re-verification of the live Supabase OTP session before setting authed. |
Gate.tsx | Gate(), submitToken(), sendCode(), submitCode() | Two-step sign-in UI: (1) metrics URL + owner token, verified with a cheap fetchMetrics(1) probe; (2) email OTP to the fixed superadmin address. | The metrics URL field is deliberately left blank (never prefilled) — "a stranger who reaches the gate must already know the metrics URL," and the OTP step never names the destination address, so a leaked owner token alone reveals nothing about where the code lands. |
api.ts | store | Thin localStorage wrapper for the metrics URL/token, reusing the same keys as apps/website/admin.html so a signed-in owner's config carries over between UIs. | Auth "session" for the owner-token half of the two-factor scheme lives entirely client-side in localStorage; it is not itself sufficient to call admin-ops (see ops()). |
api.ts | ops<T>(action, body) | Generic POST wrapper for every admin-ops call: attaches the owner-token bearer plus x-superadmin-jwt (from superadminJwt()), throws ApiError on non-2xx. | Single choke point for the second factor — every mutation goes through here, so the JWT header is never forgotten by an individual screen. |
api.ts | adminOps.usersList() | Calls users_list then normalizes each row's id → user_id. | Explicitly documented gotcha: the server returns rows keyed id, not user_id; skipping this normalization makes every subsequent tier/revoke write a silent no-op (undefined user_id matches no profile) while the UI still reports success. |
api.ts | friendly(e), isMissing(e), ApiError | Turns a raw fetch/HTTP failure into a toast-ready message; isMissing flags a 404/501 (the whole admin-ops function not deployed yet) so callers fall back to demo data instead of showing a raw error. | Explains why the console is usable pre-deploy — every screen degrades to demo.ts data on 404 rather than breaking. |
api.ts | adminOps.userDelete, dataReset, tierDelete, featureReset, etc. | Typed wrappers for all 27 admin-ops actions (see full list in §5's Interconnections table and the sequence diagrams). | This object is the client-side contract; cross-reference against supabase/functions/admin-ops/index.ts's switch(action) (line ~124) when auditing for drift. |
supabase.ts | SUPERADMIN_EMAIL | Env-driven (VITE_SUPERADMIN_EMAIL), lowercased, fallback tm@excelrewind.com. | Must exactly match the SUPERADMIN_EMAIL secret read by admin-ops's Deno runtime — a mismatch locks the owner out with 401 otp_required even with a correct OTP. |
supabase.ts | superadminJwt() | Returns the current session's access token only if the signed-in email equals SUPERADMIN_EMAIL; '' otherwise. | Client-side mirror of the server check — but the server (isSuperadminJwt in admin-ops) re-validates independently against Supabase Auth, so this function is a UX nicety, not the security boundary. |
screens/Tiers.tsx | Tiers(), patch(), remove(), NewTier() | Tier designer: list/edit tiers inline (name, price, has-members, seat_limit, active), create new tiers, delete tiers with zero users. Optimistic UI mirrors the server's "seat_limit snaps to 1 when has_members turns off" rule. | public.tiers is the canonical tier list per CLAUDE.md — this screen is its only write UI. Deleting a tier is blocked client- and server-side while user_count > 0. |
screens/Features.tsx | Features(), AccountExceptionCard(), PerTierCard(), lockedByGlobal(), FEATURE_META | The full entitlement cascade UI: global state per feature, per-tier override, and one-off account exception (resolved by email via account_lookup to either an org subject or a user subject). | lockedByGlobal(global) = global === 'disabled' is the client-side mirror of the server's hard-kill floor — a global disabled greys out every narrower control so an admin can't be misled into thinking a lower-level toggle will re-enable it (it will be rejected server-side too). |
screens/Users.tsx | Users(), setStatus(), runBulk(), DeleteUserModal, TierModal | Searchable user table with per-row and bulk (multi-select) tier change / revoke / restore, plus a guarded hard-delete flow. | runBulk() is a plain client-side sequential loop over setTier/setStatus calls (commented ponytail: — no batch RPC), so a large bulk action is O(n) network round-trips, one per selected user, with a live progress modal. |
screens/Users.tsx | DeleteUserModal | Hard-delete confirm gated behind typing the user's exact email; surfaces the server's user_owns_org error as a friendly message. | Deletion is irreversible and blocked entirely if the account owns an org — the UI must guide the admin to reassign the org first rather than silently failing. |
screens/System.tsx | MaintenanceCard() | Global maintenance kill switch: reads real state from app_config on mount (direct supabase-js read), writes via adminOps.maintenanceSet(!next, msg). | Inverted boolean convention: UI's "Maintenance ON" = app_config.enabled = false. Getting the inversion wrong in either direction takes the product down or leaves it silently up during an incident — called out twice in code comments. |
screens/System.tsx | DangerZone(), RESET_PHRASE | Triple-guarded pre-launch data wipe: (1) exact-phrase text input arms the button, (2) window.confirm, (3) server re-checks the phrase inside admin_data_reset(). | Wipes users + all telemetry but explicitly keeps config (tiers, feature flags, pricing, promo, maintenance) — meant to run exactly once before public launch to clear beta clutter. |
useTierOptions.ts | useTierOptions(includeFree?) | Shared hook wrapping adminOps.tierList() for every tier <select> in the app (Cohorts, Coupons, Users, …), with demo/offline fallback. | Prevents any screen from hardcoding a tier list — enforces the "tiers table is the single source of truth" rule from CLAUDE.md at the client level too. |
Shell.tsx | NAV, TITLE, CRUMB | Static config for the 11-tab sidebar (Overview, Growth, Revenue, Cohorts, Users, Coupons, Referrals, Pricing, Tiers, Features, System). | Adding a new admin screen means adding an entry here and to Tab in the same file and to the ternary in App.tsx — three places, no central registry. |
demo.ts | demoUsers, demoTiers, demoFeatures, demoAccountLookup, demoPromo | Hardcoded sample data used both for the explicit ?demo=1 preview mode and as the automatic fallback whenever admin-ops 404s. | Every screen's demo fallback path is exercised even in production if the edge function isn't deployed yet — worth knowing when a screen "looks fine" but is actually showing fake data (watch for the DemoFallbackBanner). |
| Location | Function | What it does | Why it matters |
|---|---|---|---|
| lines ~97–112 | Deno.serve handler entry | Rejects non-POST with 405; checks Authorization: Bearer against OWNER_METRICS_TOKEN with timingSafeEqual; then checks x-superadmin-jwt via isSuperadminJwt(). | Both checks happen before the request body is even parsed for the action — a caller with only one of the two factors gets a flat 401 regardless of what action they tried. |
| ~87–95 | timingSafeEqual(a,b) | Constant-time string compare for the owner token. | Explicit defense against timing side-channel leaking the token's length/prefix. |
| ~67–83 | ALLOWED_ORIGINS, cors(origin) | CORS allowlist: admin.excelrewind.com, excelrewind.com, localhost:5175, plus a dev Pages preview host. Echoes the origin only if allowlisted; otherwise falls back to the prod host (never a wildcard). | A browser-based caller from any other origin is blocked at the CORS layer even with valid tokens. |
| ~244–252 | userDelete(b) | Wraps Postgres RPC user_delete(p_user, p_email); requires user_id AND a matching email. | Server-side email confirmation, not just the client's type-to-confirm modal — defends against a stale/wrong user_id silently deleting the wrong account. |
| ~257–263 | maintenanceSet(b) | Direct UPDATE app_config SET enabled, message WHERE id=1. | No RPC/trigger layer — a bug here has an immediate, global blast radius (every client's kill switch). |
| ~453–462 | dataReset(b) | Re-checks confirm === 'RESET DATA' server-side (independent of the client's armed state) before calling RPC admin_data_reset(p_confirm). | The phrase check is duplicated deliberately — a compromised or buggy client alone cannot trigger the wipe. |
| ~534–548, ~549–565, ~577–583 | tierCreate, tierSet, tierDelete | Thin validating wrappers around RPCs tier_create/tier_set/tier_delete; map SQL errors (bad_key, tier_exists, tier_in_use, no_tier) to 400s via rethrowFeature. | This is the only mutation path for public.tiers, the single source of truth referenced throughout CLAUDE.md's Tiers section. |
| ~570–576 | accountLookup(b) | RPC account_lookup(p_email) → resolves an email to either an org subject (Team/Enterprise, so all members inherit the exception) or a user subject (solo tiers). | The scope/subject resolution logic (org vs. user) lives entirely server-side in this RPC — the client (Features.tsx) just renders whatever scope/subject comes back. |
| ~585–602+ | referralStats() | Aggregates referrals + tiers (paid = key != 'free') in JS after two service-role reads; explicitly returns only referrer emails, never referee lists. | Commented ponytail: — a deliberate "small pre-launch scale, JS aggregation beats a bespoke SQL rollup" shortcut with a named upgrade path (move to a SECURITY DEFINER SQL rollup if it grows). |
app_config select in System.tsx.
OWNER_METRICS_TOKEN bearer and a live OTP session for one hardcoded email
(x-superadmin-jwt). The client-side Gate.tsx flow is explicitly documented
as convenience only: "admin-ops enforces BOTH server-side, so this UI is convenience, not
security." A leaked owner token alone yields 401 otp_required; a stolen JWT for a
different email is rejected regardless of the token.
disabled is a hard-kill floor in the feature cascade
(global → tier → account). No tier or account-level override can re-enable a feature
the superadmin has disabled globally — enforced both in the UI (lockedByGlobal greys
out the control) and, per the backend contract, server-side too. forced mandates ON
everywhere below unless a narrower target explicitly turns it off; allowed defers to
the level below.
users_list returns rows keyed id, not user_id —
adminOps.usersList() normalizes this client-side. Skipping the normalization makes
every subsequent tier-change or revoke write target an undefined id, matching no
profile — a silent no-op where the UI still shows a success toast. This is called out
explicitly in api.ts as a trap for anyone rewriting that wrapper.
app_config.enabled = true means the app is up; the admin toggle's
on state means Maintenance Mode is on (app down). Every read and
write in MaintenanceCard negates across this boundary
(setOn(!data.enabled) on load, maintenanceSet(!next, msg) on write) —
flipping the negation in either direction either bricks the app or leaves it silently live during
an incident.
Other invariants and platform notes:
CLAUDE.md: public.tiers is the single source of truth for the tier ladder; nothing else in the codebase should hardcode a tier list. useTierOptions.ts exists specifically so every <select> in the admin app (and elsewhere) reads live from tier_list() instead of a literal array.active=false hides it from new customers while existing members keep working; deletion is only permitted once user_count === 0 (checked both client- and server-side).user_delete refuses to remove an account that owns an org — the operator must reassign the org first. The delete modal also requires typing the exact email as a last-chance confirm.runBulk() in Users.tsx), not a batch RPC — explicitly marked ponytail: as acceptable for a rare, small-scale admin operation. Partial failure is possible and surfaced as "N succeeded, M failed."referral_stats returns referrer emails and aggregate counts only, enforced in the server function's own comments and matching the product's PII-minimization posture described in CLAUDE.md (paid-tier/session data must never be casually exposed).?demo=1 is intentionally left open (no server flag gates it) because it touches zero real data — every screen swaps in demo.ts fixtures and every adminOps call is skipped (if (!isDemo) await adminOps...). It is also the fallback UI whenever admin-ops 404s, so "looks like it worked" in the console does not always mean a real write happened — check for the DemoFallbackBanner.public/_headers restricts connect-src to 'self' plus the exact Supabase project origin — no analytics, no third-party script origins — and the whole app is X-Robots-Tag: noindex so it never appears in search results, since a leaked URL is the console's first line of defense (the gate does not even prefill the metrics endpoint).supabase.ts deliberately uses window.sessionStorage so a brand-new browser session (new tab/window after closing) always re-requires the OTP step even if the owner token in localStorage is still valid.