NAI OS

NAI Analysis — the one that isn't a slash command

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.

What an issue is

Decision

Options are on the table, and the point of the issue is that one of them eventually gets picked.

Discussion

Analysis or notes get captured — no option is ever picked. A record and a place to talk something through.

Type is fixed

Chosen at creation — Decision or Discussion — and never changed afterward, so pick carefully.

Who it's for

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.

The access model

Private by default

Nothing is reachable except through one of two paths, or an issue explicitly flagged for the public showcase.

The invite

Tied to one exact Google address — full participation: read, comment, and on a Decision, pick an option.

The read-only link

A hard-to-guess URL, no sign-in at all — view-only, no comments, no options, no controls.

Row Level Security

Enforcement lives in Postgres RLS, not application-layer checks — the single enforcement point for who sees what.

SECURITY DEFINER, narrow anon role

Both no-login paths are served by narrow SECURITY DEFINER database functions; the anonymous database role holds no direct table access of its own.

The screens

/

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.

/issues

The dashboard. Any signed-in account.

/issues/new

The new-issue form. Any signed-in account.

/issues/<id>

An issue's detail page. The owner/admin, plus that issue's invited client.

/issues/archive

The archive screen. Admin only.

/issues/shared

The share-link revoke/restore screen. Admin only.

/help

The full help page, six topics. Any signed-in account.

Working an issue

Markdown body

An issue's body renders as markdown, the same formatting used throughout the app.

Options and who can pick

On a Decision issue, the owner, the admin, and the invited client can each pick an option.

Decided badge

The winning option is badged Decided with a caption of who and when; every other option dims and loses its pick control.

Options stay editable

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.

Three statuses

Needs attention, Waiting, and Decided & closed — set manually from the menu, never inferred, independent of both the decision and the showcase flag.

Comment thread

A flat, oldest-first markdown thread open to the owner, admin, and any invited client. Comments cannot be edited or deleted by anyone.

The dashboard

Cards and ordering

Grouped under three status headings, most-recently-updated first, carrying Decision/Discussion, Showcased and Archived badges as they apply.

Search

Looks inside both the title and the full body content, not just the title.

Status pillsshipped 260805

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.

The menu

Status changes, the showcase toggle, and — admin only — Archive, all from one per-card menu.

Sharing, revoking, and the showcase

Invite composer

One Client email field with autocomplete from past invites; only one invite per issue, ever.

Wrong Google account

Signing in with the wrong address shows a dedicated mismatch screen with Sign out and try again.

What an anonymous visitor sees

Type badge, title, a read-only notice, and the body — no comments, options, status line, or controls.

Shared links screenshipped 260805

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.

Showcase cards

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.

Soft, reversible archiving

Hides the issue from everyone including the admin and switches off its share link and showcase card, while preserving everything underneath.

Guarded permanent delete

Filter the archive list down, then type DELETE exactly to enable Confirm delete — takes options, invites and comments with it, unrecoverable.

Help system

A context-sensitive slide-in panel matched to the current page, plus a full /help page carrying all six topics.

How it's built

%%{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
No orchestrator, no subagents — a normal web app's request path. What carries over from the agent systems is the boundary discipline: one narrow gate (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 / React

Next.js 16 App Router with React 19, hosted on Vercel.

Supabase + RLS

Postgres + Auth (Google sign-in only), with Row Level Security as the enforcement point for every access rule.

SQL migrations

Versioned under supabase/migrations/.

Tailwind + shadcn/ui

Tailwind v4 with shadcn/ui components.

TypeScript

Throughout.

Verification harness

A live cross-account access-control harness under verification/ that proves the access model against the real hosted database, not against mocks.

The seam to this workspace

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 email from this app

No notification email is sent by the app itself; a separate scheduled /nai-issue-check agent handles telling Udo about new client activity.

Agent write path

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.

Known limitations

No automatic import

Content arrives by paste-in today; the agent write path exists but nothing pulls automatically, and there's no UI for it.

No audit-log screen

Access events are recorded in the database but not yet surfaced anywhere to view.

Comments are permanent

Cannot be edited or deleted by anyone, including the person who posted them.

Title and content are permanent

Cannot be edited after creation — a correction belongs in a comment, not a re-edit.

No link rotation

A share link cannot be regenerated or rotated — only revoked and later restored to the exact same address.

One invite per issue

No re-inviting a different address, and no control to remove or re-address an existing invite.

No un-pick

The only way back to an undecided state is deleting the decided option, which clears the decision along with it.

Accepted risk

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.

NextNAI Onto