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 governance on project nomoi-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)

InputModel aliasResolves toRegionNotes
Check-in text / image parsegemini-2.5-flash (GOVERNANCE_CHECKIN_MODEL)Vertex AI gemini-2.5-flasheurope-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 transcriptiondisabled unless GOVERNANCE_STT_MODEL is explicitly configuredno reviewed route on the gatewayn/aThe 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 fallbackin 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 (has auth_uid for 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)

#FileWhat it does
0001schemaAll tables, generated columns, indexes.
0002rls_datelockRLS enable + policies; role helpers (current_user_id, role_in_org, is_member, can_govern_project); the BEFORE-UPDATE date-lock trigger.
0003functions_viewsrequest/decide_date_change, rebaseline_task, run_escalations; auto-computed project_health + project_top_risk views (security_invoker); grants.
0004demo_seedMeridian Health demo org (4 projects, RAG spread, tasks, risks).
0005checkin_channeluser_by_phone, open_tasks_for_user, record_checkin (sanctioned owner-writable check-in path).
0006whatsapp_sourceadds whatsapp to the check-in source enum.
0007checkin_bindinglast_prompted_task (per-prompt task binding).
0008access_codes_identityorg_access_codes + redeem_access_code, generate_wa_link_code, link_whatsapp_by_code, join_org_by_whatsapp.
0009code_hardeningcode expiry/max-uses; WhatsApp JOIN restricted to owner/viewer; extensions.gen_random_bytes fix; wider link-code entropy.
0010org_adminmint_access_code, admin_list_members, admin_add_owner, admin_set_role, admin_remove_member, admin_deactivate_code (last-admin guard).
0011provision_orgoperator one-call new-tenant + single-use admin bootstrap code.
0012fix_users_policy_recursionrewrote users_self SELECT policy to remove 42P17 self-recursion (see §9).
0038open_tasks_rlsopen_tasks_for_user becomes SECURITY INVOKER (RLS stays the tenant boundary; service_role keeps cross-org worker behavior).
0039auth_throttleHMAC-keyed atomic throttle tables + service-role RPCs (login email / OTP verify limits; shadow-log mode default).
0040transactional_imports_and_rfp_decisionatomic SECURITY INVOKER RPCs: import_project_plan, import_vendor_contract, import_vendor_rfp, select_rfp_winner.
0041channel_hardening_and_renewal_radarinbound_email_seen + whatsapp_seen (replay dedupe), notifications.smtp_message_id + renewal_radar type.
0042document_expiry_and_packet_evidencevendor_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.
0044enable_pgcryptopgcrypto enabled (watchdog originally called digest(); see 0045).
0045watchdog_dedupe_md5watchdog 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_request applied by decide_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_risk are 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 /join redeems 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/new creates a project (current = baseline on insert).
  • /projects/[id] (authoring controls in ProjectAuthoring.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; governance added via the authenticator

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 users recursed; rewritten. LESSON: test

authed-RLS read paths with a real session, not just the service client or definer RPCs.

  • Cloudflare 1010 — *.nomoi.ai bot-fight blocks scripted UAs (python-urllib); use curl -A or 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.