NAI OS

Running GSD on a Web Application

Fourteen findings on coding, code management, and deployment — from actually building and shipping one real app with OpenGSD, not from a demo.

nai-analysis is a Next.js + Supabase app with Row Level Security, checked-in database migrations, Google OAuth, and a live Vercel deployment, built phase by phase with OpenGSD's discuss→plan→execute→verify workflow. It also has real stakes: the content it holds is client-confidential, and its own CLAUDE.md says so in as many words — "security of access is not a feature here, it's the constraint everything else is built inside of." The findings below are scoped deliberately narrow: nothing about agent design in the abstract, only what changed once GSD had to build, manage, and ship this codebase.

  1. Go big or go quick
  2. One-liner vs. CIO mode vs. chat frequency, with or without auto-approve
  3. The session token window only goes so far
  4. Claude Code local vs. cloud vs. dispatch
  5. Every nth iteration surfaces a legacy flaw
  6. Iterate, test, iterate, test, iterate, test…
  7. Feature known in advance vs. discovered on the go
  8. To branch or not to branch
  9. Document on merge vs. on release
  10. Monolith vs. separate vs. linked repositories
  11. Blind spots in Vercel and Supabase administration
  12. Secrets in the repository, and secrets in the chat
  13. To look into the code vs. to trust it
  14. Own your architecture

01Go big or go quick

The same codebase runs both speeds, and the split is clean once you look for it: access control went big on purpose — Postgres Row Level Security plus SECURITY DEFINER RPCs as the sole enforcement point, never an app-layer check, because the failure mode of getting it wrong is a client-confidentiality breach, not a bug report. UI work goes quick by design in the same repo — a GSD "quick task" pattern (quick-260727-ao0, quick-260727-bdg) shipped a site-header logo and a copyright line same-session, no phase, no plan file. The tell for which speed a piece of work needs isn't its size, it's what a wrong version of it costs: a wrong RLS policy is a live security incident; a wrong header height is a follow-up commit.

02One-liner vs. CIO mode vs. chat frequency, with or without auto-approve

nai-analysis/.claude/CLAUDE.md takes this decision out of the moment entirely: "Before using Edit, Write, or other file-changing tools, start work through a GSD command… Do not make direct repo edits outside a GSD workflow unless the user explicitly asks to bypass it." That's CIO-mode-by-policy, structurally forced rather than left to how attentive a given session feels. In practice the two speeds from finding 1 get different postures anyway: the RLS migrations went through full phase gates with an explicit human verify step at each one; the Radix pointer-events fix and the card click-through fix (both same-day UI bugs) got fixed, checked, and committed inline, closer to one-liner. The dial that actually matters is per-change reversibility, not a fixed session-wide setting.

03The session token window only goes so far

This project's own .planning/ directory is the proof this matters: PROJECT.md, REQUIREMENTS.md, ROADMAP.md, STATE.md, and a phases/ directory with a SUMMARY.md per completed phase (phase 02-01's Google-auth-and-owner-RLS work, for instance, ended with its own SUMMARY.md and a STATE.md update). None of that depends on the chat that produced it still being in context. A phase started on one day and resumed days later — exactly what happened moving from Phase 1's RLS migrations to Phase 3's role-model RPC work — picks back up from those files, not from scrollback. If a phase's state only exists in the conversation that built it, it doesn't survive the first context reset.

04Claude Code local vs. cloud vs. dispatch

This app's build was inherently local-bound work, and knowing why matters more than the general rule: verifying a Google OAuth round-trip, a live Supabase session cookie refresh in proxy.ts, or RLS isolation between two real accounts all need a running local dev server, a real browser, and the project's own .env.local — none of which a stateless cloud run has, and none of which is worth reconstructing for a single check. Where dispatch earned its keep instead was research and drafting that didn't touch the running app at all — ecosystem research for the original 4-phase roadmap, or generating the doc suite (README.md, ARCHITECTURE.md, and friends) from the finished code. Pick local when the task needs to prove something against a live, authenticated system; dispatch when it doesn't need the system running at all.

05Every nth iteration surfaces a legacy flaw like a missing DENY for another subproject

The clearest example here isn't even a guardrail gap, it's a migration: 20260726190000_ revert_creation_restriction_add_admin_visibility.sql exists specifically to walk back an owner-only issue-creation restriction that an earlier migration had shipped, once a later iteration found it was too restrictive in practice. The workspace-level guardrail gap is the same shape at a different layer: the shared repo's Stop hook had DENY patterns written before nai-analysis/ existed as a subdirectory, so its .env.local and Supabase keys weren't automatically covered by a guard scoped to the old tree until a later pass added it. Both are the same finding: a rule written against the codebase's shape at time T doesn't know about anything added after T, and someone has to notice.

06Iterate, test, iterate, test, iterate, test…

Eight real migrations shipped in three days, each one gated by its own test pass rather than one final check at the end: Phase 1 shipped 3 migrations with 8/8 tests verified before Phase 2 was allowed to start; Phase 3's role-model RPC and status-filter work added 16/16 more; RLS isolation between two real accounts got re-verified after the admin-visibility revert, not assumed to still hold just because it held before the schema changed. The pattern that makes this affordable instead of exhausting is scope: each round tests the migration that round shipped, not the whole schema from scratch, so the loop stays cheap enough to actually run every time instead of being skipped under deadline pressure.

07Feature known in advance vs. discovered on the go

The access-control core was knowable up front and planned that way — 17 of 17 v1 requirements mapped into a 4-phase roadmap before Phase 1's first migration. The UI bugs weren't, and no amount of upfront planning would have surfaced them: a Radix pointer-events race that silently blocked navigation, and an issue card whose clickable area was too small, both only became visible once there was a real UI to click through on a real screen. The finding isn't that planning failed — it's that "known in advance" and "found by using the thing" are different discovery modes for different categories of defect, and a workflow that only budgets time for the first one will ship the second straight to users.

08To branch or not to branch

Every feature commit for this app — migrations, RLS policies, UI fixes, the admin-visibility revert included — lands straight on main, auto-committed turn by turn, with no feature branch in between. That's a deliberate bet, not an oversight: it works because every migration is forward-only and reviewed before it's written (never edited after landing), and because the revert migration shows the actual safety net is "ship a fix-forward migration," not "never let a bad one reach main." The bet only holds because Vercel's preview deployments exist as the de facto staging step; a schema change with no reviewable preview and no easy forward-fix would need the branch that this workflow currently skips.

09Document on merge vs. on release

Documentation here rides along with the commit that needs it, not a release cut: the "quick task" pattern pairs a feat(quick-…) commit with its own docs(quick-…) commit in the same breath, and a stale claim gets fixed the same way — docs(fix): correct shadcn dependency claim in DEVELOPMENT.md is a real commit in this repo's history, catching a doc that had drifted from what the code actually does. The generated suite (README.md, ARCHITECTURE.md, DEVELOPMENT.md, each tagged <!-- generated-by: gsd-doc-writer -->) can be regenerated against the live code whenever it's suspected of drifting; nothing here waits for a release milestone to find out it was wrong.

10Monolith vs. separate vs. linked repositories

nai-analysis is a git-tracked subdirectory of a larger workspace repo, not a submodule and not its own repo — 156 files with its own package.json, .planning/, and .claude/CLAUDE.md, sharing one clone and one Stop hook with everything else in the parent workspace. The cost of that arrangement is concrete and already named in this app's own tooling: a root-level .vercelignore exists specifically to scope any root-directory Vercel build down to "each project's own directory (docs/ or nai-analysis/)" — a backstop that would not be needed at all if this app lived in its own repo with its own root. Sharing a root buys one clone and one set of conventions; it charges rent in the form of exactly this kind of scoping guard, forever.

11Blind spots in Vercel and Supabase administration

The .vercelignore backstop above exists because the thing it's guarding against — which Vercel project has Root Directory set to what, whether it's git-connected at all — lives exclusively in a dashboard, invisible to the repo and to anything reading it: "there is no API for this toggle, so it can't be done from Claude, it's a manual click." The same blind spot exists on the Supabase side for this app specifically: every RLS policy and RPC here is a checked-in migration by design, precisely so the schema's security posture is not only readable from a dashboard — but that guarantee only holds as long as nobody edits a policy by hand in the SQL editor instead of writing a migration for it. The discipline has to be enforced by habit, because nothing in the repo can detect a dashboard-only change after the fact.

12Secrets in the repository, and secrets in the chat

This app's own .gitignore states its reasoning inline, written before the risk even existed: "Safety net ahead of the Phase 2 Next.js scaffold … the app's real Supabase keys land in .env.local — make sure no env file with values can ever be committed, even before the scaffold writes its own .gitignore." That's the guard done right: proactive, in place before the secret-bearing file existed. The remaining surface it can't cover is conversational — an agent reading .env.local to debug an auth failure, or quoting a Supabase error response containing a key back into chat, leaks the same secret somewhere a .gitignore can't reach. The habit has to extend past what gets committed to what gets said out loud in the session.

13To look into the code (effort, architecture) vs. to trust it (speed, lock-in)

The RLS-as-sole-enforcement-point design is itself a bet on looking, not trusting: "an app-layer bug leaks data; a database-layer (RLS) bug still requires bypassing Postgres itself," as this project's own stack notes put it — a deliberate choice to put the security boundary somewhere it has to be read and reasoned about as SQL, not somewhere a fast, unexamined app-layer change could quietly weaken it. The GSD-workflow-enforcement rule in this app's CLAUDE.md is the same bet at the process level: paying the plan-and-verify tax on every change rather than trusting a quick direct edit. Not paying that tax is exactly how a wrong RLS policy or a silently-broken invariant would survive unnoticed until it's exploited, not just until it's inconvenient.

14Own your architecture

Every finding above comes back to one line already sitting in this app's own CLAUDE.md: "security of access is not a feature here, it's the constraint everything else is built inside of." GSD can propose a migration, draft an RLS policy, or scaffold a Next.js route group — but only the person who has to answer for a data leak, a duplicated Vercel project, or a client finding their confidential issue exposed knows which of those constraints is actually non-negotiable. Coding, code management, and deployment can all be delegated a long way. The constraint that says what "correct" even means here can't be, and this project's own written policy is the proof it was never meant to be.

NextWhen the name is the only handle