Docs
Architecture
Canonical technical reference for the system.
NOMOI Governance — Architecture
Canonical technical reference for the system live at https://governance.nomoi.ai. Repo: github.com/asselwan/nomoi-governance. Last updated 2026-07-02.
1. Overview
An opinionated, multi-tenant portfolio-governance SaaS. Delivery leads run a portfolio where plan dates are immutable, check-ins are collected with zero friction over WhatsApp and email, and the executive dashboard is exception-only and ends in a decision. Each client is an isolated tenant on a shared Supabase Postgres database (schema governance), enforced by Row-Level Security.
2. Stack
- App: Next.js 14.2 (App Router, React server components + server actions), Tailwind 3.4.
- Data + auth: Supabase Postgres, dedicated schema
governanceon projectnomoi-core
(umodapwphcxtiijizqll); Supabase Auth (email magic link) via @supabase/ssr.
- Hosting: Coolify on Hetzner (app uuid
fjj86rbam4qtre3tifvqk9ls), fronted by Cloudflare
(tunnel b4ef3f70-...; each subdomain needs an ingress rule).
- Channels: Twilio (WhatsApp), Mailgun (inbound email), Zoho SMTP (outbound email), the NOMOI
LiteLLM gateway litellm.nomoi.ai (multimodal parsing; voice STT is currently unrouted, see below).
- Cron: a Coolify scheduled task (Tue/Fri) hits the check-in prompt endpoint.
LLM data path (verified 2026-07-22)
| Input | Model alias | Resolves to | Region | Notes |
|---|---|---|---|---|
| Check-in text / image parse | gemini-2.5-flash (GOVERNANCE_CHECKIN_MODEL) | Vertex AI gemini-2.5-flash | europe-west3 (Frankfurt) | Vertex only, never AI Studio. Same city as the Supabase data. Vertex AI does not use submitted data to train Google models. |
| Voice note transcription | disabled unless GOVERNANCE_STT_MODEL is explicitly configured | no reviewed route on the gateway | n/a | The app fails closed before any provider call and the user is asked to text. Voice STT stays off until an in-region provider is wired and reviewed. |
| No gateway configured | — | heuristicParse regex fallback | in the app (no model call) | Runs in the governance server process; no LLM provider is contacted. |
Residency posture (engineering note, not a compliance determination): project data rests in the EU (Frankfurt) and the check in LLM runs on Vertex in Frankfurt. A Google Cloud HIPAA Business Associate Agreement is accepted on the account (founder-confirmed 2026-07-22), so the AI processing path on Vertex is BAA-covered and product copy may reference it. For the UAE market the relevant regimes are ADHICS (DoH Abu Dhabi) and PDPL; me central1 (UAE) does not currently serve the model, so Frankfurt is the closest available region.
3. Data model (schema governance)
- orgs — tenants. users — decoupled from
auth.users(hasauth_uidfor web logins,phone
for WhatsApp, wa_link_code/wa_link_expires for phone linking). memberships — (user, org, role) where role ∈ exec | pm | owner | viewer.
- projects — org_id, name, pm_id, baseline_start/end, current_start/end, ask_of_leadership,
cached health (never the source of truth).
- tasks — project_id, title, owner_id, is_critical, progress_pct, status, notes,
baseline_start/end (immutable), current_start/end (PM-only), variance_days (generated = current_end − baseline_end), date_change_pending, last_update_at.
- date_change_requests — the gated change-control workflow (proposed dates + mandatory reason +
PM decision).
- risks — PMI 5x5: probability, impact,
exposure(generated = probability × impact),
mitigation, action, status.
- check_in_logs — every check-in (source ∈ slack | teams | email | web_form | api_sync |
whatsapp), raw payload retained.
- notifications — escalation/prompt queue with
dedupe_key. - audit_log — append-only record of every governance action.
- org_access_codes — role-bearing join codes with optional expiry + max-uses.
4. Migrations (0001–0012)
| # | File | What it does |
|---|---|---|
| 0001 | schema | All tables, generated columns, indexes. |
| 0002 | rls_datelock | RLS enable + policies; role helpers (current_user_id, role_in_org, is_member, can_govern_project); the BEFORE-UPDATE date-lock trigger. |
| 0003 | functions_views | request/decide_date_change, rebaseline_task, run_escalations; auto-computed project_health + project_top_risk views (security_invoker); grants. |
| 0004 | demo_seed | Meridian Health demo org (4 projects, RAG spread, tasks, risks). |
| 0005 | checkin_channel | user_by_phone, open_tasks_for_user, record_checkin (sanctioned owner-writable check-in path). |
| 0006 | whatsapp_source | adds whatsapp to the check-in source enum. |
| 0007 | checkin_binding | last_prompted_task (per-prompt task binding). |
| 0008 | access_codes_identity | org_access_codes + redeem_access_code, generate_wa_link_code, link_whatsapp_by_code, join_org_by_whatsapp. |
| 0009 | code_hardening | code expiry/max-uses; WhatsApp JOIN restricted to owner/viewer; extensions.gen_random_bytes fix; wider link-code entropy. |
| 0010 | org_admin | mint_access_code, admin_list_members, admin_add_owner, admin_set_role, admin_remove_member, admin_deactivate_code (last-admin guard). |
| 0011 | provision_org | operator one-call new-tenant + single-use admin bootstrap code. |
| 0012 | fix_users_policy_recursion | rewrote users_self SELECT policy to remove 42P17 self-recursion (see §9). |
| 0038 | open_tasks_rls | open_tasks_for_user becomes SECURITY INVOKER (RLS stays the tenant boundary; service_role keeps cross-org worker behavior). |
| 0039 | auth_throttle | HMAC-keyed atomic throttle tables + service-role RPCs (login email / OTP verify limits; shadow-log mode default). |
| 0040 | transactional_imports_and_rfp_decision | atomic SECURITY INVOKER RPCs: import_project_plan, import_vendor_contract, import_vendor_rfp, select_rfp_winner. |
| 0041 | channel_hardening_and_renewal_radar | inbound_email_seen + whatsapp_seen (replay dedupe), notifications.smtp_message_id + renewal_radar type. |
| 0042 | document_expiry_and_packet_evidence | vendor_documents.expires_at/doc_category; run_document_expiry_watchdog (60/30/7/expired chases, dedupe_key); import_vendor_rfp extended; audit_vendor_contract_read policy; vendor_packet_evidence. |
| 0044 | enable_pgcrypto | pgcrypto enabled (watchdog originally called digest(); see 0045). |
| 0045 | watchdog_dedupe_md5 | watchdog dedupe hash switched to built-in md5 — pgcrypto installs to the extensions schema outside the function's search_path (caught by runtime probe 2026-08-25). |
All migrations are applied LIVE to nomoi-core via the Supabase MCP apply_migration; the repo files are the source of record.
5. Security model (the moat, in the database)
- Owner cannot change dates. A BEFORE-UPDATE trigger (
enforce_task_datelock, SECURITY INVOKER)
keys off current_user: API roles (authenticated/anon) are blocked from changing baseline dates, current dates, project_id, or date_change_pending. Only trusted backend contexts (the SECURITY DEFINER RPCs, which run as postgres, and service_role) may. Verified to hold even for the PM/admin: they can create a task with dates but cannot edit those dates afterward — they re-baseline (logged) or approve a change request.
- Baseline immutable except via
rebaseline_task()(PM/exec, logged with a reason). - Current dates move only via an approved
date_change_requestapplied bydecide_date_change(). - Tenant isolation. RLS on every table scopes reads/writes to the caller's orgs via
is_member/role_in_org (both SECURITY DEFINER). No cross-org path (two independent audits).
- Auto-computed health.
project_health/project_top_riskare security_invoker views computed
from variance — never self-declared.
- Append-only audit_log (no INSERT/UPDATE/DELETE policy for API roles; definer functions write).
- Demo mode (
GOVERNANCE_DEMO_MODE=true) serves the public/dashboard,/board,/vendors
views from code-level fictional fixtures (src/lib/demo-governance.ts, demo-vendors.ts). No database query and no service client run on the anonymous demo path, so real tenant rows can never reach the public demo. Nested routes stay authenticated.
6. Auth + identity
- Web: email magic link (
/login→/auth/callback) via@supabase/ssr; then/joinredeems an
org access code (redeem_access_code) to create the profile + membership at the code's role. Middleware policy lives in src/lib/supabase/middleware-policy.ts — in normal mode it protects /dashboard, /board, /projects/*, /vendors(+nested), /admin, /billing, /link-whatsapp, /orgs/new; in demo mode only the three top-level demo views stay public. Anchored to the auth email (users.auth_uid).
- WhatsApp:
JOIN <code> <name>self-onboards owner/viewer (join_org_by_whatsapp, pm/exec are
web-only); LINK <code> binds a phone to an existing account (link_whatsapp_by_code, code from /link-whatsapp). Anchored to users.phone.
- Roles: owner (progress only), pm (admin — full org control), exec (read + admin), viewer (read).
- New tenant: operator calls
provision_org(name)→ isolated org + single-use pm bootstrap code.
7. Authoring (pm/exec only, RLS-scoped authed client)
/projects/newcreates a project (current = baseline on insert)./projects/[id](authoring controls inProjectAuthoring.tsx): add/reassign/delete tasks, add/
edit/delete risks, edit the ask, re-baseline (RPC), delete project.
- Dates are only ever set at INSERT or via
rebaseline_task; there is no raw date edit (the trigger
rejects it). Owners/viewers see the read-only view.
8. Check-ins (zero-friction, multimodal)
src/lib/checkin.ts normalises any input to {progress_pct, is_blocked, status_notes}:
- text → LLM extraction; voice → STT then extraction (STT is disabled and fails closed, so
voice notes fail and the user is asked to text); image/PDF → multimodal Gemini vision (parseCheckinFromMedia). Heuristic fallback if the gateway is down.
/api/whatsapp(Twilio, signature-validated) routes by media type; task selection = explicit
"task N"/title match > last-prompted task > top at-risk; writes via record_checkin.
/api/email/inbound(Mailgun) verifies the Mailgun signature + the signed reply address
(gov+<user>.<task>.<sig>@mg.nomoi.ai) + that the sender matches the owner's registered email + SPF.
/api/cron/checkin-prompt(Coolify Tue/Fri) sends the prompt via WhatsApp (phone) or email.
9. Notable incidents / lessons (fixed)
- Lockfile drift — Codex built the lockfile offline; regenerated for a clean
npm ci(first deploy). - Exposed-schema — PostgREST serves only listed schemas;
governanceadded via theauthenticator
role GUC (pgrst.db_schemas) + reload; do NOT add a non-existent schema (broke the whole instance once with a bogus frontdesk).
- users_self recursion (0012) — an authed read of
usersrecursed; rewritten. LESSON: test
authed-RLS read paths with a real session, not just the service client or definer RPCs.
- Cloudflare 1010 —
*.nomoi.aibot-fight blocks scripted UAs (python-urllib); use curl-Aor a
browser. Real Twilio/Mailgun/server traffic passes.
10. Environment variables
See docs/RUNBOOK.md for the full catalog and where each value comes from.