Version: 0.1 (pre-implementation) Status: Awaiting approval. No application code written yet.
| Document | Contents |
|---|---|
ARCHITECTURE.md (this file) |
A. Product · B. System · C. Modules · D. Tech stack |
DATA-MODEL.md |
E. Database entity map, relationships, indexes, isolation |
PLATFORM.md |
F. Folder structure · G. Auth · H. Multi-tenancy · I. RBAC |
AI-AND-AUTOMATION.md |
J. AI · K. Agents · L. Workflow engine |
OPERATIONS.md |
M. API · N. Security · O. Testing · P. Deployment · Env vars |
ROADMAP.md |
Q. Phases · R. Risks · S. What to build first · Decision log |
This is the contract the implementation must satisfy. Every phase of work references a
section here. When a decision changes during implementation it is changed here first,
with the reason recorded in the Decision Log (ROADMAP.md §T).
Conventions used throughout:
Organization. Every business record belongs to exactly one organization.User acting inside a specific organization.{ userId, orgId, membershipId, roles, permissions: Set<string>, requestId, actorType }.
Nothing reads or writes tenant data without a Ctx.NEXUS OS is a business operating system: the single place where a company’s work, money, people, clients, documents and decisions live — with an AI layer that can read and act on all of it inside the acting user’s permission boundary.
The product identity is Command Center + Automation Engine + Agent layer. CRM, Projects and Finance are applications running on the OS, not the product itself.
Three product-level principles follow from that:
┌──────────────────────────┐
│ EXECUTIVE COMMAND │ the "home" of the OS
│ CENTER (widget grid) │
└────────────┬─────────────┘
│ reads
┌───────────┬───────────┬───────────┼───────────┬───────────┬───────────┐
│ CRM │ PROJECTS │ TASKS │ PEOPLE │ FINANCE │ DOCUMENTS │ applications
└─────┬─────┴─────┬─────┴─────┬─────┴─────┬─────┴─────┬─────┴─────┬─────┘
│ │ │ │ │ │
└───────────┴──────── domain events ────────────┴───────────┘
│
┌─────────────┬───────────────┼───────────────┬──────────────┐
│ WORKFLOW │ NOTIFICATIONS │ ANALYTICS │ SEARCH │ platform services
│ AUTOMATION │ │ + REPORTING │ + AUDIT │
└─────────────┴───────────────┴───────────────┴──────────────┘
│
┌────────────┴─────────────┐
│ AI INTELLIGENCE + AGENTS │ act through tools → services
└──────────────────────────┘
| Persona | Primary surface | Job to be done |
|---|---|---|
| Founder / Exec (Owner) | Command Center, Reports, Executive Agent | “What needs my attention today?” |
| Delivery / Ops Manager | Projects, Tasks, Workload, Project Agent | keep delivery on track |
| Sales (Manager/Employee) | CRM pipeline, Deals, CRM Agent | move deals, convert won deals to projects |
| Finance Manager | Invoices, Expenses, Budgets, Finance Agent | cash in / cash out, chase overdue |
| HR Manager | People, Teams, Departments, Skills | staffing and workload balance |
| Employee | My Work, Documents, Comments | execute assigned work |
| Client (external) | Client Portal — read-only scoped subset | project status, invoices |
| Admin | Settings, Roles, Security Center, Audit | governance and compliance |
| From → To | Contract |
|---|---|
| Deal won → Project | one-click conversion: deal value seeds project budget, contact becomes client, deal activities copied to project activity |
| Project → Finance | project budget vs. actual (expenses + logged cost); invoice can be raised from a project and its milestones |
| Task → Analytics | completion/overdue events feed throughput and workload metrics |
| Any entity → Documents | polymorphic attachment (entityType, entityId) with inherited access |
| Any entity → Activity/Audit | every write produces an activity item and an audit record |
| Any event → Workflow | the trigger catalogue is the event catalogue |
| Everything → Search | outbox-driven denormalized SearchDocument rows |
Real-time collaborative editing, email inbox sync, video meetings, native mobile apps, third-party plugin marketplace, per-tenant custom code, time tracking with payroll. The architecture leaves room for all of them (events + adapters); none are in scope.
Browser (Next.js App Router — RSC shell + client islands)
│ HTTPS
▼
Vercel Edge middleware (session cookie check, org slug resolution,
│ rate-limit bucket, CSP nonce, security headers)
▼
Next.js Node runtime ──────────────────────────────────────────────┐
├─ Server Components read path → queries → services │
├─ Server Actions write path → validated commands │
├─ Route Handlers webhooks · AI streaming · file signing · │
│ cron · public API v1 │
│ │
├── SERVICE LAYER business rules · authorization · transactions│
│ ├─ writes AuditLog (same TX) │
│ └─ writes OutboxEvent (same TX) │
├── REPOSITORY LAYER Prisma only, org-scoped by client extension│
└── ADAPTERS AI · Storage · Email · Payments · Queue · Cache ───┘
│ │ │ │ │ │
▼ ▼ ▼ ▼ ▼ ▼
Anthropic S3/R2 Resend Stripe JobQueue Upstash
(swappable) presigned webhooks (DB+cron) Redis
Background plane
Vercel Cron (* * * * *) → POST /api/jobs/drain (authenticated by CRON_SECRET)
├─ drain OutboxEvent → WorkflowEngine · NotificationService ·
│ SearchIndexer · AnalyticsRollup
├─ resume paused WorkflowRuns (delays, approvals, retries)
└─ scheduled jobs: nightly metric rollups, overdue sweeps,
digest emails, subscription reconciliation, dead-letter alerts
app/** UI: RSC pages, client islands
↓ may import
modules/<m>/actions|queries transport + command boundary (Zod, Ctx, rate limit)
↓ may import
modules/<m>/service business logic, authorization, transactions, events
↓ may import
modules/<m>/repository Prisma access only
↓
lib/db Prisma client + org-scope extension
Hard rules, mechanically enforced by eslint-plugin-boundaries:
next/headers, or next/navigation.ctx: Ctx as its first parameter.Read (RSC page):
page.tsx → requireCtx() (session + membership + permissions) → queries.listProjects(ctx, filters)
→ service applies permission scoping → repository issues an org-scoped Prisma query
→ RSC renders → client islands hydrate for interactivity only.
Write (Server Action):
client form → action wrapped by createAction() → rate limit → Zod parse → build Ctx →
ctx.require('project.update') → service → single transaction: entity write + AuditLog
revalidateTag() → typed result { ok } | { error } → toast.Async (after commit):
cron drain → dispatcher loads subscribers for project.updated → WorkflowEngine matches
triggers and enqueues runs; NotificationService fans out per user preference;
SearchIndexer upserts SearchDocument; AnalyticsRollup marks the day dirty.
Every mutation writes, in one transaction: (1) the entity change, (2) an AuditLog row,
(3) an OutboxEvent row. A cron-driven runner drains the outbox at-least-once with
exponential backoff and a dead-letter queue.
This buys us: no “task created but automation never fired” class of bug; replayable history; a single place to add any new cross-cutting feature; and a natural trigger catalogue for the workflow engine (the trigger list is the event list).
Consumers must be idempotent — every handler keys off (eventId, subscriberId) in a
OutboxDelivery table.
| Module | Responsibility |
|---|---|
kernel/auth |
credentials + OAuth, sessions, MFA, email verification, password reset |
kernel/tenancy |
org resolution, org switching, membership lifecycle, invitations |
kernel/authz |
roles, permissions, can() / require(), record-level policies |
kernel/errors |
AppError taxonomy, safe mapping to HTTP / action results |
kernel/validation |
shared Zod primitives, schema registry, pagination/filter contracts |
kernel/events |
domain event catalogue, outbox writer, dispatcher, delivery tracking |
kernel/jobs |
job queue, scheduler, retry/backoff, dead-letter, cron entrypoint |
kernel/audit |
append-only audit writer + query API |
kernel/ratelimit |
per-user / per-org / per-IP / per-route sliding window |
kernel/cache |
request memoization + Redis with org-scoped keys and tag invalidation |
kernel/config |
typed env loading, feature flags, plan entitlements |
kernel/observability |
pino logger, request ids, Sentry, timing |
kernel/ui |
design system primitives, DataTable, PageShell, states |
| Concern | Choice | Why this, not the alternative |
|---|---|---|
| Framework | Next.js 15 App Router, React 19 | RSC lets the read path call services directly — no API round-trip, no duplicated DTOs. Server Actions give a typed, CSRF-protected write path. |
| Language | TypeScript strict + noUncheckedIndexedAccess |
The tenancy/authz design leans on types (branded OrgId, Ctx, permission string unions). |
| UI | Tailwind v4 + shadcn/ui + Radix + Lucide | shadcn is copy-in, so the design system is ours — required to give NEXUS a real identity instead of default-shadcn look. Radix supplies accessibility. |
| Database | PostgreSQL (Neon) | DB branching per PR makes real integration tests cheap; serverless driver avoids connection exhaustion; RLS available as defense-in-depth. |
| ORM | Prisma 6 + $extends |
The client extension is what makes tenant isolation structural (auto-injected orgId filter) rather than something developers must remember. Drizzle was considered; Prisma wins on extensions + migrate for this shape. |
| Pooling | Neon serverless driver / PgBouncer transaction mode | Serverless + Postgres connection limits is the single most common production failure for this stack. |
| Validation | Zod | One schema drives forms, action input, API contracts and AI tool parameters. |
| Forms | React Hook Form + zod resolver | Uncontrolled inputs keep dense data-entry screens fast. |
| Client data | TanStack Query v5 | Only where genuinely client-driven: kanban optimistic DnD, infinite tables, run polling. RSC covers everything else. |
| Auth | Auth.js v5 + Prisma adapter, database sessions | We need multi-org membership, revocable sessions and a Security Center. A hosted identity vendor would own the org model — but the org model is the product. Cost: we implement MFA and verification ourselves (accepted). |
| Hashing | argon2id | Memory-hard, current default. |
| AI | Vercel AI SDK v5 as transport + our own provider interface on top | AI SDK handles streaming and tool-call plumbing; our interface keeps providers swappable and gives one seam for cost metering, redaction and audit. Default: claude-opus-5 for agents, claude-sonnet-5 for high-volume summarization. |
| Embeddings/RAG | pgvector in the same Postgres | No extra service, per-org partitioning, sufficient to millions of rows. |
| Jobs | DB-backed Job table + Vercel Cron, behind QueueAdapter |
Transactional with the outbox, zero extra vendor for v1; swapping to Inngest/QStash later is an adapter change. |
| Cache / limits | Upstash Redis | Serverless-native; needed for distributed rate limiting regardless. |
| Storage | S3-compatible (R2 or S3), private bucket, presigned URLs | Files never served from the app origin; no public object URLs ever. |
| Resend + React Email | Templates as components; good deliverability. | |
| Payments | Stripe Checkout + Billing Portal + webhooks | No card data touches us; entitlements derived from webhook-synced state. |
| Charts | Recharts | Composable and SSR-friendly; sufficient for the analytics surface. |
| Tables | TanStack Table v8 (headless) behind one DataTable shell |
Server-side pagination/sort/filter with a single consistent UI. |
| Drag & drop | dnd-kit | Keyboard-accessible; react-beautiful-dnd is unmaintained. |
| @react-pdf/renderer in a Node route handler | Deterministic; avoids headless Chrome on serverless. | |
| Testing | Vitest + Playwright + Neon branch per CI run | |
| Telemetry | Sentry + structured pino logs + request ids | |
| Quality gates | ESLint (+ boundaries), Prettier, knip, tsc --noEmit |
Boundaries plugin mechanically enforces §B.2. |
| CI | GitHub Actions | typecheck → lint → unit → integration (Neon branch) → e2e → build |
Repository shape decision: a single Next.js application with enforced internal module
boundaries (src/modules/*), not a Turborepo monorepo. One deployable, one tsconfig,
no package-publishing ceremony; boundaries enforced by lint rules instead of package.json
files. If a second deployable ever appears, extraction is mechanical because the module
boundaries already exist.