nexus-os

NEXUS OS — Final engineering report

Built by Muhammad Kashif — kashifarish2001@gmail.com

24 phases, a production-hardening pass (§25) and a completion pass (§26). Verified at the point of writing: npm run verify green (1,089 unit and integration tests across 51 files), npm run test:e2e green (54 Playwright journeys across 6 specs against a production build), npm run build green, npm run check:bundle inside budget.

Status: ready for a first staging deployment; not production-ready. §25.5 and §26.7 say why, and §26.5 lists every external integration that has never been contacted.

Where this report states a number, it was measured. Where something is missing, it says so.


1. All modules

Sixteen module folders under src/modules/, each with the same internal shape — actions.ts / queries.ts (transport), service.ts (rules and authorization), repository.ts (Prisma only), schema.ts (Zod), components/.

Module Ships
auth Registration, sign-in, email verification, password reset, throttling, account overview
organizations Creation, settings, members, invitations, role assignment, org switching
crm Companies, contacts, leads, pipelines, deals, activities, tags, lead conversion
projects Projects, members, milestones, budgets, derived health, portfolio
tasks Board, dependencies, checklists, comments, labels, assignment, time
people Profiles, teams, departments, skills, workload
finance Invoices, payments, expenses with approval, budgets, summaries
documents Upload, versions, folders, sharing, attachments, storage quota
notifications In-app and email delivery, preferences, digest, outbox drain
workflows Visual builder, engine, triggers, conditions, approvals, delays, runs
ai Provider port, gateway, Intelligence Center, insights, 13 tools, 5 agents
analytics 11 metrics, period comparison, series, drill-down
reports 4 templates, parameters, CSV and printable export, scheduling
security Sessions, login history, audit log, API keys, security policy
billing Plans, entitlements, checkout, portal, webhooks, usage
dashboard The Command Center as a widget registry — 17 widgets contributed by other modules

60 pages and 8 API routes. Adding a module touches no existing module: it registers its own widgets, its own workflow actions and its own AI tools.

2. Database architecture

PostgreSQL with Prisma 7 using driver adapters (@prisma/adapter-pg). 77 models, 46 enums, 19 migrations, split by domain across prisma/schema/ — identity, organization, authorization, crm, projects, tasks, people, finance, documents, events, workflows, ai, agents, analytics, reports, security, billing, base.

Design decisions that matter:

3. Authentication

Email and password with server-side sessions. Auth.js was specified and rejected; the reasoning is in docs/ROADMAP.md §U9.

Property Implementation
Password hashing argon2id, 19 MiB / t=2 / p=1, via @node-rs/argon2
Session token 256 bits of randomness in an httpOnly, SameSite=Lax, __Host- prefixed cookie
Token storage SHA-256 digest only — a database leak yields nothing replayable
Lifetime 30 days, sliding, refreshed at most hourly; revocable individually from the account page
Route protection requireUserPage() in the server layout. The edge proxy only redirects early
Throttling 5 failures per email and 20 per IP in 15 minutes, counted from LoginEvent rows
Enumeration Sign-in and password reset answer identically for known and unknown addresses
Reset tokens Single-use, 30-minute TTL; using one revokes every session

Not built: OAuth providers, MFA/TOTP, passkeys. The schema has room for them; no code pretends they exist.

4. Multi-tenancy

Isolation is structural rather than remembered. Eight layers, of which the decisive one is the fourth:

  1. The edge proxy redirects an obviously signed-out visitor — a convenience, not a control.
  2. requireCtxPage(orgSlug) in the organization layout resolves the slug to an ACTIVE membership for the signed-in user, and answers 404 for a non-member. Not 403: a 403 confirms the organization exists.
  3. Every service takes a Ctx carrying orgId, membershipId and resolved permissions.
  4. getDb(orgId) returns a Prisma client extension that injects organizationId into every query against a tenant model. Forgetting a where clause returns your own rows.
  5. A caller-supplied organizationId that does not match the context throws CrossTenantAccessError — it is not silently rewritten, because silently rewriting turns an attempted attack into a successful-looking request.
  6. findUnique is rewritten to findFirst so an id belonging to another tenant returns null rather than that tenant’s row.
  7. TENANT_MODELS completeness is enforced by a test that parses prisma/schema/*.prisma.
  8. A generated isolation matrix exercises read, count, update and delete against all 66 tenant models.

getSystemDb() is the unscoped escape hatch, restricted by lint to authentication, organization creation, invitation acceptance, webhooks and cron, and conspicuous where used.

Not built: Postgres row-level security. It is defence in depth on top of the above, not a replacement for it, and its absence is recorded in docs/OPERATIONS.md §Q.2.

5. RBAC

109 permissions in a code catalogue (src/kernel/authz/catalogue.ts), from which the Permission union type is derived — a typo is a compile error. 7 system roles: Owner, Admin, Manager, Finance Manager, HR Manager, Employee, Client. Custom roles per organization, plus per-resource grants (ResourcePolicy).

The rule that makes the whole thing safe to hand an external party: a denial beats any grant, including the owner short-circuit. The Client role denies twelve permissions explicitly, so a future edit that widens a grant cannot accidentally expose finance or people data to a customer.

resolveScope(any, own) collapses the common “all or only mine” pair into one call, so a repository filter cannot drift from the permission that authorised it.

A generated matrix (authorization-matrix.test.ts, 53 assertions) covers all 7 roles against all 109 permissions and asserts: every granted permission exists in the catalogue, the owner has everything not explicitly denied, Admin ⊇ Manager ⊇ Employee with no exceptions, and DENY beats ALLOW even for an owner.

UI checks are separate from server checks. ctx.can() in a component decides what to render; ctx.require() in a service decides what happens. Hiding a button is not authorization.

6. CRM

Companies, contacts, leads, pipelines with ordered stages, deals, activities and tags.

7. Projects

Projects carry members, milestones, a budget, a manager, and a visibility (ORGANIZATION / PRIVATE). A private project is invisible to a non-member rather than forbidden.

Health is derived, never stored. A score computed from schedule slip, milestone completion, overdue tasks and budget consumption, in one pure function (health.ts) with its own tests. A stored health field would go stale the moment anything else changed.

The portfolio view aggregates across projects the caller can see — which is the point of resolveScope: the summary and the list agree because they share one filter.

8. Tasks

A Kanban board with drag-and-drop, plus a table view. Dependencies, checklists, comments, labels, assignment, estimates and spent time.

9. People

Profiles, teams, departments, skills with proficiency, and a workload view computed from real task assignments and estimates.

Sensitive fields are a separate permission. people.profile.read.sensitive gates pay rates and compensation, and is held by HR Manager and Owner — explicitly not by Manager or Finance Manager. The authorization matrix asserts that.

10. Finance

Invoices, payments, expenses with an approval flow, and budgets.

11. Documents

Versioned files with folders, per-document sharing, attachment to any record, and a storage quota enforced before the bytes are written.

Not built: malware scanning. Documents record a scan status of SKIPPED rather than a fabricated clean result.

12. Notifications

In-app and email, with per-type preferences and a digest option.

The important part is the delivery mechanism: a transactional outbox. emitEvent(input, tx) writes the event in the same transaction as the change that caused it, so a notification can never be sent for a change that rolled back, and a change can never fail to notify because the mailer was down.

drainOutbox() delivers to subscribers independently, with per-subscriber OutboxDelivery idempotency and MAX_ATTEMPTS = 5. Draining happens after the request via after(), with the cron endpoint as the safety net for a process that died mid-drain.

13. Workflow engine

A JSON graph, interpreted — not generated code, not eval.

14. AI Intelligence Center

An assistant over the organization’s own data, plus generated insights.

With no ANTHROPIC_API_KEY configured the assistant says it is not configured. It does not produce a canned answer that looks like a real one.

15. AI agents

Five agents: Project Health, Receivables, Pipeline, Workload, Daily Briefing. Each maps to an existing permission in the catalogue.

The governing design is propose, then dispose:

16. Analytics

11 metrics in a registry, computed live from the organization’s own rows: revenue received, invoiced, expenses, tasks completed, tasks overdue, active projects, at-risk projects, deals won, pipeline value, win rate, active members.

Not built: nightly rollups. MetricSnapshot exists in the schema; nothing writes to it on a schedule.

17. Reporting

4 templates: executive summary, receivables ageing, project status, team workload.

A report is parameters, not a snapshot. Reopening one recomputes it from current data, which is the difference between a report and a screenshot. Export to CSV and to a printable HTML view. Scheduled reports run from the cron endpoint under a membership context.

18. Security

The Security Center: sessions, login history, an audit log, API keys, and organization security policy (session lifetime, IP allowlist, new-IP alerting).

19. Testing

Layer Count
Unit Pure logic: money, dependency graphs, conditions, health, periods, key handling
Integration Against real PostgreSQL, per module
Generated matrices Authorization (7 × 109), tenant isolation (66 models), index coverage, tenant registry
Query budgets Every list screen at 5 rows and at 200, requiring an identical query count
End-to-end 53 Playwright journeys against a production build
Total 1,087 unit and integration tests in 51 files; 53 E2E in 6 specs

Coverage is 50% of statements across kernel, lib and modules. That figure includes Server Actions, which Vitest does not instrument and which the E2E suite covers instead; React components are excluded with the reason recorded in vitest.config.mts. It is reported rather than improved by narrowing what it counts.

The tests found real defects, which is the argument for having written them this way rather than as confirmations: the proxy returning HTML to API callers, the agent tool allowlist not being enforced at invocation, a step-position race inside Promise.all, a development database created in WIN1252, a silently-ignored Vitest option that left the suite fully parallel, two notification tests passing vacuously, six create forms that invited duplicate records, and — from writing the deployment notes — a proxy rule that would have made every Stripe webhook and every cron run fail in production.

20. Deployment readiness

Ready: the application builds, all gates are green, the security headers and CSP are real and tested, /api/health answers, the cron endpoint is scheduled in vercel.json and accepts both the GET Vercel sends and a POST, logging is structured, and docs/OPERATIONS.md §Q has the exact steps.

Not ready to be called production-grade, and it would be dishonest to say otherwise. The list below is as it stood before the hardening pass; §25 records what changed.

21. Remaining limitations

  1. No row-level security. Isolation is enforced by the scoped client and proven by the matrix. RLS is the layer that would survive a bug in that client.
  2. No caching. Every figure is computed per request. The Command Center is 18 queries and analytics is 29 — fine now, and the first thing to revisit at scale.
  3. No nightly rollups. Analytics computes live over the full history.
  4. No malware scanning on document upload.
  5. No OAuth, MFA or passkeys.
  6. No real-time. No WebSocket or SSE; the notification bell polls.
  7. IPv6 is not supported in the IP allowlist — the CIDR validator refuses what the matcher cannot enforce, rather than accepting it and quietly not enforcing it.
  8. Email is single-transport. Without SMTP configured it prints to the server log.
  9. The local storage driver is unsuitable on serverless; S3 credentials are required for a real deployment.
  10. Coverage is 50%. The untested half is overwhelmingly transport and UI, but it is untested.
  11. No internationalisation. English, UTC, and en number formatting.
  12. Accessibility has not been audited with a screen reader. Semantics, labels, focus order and aria-* were built in deliberately and are exercised by role-based E2E selectors, but that is not the same as an audit.

22. Required production environment variables

Corrected during the hardening pass (§25). This table previously listed AUTH_SECRET as required — it does not exist in the code and never did — and listed email, storage, cron and error reporting as optional. Three of those break the product on a serverless host and the fourth blinds you to it. All four fail silently.

Required. npm run check:production refuses a configuration missing any of these, and says what breaks rather than only naming the variable:

Variable Purpose Without it
DATABASE_URL Pooled connection, runtime Every request fails.
DIRECT_DATABASE_URL Direct connection, migrations only prisma migrate deploy is unreliable through a pooler.
NEXT_PUBLIC_APP_URL Absolute origin, https Every link in every email is wrong.
NEXT_PUBLIC_APP_NAME Display name Defaults to “NEXUS OS”. Cosmetic.
EMAIL_PROVIDER + EMAIL_FROM A real transport: smtp or resend Sign-up strands every user — the verification link goes to a log.
STORAGE_DRIVER=s3 + S3_* Object storage Uploaded documents vanish on an ephemeral filesystem.
CRON_SECRET Shared secret for the scheduled sweep No notifications, no workflow resumption, no scheduled reports.
SENTRY_DSN Error reporting Errors reach stdout only. No alerting, no retention.

EMAIL_PROVIDER=smtp additionally requires SMTP_HOST, SMTP_USER and SMTP_PASSWORD (and SMTP_PORT / SMTP_SECURE if not 587/STARTTLS); EMAIL_PROVIDER=resend requires RESEND_API_KEY. STORAGE_DRIVER=s3 requires S3_ENDPOINT, S3_REGION, S3_BUCKET, S3_ACCESS_KEY_ID and S3_SECRET_ACCESS_KEY. Each is validated at the point of use, and selecting a transport without its credentials throws rather than falling back — a deployment that believes it is sending email and is not is worse than one that knows it cannot.

Genuinely optional — each enables a capability, and its absence is a state the product is explicit about rather than a degradation it hides:

Variable Without it
ANTHROPIC_API_KEY The assistant says it is not configured. It invents nothing.
STRIPE_SECRET_KEY + STRIPE_WEBHOOK_SECRET No checkout; an owner sets the plan, recorded as manual.
STRIPE_PRICE_TEAM / STRIPE_PRICE_BUSINESS That plan cannot be purchased.
AI_MONTHLY_BUDGET_MICROS Defaults to 50 USD per organization per month.
SENTRY_ENVIRONMENT / _RELEASE / _SERVER_NAME Reports are harder to group. Nothing breaks.
STORAGE_MAX_UPLOAD_BYTES Defaults to 25 MB.

Two checks, answering two different questions:

No secret is ever NEXT_PUBLIC_, and .env.local is gitignored and not committed — only .env.example, which contains placeholders and nothing else.

23. Running it locally — exact commands

# Install, approving the install scripts npm 11 blocks by default
npm install
npm approve-scripts prisma @prisma/engines esbuild unrs-resolver
npm rebuild prisma @prisma/engines esbuild unrs-resolver

# A real PostgreSQL, no Docker and no admin rights
npm run db:start                 # prints the DATABASE_URL to use

# Configure
cp .env.example .env.local       # paste in the two database URLs

# Schema and permission catalogue
npm run db:generate
npm run db:deploy
npm run db:seed

# Optional: a populated organization to look at
npm run db:demo                  # prints sign-in details

# Run
npm run dev                      # http://localhost:3000

Verification:

npm run typecheck                # tsc --noEmit
npm run lint                     # ESLint, including architectural boundary rules
npm run format:check             # Prettier
npm run check:env                # .env.example matches the schema
npm run check:production         # is THIS configuration fit to serve real users
npm test                         # 1,087 unit and integration tests
npm run verify                   # all of the above in one command
npm run build                    # production build
npm run test:e2e                 # 53 Playwright journeys against that build
npm run check:bundle             # client bundle budget, gzipped

Database:

npm run db:start / db:stop       # local PostgreSQL on port 55432
npm run db:validate              # validate the schema
npm run db:migrate               # create and apply a migration (development)
npm run db:deploy                # apply migrations (CI / production)
npm run db:studio                # browse data

24. Deploying to Vercel — exact steps

Revised in the hardening pass. Steps 3, 7 and 10 changed materially: three variables previously described as optional are required, and the preflight now proves it before a deploy rather than after.

  1. Provision PostgreSQL 17 (Neon, Supabase, or any managed Postgres). Take both connection strings: pooled and direct. Enable point-in-time recovery now, not later — docs/RUNBOOK.md §2.1 lists what to turn on per provider.

  2. Provision an S3-compatible bucket (AWS S3, Cloudflare R2, MinIO). It must be private: this application never issues a public object URL. Enable versioning, so an overwrite or a delete is recoverable.

  3. Set environment variables for Production and Preview — all eight required ones from §22. Then prove the configuration is fit before deploying anything:

    npm run check:production        # exit 0, or it tells you exactly what breaks
    

    Do not skip this on the grounds that the build succeeds. Every blocker it reports fails silently at runtime: the application starts, serves pages, accepts sign-ups, and does none of the things those users are waiting for.

  4. Import the repository in Vercel. Framework preset: Next.js. No build command override.

  5. Run migrations from your machine, against the direct URL:

    DIRECT_DATABASE_URL='postgresql://…' DATABASE_URL='postgresql://…' npm run db:deploy
    
  6. Seed the permission catalogue — idempotent, and safe to re-run after every deploy that adds permissions. Skipping it means every page 403s:

    DATABASE_URL='postgresql://…' npm run db:seed
    
  7. Deploy, then confirm all four:

    curl -s  https://<domain>/api/health            # {"status":"ok","checks":{"database":"ok"}}
    curl -sI https://<domain>/ | grep -i content-security-policy
    curl -sI https://<domain>/ | grep -i strict-transport-security
    curl -s  https://<domain>/robots.txt            # Disallow: /
    
  8. Confirm the cron job under Settings → Cron Jobs. vercel.json registers /api/cron/outbox every five minutes, and the route accepts the GET that Vercel sends as well as a POST. Then confirm it actually ran: look for a cron.outbox.completed line in the function logs within ten minutes. Registration is not evidence of execution — a registered cron whose secret is wrong answers 404 forever and looks fine in the dashboard.

  9. If using Stripe, add a webhook endpoint at https://<domain>/api/webhooks/billing, subscribe to customer.subscription.* and invoice.payment_*, and set STRIPE_WEBHOOK_SECRET to the signing secret. Send a test event and confirm a BillingEvent row appears with processedAt set. Until this is done, plans can only be set by hand, and a manual set is recorded as a manual decision with no paid period — so reconciliation can always tell “we saw a payment” from “somebody said so”.

  10. Make one real call against every external integration in a staging project before production. None has ever run against its live service (§25.3), and each is a place where the documented contract and the current API can have drifted:

    • Email — sign up as yourself and confirm the verification message arrives.
    • Anthropic — open the Intelligence Center and ask one question.
    • Sentry — throw a deliberate error and confirm the issue appears.
    • Storage — upload a document, then download it from a different session.
  11. Configure monitoring — docs/RUNBOOK.md §1: an uptime monitor on /api/health, and the eight log-based alerts. The most important is the absence of cron.outbox.completed: a stalled schedule is invisible to an uptime check and stops notifications, workflow resumption and scheduled reports at once.

  12. Run the restore drill — docs/RUNBOOK.md §2.5 — and write down how long it took. Until this is done the backup strategy is a hypothesis, and the deployment is not finished.

25. The production-hardening pass

Added after the report above was first written, and it changed several of its conclusions. Where §20–§24 now disagree with an earlier statement, the later one is correct and the change is described here.

25.1 What was fixed

Two real defects in the Stripe webhook path. Neither was covered by idempotency-by-event-id, and both would have been silent — the subscription would simply have held the wrong state, with nothing logged and nothing to notice:

  1. A failed webhook could never be retried successfully. claimEvent refused any event id it had already seen, including one whose first attempt had thrown. The sequence was: attempt one claims the row, handling fails, the route answers 500 to ask for a retry — and every retry is then refused as “already processed” and answered 200. A transient database error during a subscription update meant that subscription never caught up. The claim now distinguishes seen from processed: an unprocessed row is re-claimable through a conditional update, so exactly one of two concurrent retries proceeds.

  2. Out-of-order delivery could revive a cancelled subscription. Webhooks are not ordered. A customer.subscription.updated retried after a timeout can arrive after the customer.subscription.deleted that followed it, and the older payload won. Subscription.lastProviderEventAt now records the provider timestamp of the newest applied event, and older state is recorded and refused. Idempotency does not cover this case: these are two different events, each processed exactly once, in the wrong order.

A documentation defect that would have misled every deployment. AUTH_SECRET was listed as a required production variable in three places. No code in this repository reads it, and none ever did — sessions are opaque 256-bit random tokens stored as SHA-256 digests, so there is no secret to sign with. Setting it would have done nothing while creating the impression that something was protected by it. Removed from the documentation rather than added to the code.

Four “optional” variables that are not optional. §22 previously listed email, cron, storage and error reporting as capabilities you could leave off. On a serverless deployment three of them break the product and the fourth blinds you to it — and each fails silently. npm run check:production now refuses a configuration missing any of them, with the consequence spelled out rather than the variable name alone.

Everything else added:

Area Change
Error reporting A port with a Sentry envelope adapter over fetch. Server errors report through the logger, so all 18 existing call sites are covered without touching any of them. Browser errors post to /api/telemetry/error — the DSN never reaches the client, and the same redaction applies to both.
Email Two real transports: SMTP (STARTTLS mandatory, refusing to authenticate in the clear) and Resend. Header injection is stripped from subjects, which are partly derived from organization names.
Stripe transport A 15-second timeout, and an Idempotency-Key on checkout creation so a double click cannot produce two sessions.
AI transport Bounded retry on 429/5xx honouring retry-after, with full jitter; no retry on 4xx, which would only waste the user’s time; and a timeout that reports itself in words rather than as AbortError. Retrying never outlives the caller’s own deadline.
Cron A Postgres advisory lock so invocations cannot pile up, and a cron.outbox.completed log line on every run — whose absence is the only way to detect a stalled schedule.
Runbook docs/RUNBOOK.md: uptime-monitor configuration, eight log-based alert rules, the full restore procedure, and what the monitor will not catch.

25.2 What was verified

25.3 What could NOT be verified — BLOCKED

No credentials for any external service were available, and none were invented. Each integration below is written to its documented contract and tested against constructed payloads; the network path has never been exercised, and the first real call will be the first real call.

Integration Blocked on What a first real call would prove
Stripe STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET That the live event shape matches what handleEvent reads. One field worth watching: newer Stripe API versions moved current_period_end onto the subscription item.
Anthropic ANTHROPIC_API_KEY That the Messages API request and response shapes are current.
SMTP SMTP_HOST, SMTP_USER, SMTP_PASSWORD That real servers accept this dialogue. Servers differ in what they announce and tolerate.
Resend RESEND_API_KEY That the payload is accepted and the sending domain is verified.
Sentry SENTRY_DSN That the envelope is accepted. Grouping will be by exception type and message rather than by stack frame — see the note in reporting.ts.
Backup/restore A real database with real volume That the procedure in docs/RUNBOOK.md §2 works, and how long it takes.
Vercel cron A deployment That the schedule is registered and firing. Nothing in this repository can assert that.
Load A load-testing target Throughput under concurrency. Query counts are pinned; timing is not.

25.4 Remaining limitations, after this pass

Unchanged from §21 except where noted:

  1. No row-level security. Deliberately not attempted in this pass: RLS with Prisma through a transaction-mode pooler requires per-transaction session variables, and adding that to 66 tables at the end of a hardening pass would risk the isolation that currently works and is proven. It remains the correct next structural investment.
  2. No MFA, OAuth or passkeys. These are features, not hardening. Password-only with a 30-day sliding session remains the largest authentication-side risk.
  3. No malware scanning. Requires an external scanner; BLOCKED on infrastructure.
  4. No shared-store rate limiting. Authentication throttling is database-backed and correct across instances. The telemetry limiter is in-memory and therefore per-instance — an accepted approximation, stated where it is implemented, on an endpoint that only writes telemetry.
  5. No cache layer and no rollups. Unchanged.
  6. No load testing. Unchanged.
  7. Sentry grouping is coarse. Without the SDK there is no stack-frame parser and no source-map resolution, so issues group by exception type and message. The trade is documented in reporting.ts; switching to @sentry/nextjs is a contained change.
  8. SMTP is minimal. No connection pooling, no DKIM signing, no attachments. If any of those become necessary, Nodemailer is the answer.
  9. Coverage. Now 51 test files and 1,087 tests, up from 48 and 1,031.

25.5 Is it production-ready?

No — and the gap is now configuration and rehearsal rather than code.

What would make that a yes, in order:

  1. Set the eight required variables (§22) — npm run check:production must exit 0.
  2. Make one real call against each of Stripe, Anthropic and the email transport in a staging project, and confirm the shapes match.
  3. Point an uptime monitor at /api/health and configure the eight log alerts in docs/RUNBOOK.md §1.4.
  4. Perform the restore drill in docs/RUNBOOK.md §2.5 and write down the elapsed time.

Until step 4 is done, the honest claim is: the application is ready to be deployed to a staging environment and exercised. It is not ready to hold data somebody would miss.


26. Completion and staging-readiness pass

The last pass before a first staging deployment. It found three more documentation defects of the same class as AUTH_SECRET, one real correctness bug, and two gaps.

26.1 DONE

Item What changed
Creator attribution Built by Muhammad Kashif on the landing page footer, a new Settings → About screen with contact address, and the README. One constant (src/lib/creator.ts) so they cannot drift.
Stripe API-version handling current_period_end is read from the subscription and from the subscription item, covering both sides of the 2025-03-31.basil change. A test for each shape.
S3 request timeout The last external call without one. A hung request held a serverless invocation until the platform killed it.
Skip link A keyboard user previously tabbed through fourteen navigation items to reach content, on every navigation. Added, and covered by an end-to-end test.
Environment-variable audit The authoritative list is 35 variables, enumerated from the Zod schema. Every specified-but-never-built variable is now named as such in docs/OPERATIONS.md §P.4.
Deployment guide docs/DEPLOYMENT.md — a staging walkthrough where every step that can only be confirmed by running it is marked CONFIRM.
AI retrieval documentation docs/AI-AND-AUTOMATION.md now states that vector retrieval was never built, what replaced it, and what adding it would involve.

26.2 Three more documentation defects

The same failure mode as AUTH_SECRET in §25: documentation describing variables and features that do not exist, which a deployer would waste time on and — worse — might believe was protecting something.

  1. AI_PROVIDER does not exist. Nor does OPENAI_API_KEY, AI_MODEL_* or AI_MONTHLY_TOKEN_BUDGET. Anthropic is the only adapter and is selected by the presence of ANTHROPIC_API_KEY alone; model choice is a code table keyed by purpose (src/lib/ai/router.ts), deliberately not configuration. The budget variable that does exist is AI_MONTHLY_BUDGET_MICROS.
  2. Four of the six documented Stripe price variables do not exist, and neither does NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY — there is no Stripe script in the browser, because checkout is a redirect to a provider-hosted page.
  3. UPSTASH_REDIS_*, FEATURE_*, ENCRYPTION_KEY, AUTH_URL, AUTH_GOOGLE_* and AUTH_GITHUB_* do not exist. No cache, no feature flags, nothing encrypted at rest, no OAuth.

26.3 The pgvector question, answered

The specification’s context layer reads “Retrieval (SQL scopes + pgvector)”. Vector retrieval was never built — no extension, no embeddings table, no embedding is ever computed, no similarity search exists. EmbedRequest/EmbedResult exist on the port and the Anthropic adapter’s embed() throws rather than returning a plausible zero vector.

What exists instead: 13 typed tools, each calling a module query boundary that runs ctx.require() before reading a row, plus a rolling window of the last 20 conversation turns.

This is worth being precise about in both directions. It is a narrower capability — the assistant cannot answer by similarity, only through the filters the tools expose. It is also a stronger security position, and not merely a consolation: with retrieval-by-embedding, authorization is applied to chunks after the fact and a mis-scoped index leaks silently; with retrieval-by-tool, the AI path is the same authorization path as the rest of the application, so the isolation matrix already covers it.

Semantic retrieval is FUTURE, and adding it means pgvector, an embeddings table carrying organizationId, a second provider (Anthropic has no embeddings endpoint), a backfill, and a per-chunk permission re-check before anything reaches a prompt.

26.4 VERIFIED in this pass

Locally, without any external credential:

26.5 BLOCKED — requires credentials or a deployment

Unchanged from §25.3 and repeated because it is the part that matters:

Integration Blocked on
Email delivery RESEND_API_KEY, or SMTP_HOST/SMTP_USER/SMTP_PASSWORD
Document storage S3_ENDPOINT, S3_REGION, S3_BUCKET, S3_ACCESS_KEY_ID, S3_SECRET_ACCESS_KEY
Stripe STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET (test mode)
Anthropic ANTHROPIC_API_KEY
Error reporting SENTRY_DSN
Cron registration A Vercel deployment
Backup/restore drill A provisioned database with real volume

No live call has been made to any of them. Nothing in this repository claims otherwise.

26.6 FUTURE — deliberate, documented gaps

Not attempted, with the reason rather than an apology:

26.7 Status

Ready for a first staging deployment. Not production-ready, and §25.5 still holds: the remaining work is configuration and rehearsal, not code. The honest one-line summary is that every line of this application has been tested against a real database and none of it has ever spoken to a real external service.


Where to read further