NAI OS
A live Next.js + Supabase app for tracking client-confidential architecture
decisions. Built with GSD, not dispatched from .claude/commands. This is the one
page describing it — what it does, how it's built, and where it touches this workspace.
The agent systems in this workspace are all the same shape: type a command, an orchestrator
dispatches subagents in waves, you get a file to check. nai-analysis is something
else — a private, always-on web app — extracted to its own repository,
github.com/udopnink/nai-analysis, on 2026-07-29, so it no longer lives inside this
one — where Udo tracks architecture, MBSE, and tool-integration issues, each one either
a decision (options on the table, one eventually picked) or a
discussion (analysis with no forced choice). A specific client contact can be
invited, via Google sign-in, to view and comment on one specific issue — never anything
broader — and two narrower no-login paths exist alongside that invite: a hard-to-guess
read-only share link, and a public showcase card that carries a teaser, never the full body.
It was built with GSD’s phase-based discuss→plan→execute→verify loop rather than this workspace’s own orchestrator/subagent/tool convention, because the thing being built is a persistent product with a database and a deploy pipeline, not a one-shot dispatch that produces a document. Same underlying discipline — explicit plans, a verify step before anything is called done — applied to a different kind of artifact.
Options are on the table, and the point of the issue is that one of them eventually gets picked.
Analysis or notes get captured — no option is ever picked. A record and a place to talk something through.
Chosen at creation — Decision or Discussion — and never changed afterward, so pick carefully.
A solo EA/MBSE/tool-integration consultant tracking his own work, with specific client individuals brought into specific issues — never given broad access to anything else.
Nothing is reachable except through one of two paths, or an issue explicitly flagged for the public showcase.
Tied to one exact Google address — full participation: read, comment, and on a Decision, pick an option.
A hard-to-guess URL, no sign-in at all — view-only, no comments, no options, no controls.
Enforcement lives in Postgres RLS, not application-layer checks — the single enforcement point for who sees what.
Both no-login paths are served by narrow SECURITY DEFINER database functions; the anonymous database role holds no direct table access of its own.
/Public showcase landing page. Anyone, no sign-in.
/s/<token>Anonymous read-only view of one shared issue. Anyone holding the link, no sign-in.
/issuesThe dashboard. Any signed-in account.
/issues/newThe new-issue form. Any signed-in account.
/issues/<id>An issue's detail page. The owner/admin, plus that issue's invited client.
/issues/archiveThe archive screen. Admin only.
/issues/sharedThe share-link revoke/restore screen. Admin only.
/helpThe full help page, six topics. Any signed-in account.
An issue's body renders as markdown, the same formatting used throughout the app.
On a Decision issue, the owner, the admin, and the invited client can each pick an option.
The winning option is badged Decided with a caption of who and when; every other option dims and loses its pick control.
Owner and admin can add, edit, or delete options at any time — including after a decision is recorded and after the issue is Decided & closed.
Needs attention, Waiting, and Decided & closed — set manually from the ⋮ menu, never inferred, independent of both the decision and the showcase flag.
A flat, oldest-first markdown thread open to the owner, admin, and any invited client. Comments cannot be edited or deleted by anyone.
Grouped under three status headings, most-recently-updated first, carrying Decision/Discussion, Showcased and Archived badges as they apply.
Looks inside both the title and the full body content, not just the title.
Three multi-select pills — Needs attention, Waiting, Decided & closed — with no “All” pill. Only Needs attention is on by default; turning every pill off shows everything instead of nothing.
⋮ menuStatus changes, the showcase toggle, and — admin only — Archive, all from one per-card menu.
One Client email field with autocomplete from past invites; only one invite per issue, ever.
Signing in with the wrong address shows a dedicated mismatch screen with Sign out and try again.
Type badge, title, a read-only notice, and the body — no comments, options, status line, or controls.
Admin-only /issues/shared: title and client-email filters, checkbox selection, and direction-scoped bulk Revoke selected / Restore selected. A revoked link is indistinguishable from one that never existed, and revoking never changes the URL.
A public landing-page card showing only badge, title, and a short plain-text teaser from the start of the body — never the full content.
Hides the issue from everyone including the admin and switches off its share link and showcase card, while preserving everything underneath.
Filter the archive list down, then type DELETE exactly to enable Confirm delete — takes options, invites and comments with it, unrecoverable.
A context-sensitive slide-in panel matched to the current page, plus a full /help page carrying all six topics.
%%{init: {'theme':'neutral', 'flowchart':{'htmlLabels':false,'nodeSpacing':45,'rankSpacing':55}}}%%
flowchart TD
U["Owner or invited client<br/>signed in with Google"] --> P
A["Anyone holding a share link<br/>or reading the showcase"] --> NOLOGIN
subgraph L1 ["proxy.ts - runs on every matched request"]
P["Refreshes the Supabase<br/>session cookie"]
end
P --> PUB["(public) sign-in page"]
P --> APP["(app) route group<br/>layout.tsx gates on getClaims()"]
APP --> ACT["Server Actions<br/>issues/actions.ts"]
NOLOGIN["showcase / and share link /s/:token<br/>no session at all"]
subgraph L2 ["Supabase - the sole enforcement point"]
ANON["SECURITY DEFINER, anon-callable<br/>get_shared_issue,<br/>get_showcase_issues"]
RPC["SECURITY DEFINER RPCs<br/>create_issue_with_options,<br/>pick_decision_option,<br/>claim_pending_invite"]
RLS["Direct table writes<br/>gated by RLS policy"]
DB[("issues · issue_options<br/>issue_invites · issue_comments")]
end
ACT --> RPC
ACT --> RLS
NOLOGIN --> ANON
RPC --> DB
RLS --> DB
ANON --> DB
classDef orch fill:#dcefe9,stroke:#0f6f63,stroke-width:2px
classDef tool fill:#f4f1ea,stroke:#8a7a5a
classDef human fill:#fdf3e0,stroke:#8a5a00,stroke-width:2px
class U,A human
class P,NOLOGIN orch
class RPC,RLS,ANON,DB tool
proxy.ts +
getClaims()) for the signed-in half, a short list of anon-callable functions for the
half with no session at all, and then Postgres Row Level Security as the only place “who can
see this row” is decided. Full detail: docs/ARCHITECTURE.md in its own
repository.The boundary rule is the same species as the one on the why the promise holds page, just enforced by a different mechanism: there, a guarantee is a tool grant a subagent literally cannot exceed; here, it's a database policy the Next.js layer cannot route around even with a bug, because no Server Action performs its own ownership check — a rejected RLS write (0 rows affected) or a failing RPC is the authorization outcome, not a belt-and-suspenders backup to one.
Next.js 16 App Router with React 19, hosted on Vercel.
Postgres + Auth (Google sign-in only), with Row Level Security as the enforcement point for every access rule.
Versioned under supabase/migrations/.
Tailwind v4 with shadcn/ui components.
Throughout.
A live cross-account access-control harness under verification/ that proves the access model against the real hosted database, not against mocks.
The one seam between the two worlds is
/nai-issue-to-agent: it fetches one
nai-analysis issue, feeds it to /nai-option-study
or /nai-review, and posts the finished write-up back onto the
issue as a comment — after a STOP for explicit confirmation, since a posted comment can't be
deleted through that path. That's the only place an agent system and this app talk to each other;
everything else about nai-analysis runs on its own, independent of the systems
in this workspace.
No notification email is sent by the app itself; a separate scheduled /nai-issue-check agent handles telling Udo about new client activity.
option-study and review-pack can push decision options straight into an issue through a database function granted only to the server-side service role — no UI attached.
Content arrives by paste-in today; the agent write path exists but nothing pulls automatically, and there's no UI for it.
Access events are recorded in the database but not yet surfaced anywhere to view.
Cannot be edited or deleted by anyone, including the person who posted them.
Cannot be edited after creation — a correction belongs in a comment, not a re-edit.
A share link cannot be regenerated or rotated — only revoked and later restored to the exact same address.
No re-inviting a different address, and no control to remove or re-address an existing invite.
The only way back to an undecided state is deleting the decided option, which clears the decision along with it.
An invited collaborator could read an issue's share token directly through their own session, because that column is gated in the application layer rather than at the database level.