nexus-os

NEXUS OS — Q. Development Phases · R. Risks · S. Build Order · T. Decision Log


Q. Development phases

I reordered the brief’s 24 phases in three ways, each for a concrete reason:

  1. A new Phase 02 “Platform kernel” comes before authentication. Errors, env config, logging, the action wrapper, the event outbox and the job runner are what every later phase plugs into. Retrofitting them after ten modules exist is the single most expensive mistake this project could make.
  2. Design system moves before the dashboard (it was Phase 05 → now 06, ahead of any application screen). Building the Command Center first would set the visual language by accident.
  3. Testing is not a phase. It runs inside every phase, with a dedicated hardening phase at the end for the cross-cutting suites (isolation matrix, authorization matrix, E2E). A phase is not “done” without its tests.
# Phase Ships Done when
01 Foundation Next.js 15 + TS strict, Tailwind v4, ESLint/boundaries, Prettier, Vitest, Playwright, CI, Prisma + Neon, docs/, .env.example CI green on an empty app; prisma migrate runs
02 Platform kernel env, AppError, logger, getDb/org extension, createAction, rate limiter, cache, outbox + dispatcher, job queue + cron drain, audit writer a demo action writes an entity + audit + outbox event in one TX and a job consumes it
03 Authentication Auth.js v5, credentials + OAuth, verification, password reset, MFA, session records full auth E2E passes; sessions revocable
04 Multi-tenancy Organization, Membership, invitations, org switcher, requireCtx, [orgSlug] routing, isolation test harness isolation matrix passes for the models that exist
05 RBAC permission catalogue, roles seed, can/require, scope builders, role management UI, permission snapshot authorization matrix passes for all 7 roles
06 Design system tokens (light/dark), themed shadcn, AppShell, sidebar, command palette, DataTable, forms, charts wrapper, empty/error/skeleton states Storybook-style kitchen-sink page; a11y check passes
07 Projects projects, members, milestones, health scoring, detail shell, activity first real module end-to-end with tests
08 Tasks tasks, kanban + dnd-kit, dependencies, checklists, comments, mentions, My Work board persists, dependency cycles rejected
09 CRM companies, contacts, leads, pipelines, deals, stage history, activities, custom fields, tags, deal→project conversion pipeline board + conversion flow works
10 People employees, teams, departments, skills, workload view, sensitive-field permissions workload chart from real task data
11 Finance invoices + items + numbering, payments, expenses, budgets, tax, PDF invoice, finance analytics money math unit suite passes; totals never client-supplied
12 Documents folders, upload via presigned URL, versions, preview, permissions, polymorphic attachments unauthorized download returns 404 in E2E
13 Communication notifications + preferences, activity feeds, mentions, email delivery, digests notification fan-out driven by outbox events
14 Search + Command Center SearchDocument indexer, global search, widget registry, dashboard layout, alerts search respects permissions; dashboard is configurable
15 Workflow engine (core) registries, graph validation, versions, runner, steps, retries, approvals, delays, run inspector the §L.4 example workflow runs end to end
16 Workflow builder (UI) visual canvas, node config forms generated from registries, condition builder, test run, templates a non-developer can build the example workflow
17 AI foundation provider abstraction, model router, cost metering, redaction, tool framework, read tools, streaming assistant AI cannot access another org’s data (tested)
18 AI agents agent registry, runtime loop, memory, write tools with confirmation, the 7 agents, run transparency UI agent permissions = user permissions ∩ scope (tested)
19 AI intelligence insight engine, inline actions, meeting pipeline, AI workflow actions, pgvector retrieval insights cite their source records
20 Analytics + reporting metric engine, nightly rollups, charts, filters, drill-down, report templates, PDF/CSV export, scheduling executive report generated from real data
21 Security center + audit sessions UI, login history, audit viewer + export, API keys, security alerts, Postgres RLS, org security policies RLS active; penetration checklist complete
22 Billing plans, Stripe checkout + portal + webhooks, entitlements service, usage metering, limit enforcement, upgrade prompts limits enforced server-side; webhooks idempotent
23 Hardening full isolation + authorization matrices, E2E journeys, load test on the top queries, EXPLAIN assertions, N+1 sweep, bundle budget, a11y audit performance and security gates green
24 Production launch Vercel prod, migrations, monitoring, alerts, backups + restore drill, runbooks, demo tenant seed staging → production deploy documented and rehearsed
25 Presentation README with screenshots + architecture diagrams, ADR set, demo video, live demo with seeded data, clean commit history a reviewer can understand the system in 5 minutes

Realistic effort at senior pace: phases 01–06 ≈ 3 weeks, 07–14 ≈ 6 weeks, 15–19 ≈ 5 weeks, 20–25 ≈ 4 weeks. ≈ 18 weeks full-time. If the timeline must compress, cut scope inside modules (fewer CRM fields, no custom fields, one pipeline) — never cut phases 02, 04, 05, 21.


R. Technical risks

# Risk Likelihood / Impact Mitigation
R1 Cross-tenant leak — one forgotten where clause Med / Critical 8 enforcement layers; Prisma extension makes the safe path the default; generated isolation matrix in CI; RLS in Phase 21
R2 Serverless timeouts on workflows and agent loops High / High step-level checkpointing + re-enqueue; wall-clock budget per job; maxDuration tuned; adapter ready for Inngest if needed
R3 Postgres connection exhaustion from serverless fan-out High / High Neon serverless driver / PgBouncer transaction pooling; single Prisma instance per lambda; no long transactions
R4 AI cost blowout Med / High model routing (cheap model for bulk summarization), token budgets per org/user, context truncation, caching, hard stop + kill switch, cost dashboard
R5 Prompt injection via client-supplied content Med / High tools authorized independently of the model; untrusted content delimited and labeled; write tools require confirmation; injection regression tests
R6 Scope collapse — 19 modules is a lot High / High vertical slices: every phase ships a working module; a strict “definition of done”; demo-able at the end of each phase
R7 N+1 and slow list queries at realistic data volume High / Med repository-level include discipline, DataLoader-style batching where needed, seeded 100k-row dataset in staging, EXPLAIN assertions in CI
R8 Event-loop / infinite automation (workflow triggers itself) Med / High causation depth cap, self-trigger detection, per-org run quotas, circuit breaker per workflow
R9 Money correctness (rounding, multi-currency, tax) Med / High integer minor units, Decimal for rates, totals only computed server-side from items, FX rate captured at write time, exhaustive unit tests
R10 Migration mistakes in production Med / High expand→migrate→contract, PR preview branches, review for lock duration, backfills as jobs, PITR
R11 Stripe webhook races / duplicates Med / Med StripeEvent idempotency table, signature + timestamp verification, reconciliation cron
R12 Timezone / working-day correctness (deadlines, overdue, reports) Med / Med org timezone stored and used everywhere; all comparisons in UTC with explicit conversion; injected clock in tests
R13 Auth.js v5 gaps (MFA, org concepts are ours to build) Med / Med accepted trade-off; auth is isolated behind kernel/auth so a vendor swap is contained
R14 Design consistency drift across 19 modules Med / Med design system + page templates before module work; DataTable/PageShell reused everywhere; a UI review checklist
R15 Search scaling beyond Postgres FTS Low / Med SearchDocument is already an abstraction — swapping to Typesense/Meili is an indexer change, not a query-site change
R16 File storage abuse / malware Low / High size and type caps, magic-byte validation, scan before availability, per-plan storage quota, attachment disposition

S. What should be built first

Phase 01 + 02 in one push — the foundation and the kernel — then a “walking skeleton”.

Concretely, the first deliverable I recommend (before any business module):

  1. Repo + toolchain — Next.js 15/TS strict/Tailwind v4, ESLint with boundary rules, Prettier, Vitest, Playwright, GitHub Actions, .env.example, Zod-validated env.ts.
  2. Prisma schema for identity + tenancy only — User, Account, Session, Organization, Membership, Role, Permission, RolePermission, Invitation, AuditLog, OutboxEvent, Job — plus the migration and the permission/role seed.
  3. The kernel — AppError taxonomy, logger, getDb(ctx) with the org-scope extension, requireCtx(), can/require, createAction(), rate limiter, outbox writer, dispatcher and the cron-drained job runner.
  4. The walking skeleton — sign up → verify → create organization → invite a second user → accept → switch organizations → one trivial entity (e.g. a Note) that proves the whole spine: validated action → permission check → org-scoped write → audit row → outbox event → job consumed → notification → visible in the UI.
  5. The isolation + authorization test harnesses, wired into CI from day one so every later model is covered automatically.

Why this order: the walking skeleton is where multi-tenancy, RBAC, auditing and the event backbone are proven while the system is small enough to change. Every later module is then a repetition of a proven pattern. Building CRM or the dashboard first — the tempting, demo-able choice — would mean rewriting every module once the kernel arrives.

Immediately after that: Phase 06 (design system) → Phase 07 (Projects). Projects is the right first business module because it sits at the centre of the entity graph (clients, tasks, documents, finance all attach to it), so it forces the cross-module contracts to be designed correctly and early.


T. Decision log

# Decision Alternatives rejected Reason
D1 Single Next.js app, module boundaries enforced by lint Turborepo monorepo one deployable, no packaging ceremony; extraction stays mechanical
D2 Auth.js v5 + DB sessions Clerk/WorkOS; hand-rolled the org model is the product and must stay ours; sessions must be revocable
D3 Shared DB + row-level tenant key schema-per-tenant; DB-per-tenant migration and cost sanity; layered enforcement recovers the safety
D4 Org in the URL path subdomain; cookie-only simpler cookies/CSP; a stale cookie showing the wrong tenant is a dangerous bug class
D5 Prisma $extends org scoping manual where discipline makes the safe path the default instead of a thing to remember
D6 Transactional outbox + cron-drained job queue direct in-request side effects; external queue from day one transactional guarantees, no extra vendor, adapter keeps the upgrade path open
D7 Server Actions + RSC over tRPC/REST for first-party traffic tRPC fewer layers, no duplicated DTOs; public REST still exists for integrations
D8 Own provider interface wrapping the AI SDK use the AI SDK directly one seam for cost, redaction, audit, routing and provider swap
D9 AI acts only through authorized tools direct DB/RAG access for the model the permission guarantee is otherwise unenforceable
D10 Money as integer minor units Decimal columns; floats exactness and unambiguous arithmetic across currencies
D11 Workflow graph as a versioned validated DAG in JSON normalized trigger/condition/action tables matches the visual builder, versions cleanly, still fully validated by Zod + a graph validator
D12 RLS deferred to Phase 21 RLS from day one it is defense-in-depth, not the primary control; adding it early would slow every migration before the model stabilizes

U. Implementation deviations from the specification

Recorded as they are discovered. The specification above was written against the versions current at design time; these are the corrections found when the foundation was actually built and verified (Phase 01).

# Spec said Reality Resolution
U1 Next.js 15 16.3.6 is current stable Use 16. App Router, RSC and Server Actions are unchanged; next lint is gone, so ESLint runs as its own script, and the eslint key is no longer valid in next.config.ts.
U2 Prisma 6 7.10.0 stable (the latest tag points at an 8.0 release candidate) Use 7.10.0 stable. Two breaking changes matter: connection URLs are no longer allowed in the schema’s datasource block — the CLI reads them from prisma.config.ts, and the runtime requires a driver adapter passed to PrismaClient. Phase 02 therefore adds @prisma/adapter-pg (or the Neon adapter) when lib/db.ts is written. Prisma also no longer loads .env automatically, so prisma.config.ts does it explicitly.
U3 Zod 3 Zod 4 stable Use 4. z.url() replaces z.string().url(); issue types moved to z.core.$ZodIssue.
U4 TypeScript “strict” (implicitly latest) TS 7 is stable but typescript-eslint@8 declares typescript <6.1.0 Pin TypeScript 5.9.3. Revisit when typescript-eslint supports the native compiler.
U5 ESLint 9 flat config ESLint 10 is current, but eslint-plugin-react@7.37.5 (a dependency of eslint-config-next) supports only <=9.x and crashes on 10 Pin ESLint 9.39.5. Revisit when eslint-plugin-react ships ESLint 10 support.
U6 eslint-plugin-boundaries with element-types rules v7 replaced that rule with boundaries/dependencies + policies; elements classify folders and the new boundaries/files setting classifies layers Config rewritten to the v7 model. The intra-module layering (transport / service / repository / db) is expressed as file categories. Verified by deliberately introducing violations and confirming the rule reports them.
U7 Import resolution for boundary checks eslint-import-resolver-typescript@4 uses the new resolver interface that eslint-module-utils cannot load Pin the resolver to v3.10.1 (legacy interface, interfaceVersion: 2).
U8 — npm 11 blocks dependency install scripts by default, so Prisma’s engines are not downloaded on a plain npm install allowScripts entries are committed in package.json; the README documents npm approve-scripts + npm rebuild for first-time setup.

Phase 02 deviations

# Spec said Reality Resolution
U9 §G.1 Auth.js v5 (NextAuth) with the Prisma adapter and database sessions The two are mutually exclusive. Auth.js core throws UnsupportedStrategy("Signing in with credentials only supported if JWT strategy is enabled") — email + password forces stateless JWTs. Auth.js v5 is also still a beta (5.0.0-beta.32) after a long run. §G.3 wins over §G.1: revocable server-side sessions are a product requirement (device list, instant revocation, Security Center), while the library was an implementation choice. Phase 02 implements a first-party session layer — 256-bit opaque token in an httpOnly cookie, stored only as a SHA-256 digest, checked against the database on every request. OAuth can be added later on top of the same Session table.
U10 §E.1 Session.sessionToken @unique Storing the token verbatim means a database leak is directly replayable as a live session for every signed-in user. Store tokenHash (SHA-256) instead. The cookie holds the only copy of the secret. Same change applied to VerificationToken.
U11 §G.2 password strength via zxcvbn zxcvbn is a ~800 KB dependency for a check that is mostly length. Implemented the dependency-free part: a 12-character floor, a 128-character ceiling (an unbounded body is a cheap hashing DoS), a known-bad list, a distinct-character rule, and a rule that the password may not contain the email local part. A breach-corpus check (k-anonymity range query) remains open and is the part worth adding.
U12 §G.2 TOTP MFA, OAuth providers, magic links Not required by the Phase 02 scope, and half an MFA implementation is worse than none. Deferred with their models (Account, MFA columns) left out of the schema rather than added unused. Both are additive migrations when their phase arrives.
U13 §G.5 rate limits via Upstash Redis Redis is not provisioned, and sign-in throttling cannot wait for it. Login throttling counts recent LoginEvent rows — a table the security log needs anyway. It works across serverless instances with no extra service. The general-purpose Redis limiter still belongs to the kernel phase.
U14 Next.js middleware.ts convention Next.js 16 deprecates it in favour of proxy.ts exporting proxy. Migrated. Behaviour is unchanged: cookie-presence check only, never an authorization decision.
U15 Email delivery via Resend No API key, and an untested provider adapter would be fake production functionality. Shipped the Mailer port with a development transport that prints the message. The verification and reset flows are complete and covered by integration tests; only the transport is local. The Resend adapter is one file when the key exists.

Phase 12 deviations (Documents)

# Spec said Reality Resolution
U16 §N “short-TTL presigned URLs issued after a check” A presigned URL is a bearer token for a file: once issued it works for anyone holding it, for its whole lifetime, with no further authorization check. It also cannot exist at all for the development filesystem driver. Bytes are streamed through GET /api/orgs/[orgSlug]/documents/[documentId]/download, which re-checks permission on every request and serves with Content-Disposition: attachment, nosniff and no-store. The driver interface deliberately offers no URL-minting method, so no future caller can reintroduce one by accident. Presigning stays available as a change to the S3 driver alone.
U17 §N “malware scan before availability” No scanner is provisioned, and reporting CLEAN without one would be a lie recorded in the database. Uploads are recorded as SKIPPED with the UI stating “type and signature verified; no malware scanner configured”. The three checks that do run are real: an allowlist of types, a declared-type/extension match, and a magic-byte check on the actual leading bytes. INFECTED and PENDING are refused by the download path today, so wiring a scanner is a change to one write, not to the flow.
U18 §P.4 STORAGE_PRESIGN_TTL_SECONDS Not applicable once presigning is not used. Replaced by STORAGE_DRIVER, STORAGE_LOCAL_DIR and STORAGE_MAX_UPLOAD_BYTES. The S3_* variables are unchanged and are required only when STORAGE_DRIVER=s3.
U19 — The S3 driver is written (SigV4 signed with node:crypto, no vendor SDK) but has not been run against a live bucket, because no credentials were supplied. The local filesystem driver is the one exercised by the tests. Documented rather than claimed. The local driver is correct for development and is used by the integration and E2E suites; a production deployment on a serverless host must set STORAGE_DRIVER=s3 and verify it against its own bucket before go-live. Recorded again in the production-readiness limitations.
U20 §E.1.6 @@unique([organizationId, parentId, name]) Postgres treats NULLs as distinct, so two root folders could share a name. Prisma has no syntax for a partial unique index. Added Folder_organizationId_name_root_key as raw SQL in migration 20260929071000_folder_root_unique, noted in documents.prisma so a regenerated migration does not silently drop it, and asserted by an integration test.
U21 §G.4 proxy redirects a signed-out visitor Redirecting an /api/* request to the HTML sign-in page makes a fetch report 200 for a request that actually failed, because the redirect is followed transparently. The proxy now answers /api/* with a 401 JSON body instead of a redirect. Caught by the Phase 12 E2E suite, which expected 401 and observed 200.

Phase 13 deviations (Communication)

# Spec said Reality Resolution
U22 §L a job runner drains the outbox on a schedule No job runner and no scheduler in this environment. Events drain twice over: after() runs the drain once the response is flushed, so nobody waits on somebody else’s notification, and POST /api/cron/outbox is the safety net for retries and for anything emitted outside a request. The endpoint is authenticated by CRON_SECRET compared in constant time, and refuses everything when it is unset.
U23 §E.1.11 Notification.channels String[] Kept, and it records what was actually delivered rather than what was requested — otherwise a row claims an email that failed to send. Channels are written from the resolved plan, and an email failure is logged per recipient without failing the others or the in-app notification.
U24 Digest delivery A digest needs a scheduler to batch a day of notifications into one email, which is the same missing piece as U22. The preference is stored and shown, and the settings page says plainly that digests are not yet sent. Recording a preference the system will honour later is honest; silently dropping it would not be.
U25 — A subscriber module registers itself on import, and an ES module is imported once — so a test that cleared the registry could not get it back, and two tests were passing by asserting the absence of a notification that had never been wired up. Each subscriber is exported as a value as well as registered on import, so a test re-registers it explicitly. Both tests then failed and were fixed. Worth recording because the failure mode — a green test asserting nothing — is invisible.

Phase 14 deviations (Workflow engine)

# Spec said Reality Resolution
U26 §L.3 a job runner claims runs with FOR UPDATE SKIP LOCKED No job runner exists (the same gap as U22). Runs execute inline after the event that started them, and POST /api/cron/outbox sweeps QUEUED runs and wakes runs whose delay or approval deadline has passed. The interpreter is written to be resumed cold from WorkflowRunStep rows, so swapping in a real job runner changes who calls executeRun, not the engine.
U27 §L.2 a visual builder canvas generated from the registries A drag-and-drop canvas is a large amount of UI whose only output is the same JSON document, and a half-working canvas would make the engine look less capable than it is. The editor is a validated JSON document with the registries rendered beside it, generated from the same definitions a canvas would use. The validation — which is the part that actually protects an author — runs on the server and is identical either way. The canvas is a UI change with no engine impact.
U28 §L.2 ai.generate / ai.analyze actions No AI provider is configured yet; those actions belong to the AI phases. Four actions ship now (notification.send, task.create, activity.log, email.send), each declaring its permissions, idempotency and side-effect class. The AI actions are registry entries added by their own phase, with no engine change.
U29 §L.1 branch and loop node types A loop node is the one construct that can make a bounded interpreter unbounded, and neither is needed by the §L.4 example. Branching is expressed as labelled edges out of an approval (approved / rejected / timeout), which covers the example and keeps the graph acyclic by construction. loop is deliberately absent; the validator rejects any cycle.
U30 — Integration suites share one database, and drainOutbox() swept every tenant’s events — so a suite could consume another suite’s events and both would fail intermittently. Found when the workflow suite passed alone and failed in the full run. drainOutbox takes an optional organizationId. Production still drains everything; each suite drains only its own tenant. The bug was in the tests, but the fix belongs in the code, because “works alone, fails together” is the least debuggable failure a suite can have.

Phase 15 deviations (AI foundation and Intelligence Center)

# Spec said Reality Resolution
U31 §J.2 the provider adapter uses the Vercel AI SDK internally The Messages API transport is about eighty lines of fetch. An SDK here would put vendor types one import from the application — the exact thing the abstraction exists to prevent — and add a dependency that has to be kept current. The adapter is written against fetch with our own message types. Translation in both directions and the SSE parser are unit-tested from recorded shapes. It has not been run against the live API, because no key was available; that is stated in the file and here rather than implied to be verified.
U32 §J.6 the insight engine runs bounded analyses Kept, and made stricter: every insight is COMPUTED, never generated. Five analyses (overdue invoices, stalled deals, at-risk projects, expense backlog, workload imbalance) run in SQL, decide their own severity, and cite the rows they came from. They work with no provider configured. A model may later phrase a finding; the numbers are never the model’s.
U33 §J.3 pgvector retrieval with per-record re-checking pgvector is not installed, and embeddings need a provider this deployment does not have. Retrieval is tool-based: the model asks a tool, the tool calls the same query function the page calls, and authorization is the service’s. That satisfies the hard requirement (nothing unauthorized reaches a prompt) without an index that could express less than the permission model. Semantic search is additive when embeddings exist.
U34 §J.5 write tools with confirmation Write tools belong with the agent runtime in the next phase, and a write tool with no confirmation UI is a footgun. The registry supports sideEffect: 'write' and refuses one in any session that has not opted in; nine read tools ship. The refusal path is tested.
U35 — Test parallelism grew past what one embedded Postgres could serve: 34 worker processes, each with its own pool, produced hook timeouts in whichever suites started last — a failure that looks like a bug in the code under test. Workers capped at six in vitest.config.mts, with the reason written next to the setting. Also runQueued was sweeping every tenant’s workflow runs; the subscriber now executes exactly the runs it created, which is both correct in production and hermetic in tests.

Phase 16 deviations (AI agents)

# Spec said Reality Resolution
U36 §K seven agents Five ship. The two missing (Research, Meeting) need capabilities this build does not have — web access and a transcription pipeline — and an agent that cannot do its job is worse than an absent one. Project Health, Receivables, Pipeline, Workload and Daily Briefing, each mapped to an existing permission in the catalogue. Adding another is one registry entry.
U37 §J.5 write tools gated by confirmation in interactive sessions Kept, and made the default rather than a mode: an agent PROPOSES and a person DISPOSES. A proposal is a row; nothing happens until it is accepted, and accepting runs it with the ACCEPTING person’s permissions, not the agent owner’s. AUTONOMOUS exists and is opt-in per agent, set by someone with ai.settings.manage. Idempotency is a hash of (run, tool, args), and accepting is a conditional update so two people pressing Accept cannot both execute it.
U38 §K agent system prompts configurable per organization An editable system prompt is an editable security boundary: whoever can edit it can ask the agent to disregard its scope. The prompt is code. An organization may APPEND instructions, never replace them, and appended text cannot widen the tool allowlist or the permission ceiling.
U39 — A real bug the tests caught: the agent tool allowlist filtered only what was OFFERED to the model. A model naming a tool outside its list would have had it executed, because the registry checks permissions but not agent scope. The allowlist is enforced at invocation, not only in the offer. The test that found it asserts a Workload agent asking for createTask creates nothing.
U40 — A second bug: step positions were read from a shared counter after an await inside Promise.all, so two concurrent tool calls claimed the same position and the unique constraint rejected the run. The position is captured into a local before the await. The constraint did its job — this is why it exists.
U41 — The development database was created with the host locale’s WIN1252 encoding, so storing any character outside it — an accented name, a curly quote, an em dash — failed. In development only, which is the worst place for a difference to hide. npm run db:start creates the database with ENCODING 'UTF8' TEMPLATE template0 and warns if an existing one is not UTF-8.
U42 — The Phase 15 worker cap used poolOptions, which Vitest 4 removed — it was ignored silently and the suite was still fully parallel. Every integration suite also re-seeded the 109-permission catalogue in its own beforeAll. Top-level maxWorkers, and a globalSetup that seeds authorization exactly once before any suite. Both are recorded because a setting that is silently ignored is indistinguishable from one that works until it does not.

Phase 21 deviations (Testing)

# Spec said Reality Resolution
U43 — A real product defect the journey test found: the create forms for companies, contacts, leads, deals, projects and tasks returned the new record’s id and then stayed on the empty form. The fields were still filled and the button still worked, so a second click made a second record. Invoices, workflows and reports already navigated, each with its own copy of the same useEffect. One useCreatedRedirect(state, basePath) hook in src/components/forms/, used by all nine. The three hand-rolled copies were folded into it. An edit action returns no id, so an edit form using the same component still stays put.
U44 — The first draft of the journey test waited for waitForURL(/\/crm\/companies/) after submitting. That pattern also matches /crm/companies/new — the page the click was made on — so it resolved instantly and the test raced the server action. It passed for projects and failed for companies purely on timing. The test waits for a URL ending in a record id. A wait that can be satisfied by the state the test started in is not a wait.
U45 — A vacuous pass found while raising coverage: the API-key ceiling test used a Manager, who cannot create keys at all. It asserted FORBIDDEN and got it from the wrong check — the permission gate, not the ceiling. Split in two: a Manager is refused by the gate, and an Admin — who may manage keys — is refused when granting organization.delete, with an explicit assertion that the Admin holds organization.apikey.manage so the test cannot pass for the earlier reason again.
U46 §23 “performance and security gates green” implies a coverage number Coverage of everything under src/kernel, src/lib and src/modules is 50% statements, and that figure is honest rather than flattering: Vitest does not instrument the Next.js server, so pages, components and Server Actions count as uncovered even though 43 Playwright journeys exercise them. React components are excluded from the coverage scope with the reason recorded in vitest.config.mts; Server Actions are NOT excluded, so the figure still carries their weight. The thin areas are named in the final report rather than hidden by a narrower scope.

Phase 22 deviations (Performance)

# Spec said Reality Resolution
U47 §23 “N+1 sweep” A sweep is a one-off; the defect comes back. Reading the code for N+1s also finds only the ones you think to look for. measureQueries() in src/lib/db.ts counts scoped queries around a block, and query-budget.integration.test.ts loads every list screen at 5 rows and at 200 and requires the count to be identical. The counts are pinned as exact numbers (2 for most lists, 4 for documents) rather than upper bounds, because a bound of twenty is satisfied by a screen that used to take three. The sweep found no N+1 — which is a result worth having recorded rather than assumed.
U48 §23 “EXPLAIN assertions in CI” An EXPLAIN assertion at test-data volume proves nothing: Postgres correctly prefers a sequential scan over 200 rows, so the assertion would either fail on a well-indexed table or need enable_seqscan=off, at which point it is testing that an index exists. The structural property is tested directly, from pg_index: every table in TENANT_MODELS has an index whose first column is organizationId, and the seven hottest list filters have an index whose leading columns are exactly the columns they filter on. All 66 tenant tables already passed; the check exists so the 67th cannot ship without one.
U49 — A slow leak: getDb() cached one scoped client per organization in an unbounded Map on globalThis. On a long-lived instance serving thousands of tenants that is not a cache. Bounded to 128 with least-recently-used eviction, using Map insertion order for recency. A miss costs one object construction — not a connection, since every client shares the pool.
U50 — A real, user-visible defect: there was no loading.tsx anywhere. Next renders nothing until a page’s server work completes, so every navigation looked frozen for as long as the widest read took — and the widest reads here are the Command Center (18 queries) and analytics (29). A generic loading shell for every route under an organization, plus the analytics page restructured so the title and period selector render before any query runs and the metrics stream in behind Suspense. The server work is unchanged; what changed is that the browser has something to show while it happens.
U51 §23 “bundle budget” Not previously enforced anywhere. npm run check:bundle measures gzipped bytes after a build — 127.8 KB shared by every route against a 160 KB budget, 515.6 KB of client JavaScript in total against 900 KB — and fails the build over budget. It runs in CI after npm run build.

Phase 23 deviations (Production readiness)

# Spec said Reality Resolution
U52 §N.1 a nonce-based CSP, “introduced in Phase 02” It never was. next.config.ts carried a comment promising it and the application shipped with no Content-Security-Policy at all — the one header that turns an injected <script> from a catastrophe into a console message. Minted per request in src/proxy.ts: 'strict-dynamic' with a nonce, no 'unsafe-inline' on scripts, 'unsafe-eval' in development only. style-src 'unsafe-inline' is kept and the reason is written down — React writes style attributes and no nonce reaches them, and an inline style cannot execute. An E2E test drives sign-up, organization creation and the analytics page while failing on any browser-reported violation, because a policy that breaks the product gets weakened within a day.
U53 — A production defect, found by writing the deployment notes: the proxy answered every /api/* request without a session cookie with 401. A Stripe webhook has no cookie. Neither does Vercel Cron, an uptime probe, or an API-key caller. Concretely: no subscription event could ever have been processed, and no scheduled drain could ever have run. Four prefixes are exempted from the cookie check and authenticate themselves instead — signature, constant-time secret, hashed key. Four E2E tests assert each endpoint is reached and refused by its own check rather than by the proxy.
U54 — Vercel Cron issues a GET; the drain route exported only POST. The cron entry would have 405’d on a five-minute schedule without anything appearing to be wrong. GET accepted, delegating to POST, secret still required. Recorded rather than quietly fixed because it is the kind of mistake that produces no error anywhere.
U55 §P.3 “structured pino logs” 18 console.error('[module] …', error) call sites. Readable in a terminal, unsearchable in an aggregator: you cannot alert on the third argument of a message beginning [billing]. src/kernel/observability/logger.ts — JSON lines in production, readable in development, errors serialised with their stack, and field names redacted through the same list the AI layer uses, because a log line is as much an exfiltration path as a prompt. No pino: one file of about eighty lines does the job, and a dependency here would be for the name.
U56 §P.3 health check covering “DB, Redis, storage and provider reachability” There is no Redis, and a health check that calls a payment provider makes an unauthenticated endpoint into a way to bill somebody else’s rate limit. GET /api/health runs SELECT 1 and returns 200 or 503 with no detail: no version, no hostname, no error text. Detail goes to the log, where the people who can act on it are looking.
U57 §M–P generally The documentation described the target and read as a description of the build. A new §Q in docs/OPERATIONS.md states what is present, what is not (no Sentry, no Redis, no rollups, no alerting, no restore drill, no RLS, no malware scanning, and two provider adapters never run against the live services), the exact Vercel steps, and which capability each optional environment variable turns on.

Phase 24 deviations (Final polish)

# Spec said Reality Resolution
U58 — The landing page was still the Phase 01 build-status page. The first thing any visitor saw was a checklist reading “Phase 02: Next, Phase 03: Planned” over a finished product, with no way to sign up from it. Replaced with a real front door: what the product is, the fourteen modules, and two ways in. Every claim on it names something that exists and can be used within a minute of signing up.
U59 §24 “demo tenant seed” Not built. An empty product is impossible to evaluate: the Command Center, analytics and the pipeline all read as broken when they are merely empty. npm run db:demo creates four people on four roles, three clients, two projects, ten tasks and four invoices in four states — by calling the same services the application calls, never inserting behind them. The invoice totals are the finance service’s arithmetic; the task numbers come from the atomic allocator. It refuses to run against production and refuses to run twice.
U60 §P.4 “fails the boot if anything required is missing” DATABASE_URL is optional in the Zod schema, and the comment beside it said Phase 02 would make it required. It never did — correctly, as it turns out. The comment now says what is true and why: next build opens no connection, so a schema that required a live database would make the application unbuildable in CI. getDatabaseUrl() throws at first use with a message naming the variable.