Architecture (Restructured)
1. Goals of the Restructure
One app, one deploy. A single Next.js project instead of 8 workspace packages.
No tRPC. Use native Next.js primitives: Server Components for reads and Server Actions (plus a few Route Handlers) for writes. Type-safety comes from plain TypeScript function signatures + Zod + Prisma, not a transport layer.
Keep clean layering. UI → Action → Service → Repository → Prisma. Business rules never leak into components; SQL never leaks out of repositories.
Extensible by default. The data model (see
ER_DIAGRAM.eraser) is additive-only, polymorphic where it helps, and JSON-extensible so future features don't require breaking migrations.
2. Technology Stack
| Concern | Choice |
|---|---|
| Framework | Next.js (App Router) — RSC + Server Actions |
| Language | TypeScript (strict) |
| Database | PostgreSQL |
| ORM | Prisma |
| Validation | Zod (shared input/output schemas) |
| Auth | Session-based (httpOnly cookie + sessions table) |
| Authorization | Table-driven RBAC (roles / permissions / role_permissions) |
| UI | Tailwind CSS + shadcn/ui |
| Client data (interactive) | TanStack Query (only where client interactivity needs it) |
| Files | Local disk now → S3-compatible later (storage abstraction) |
| Background work | In-process job runner (notifications, deadline sweeps) |
| Deploy | Node (next start) under PM2, Caddy reverse proxy, Docker Postgres |
Removed vs old SDD: tRPC, packages/trpc, packages/database, packages/notifications, packages/ui, packages/shared, packages/config, packages/utils, packages/ai, turbo.json, pnpm-workspace.yaml.
3. High-Level Layering
flowchart TD
subgraph Client
RSC[Server Components<br/>read data]
CC[Client Components<br/>forms, boards, editors]
end
subgraph NextServer[Next.js Server]
SA[Server Actions<br/>'use server']
RH[Route Handlers<br/>/api/* files, webhooks, health]
SVC[Service Layer<br/>business rules, RBAC, tx, activity, notifications]
REPO[Repository Layer<br/>Prisma queries only]
end
DB[(PostgreSQL)]
RSC -->|call service read| SVC
CC -->|invoke| SA
CC -->|fetch| RH
SA --> SVC
RH --> SVC
SVC --> REPO
REPO --> DB
Rules
Components (RSC or client) never touch Prisma directly.
Server Actions/Route Handlers are thin: authenticate → validate (Zod) → call a service → return.
Services own business logic, permission checks, transactions, activity logging, and notification triggers. Services may call multiple repositories.
Repositories are the only place Prisma is used. No business rules inside them.
4. Request Flows
4.1 Mutation (write)
sequenceDiagram
participant U as Client Component
participant A as Server Action
participant S as Service
participant R as Repository
participant DB as PostgreSQL
U->>A: submit form (typed args)
A->>A: getCurrentUser() + Zod.parse(input)
A->>S: service.doThing(user, dto)
S->>S: can(user, permission, resource)?
S->>R: repo.write(...) inside prisma.$transaction
R->>DB: INSERT/UPDATE
S->>R: activityRepo.record(...)
S-->>A: result
A-->>U: revalidatePath() / typed result
4.2 Read (RSC)
sequenceDiagram
participant P as Server Component (page.tsx)
participant S as Service (read)
participant R as Repository
participant DB as PostgreSQL
P->>S: service.getProjectDashboard(user, projectId)
S->>S: assert membership / RBAC scope
S->>R: repo.findDashboardData(...)
R->>DB: SELECT ...
R-->>S: rows
S-->>P: view model
P-->>P: render
5. Folder Structure (single Next.js app)
agency-os/
├── prisma/
│ ├── schema.prisma # single schema (implements ER_DIAGRAM.eraser)
│ ├── migrations/
│ └── seed.ts # roles, permissions, demo users/project
├── public/
├── src/
│ ├── app/ # App Router (routes only)
│ │ ├── (auth)/login/
│ │ ├── (main)/ # authenticated shell (sidebar + header)
│ │ │ ├── layout.tsx
│ │ │ ├── page.tsx # role-aware dashboard
│ │ │ ├── projects/
│ │ │ │ ├── page.tsx
│ │ │ │ ├── new/
│ │ │ │ └── [projectId]/
│ │ │ │ ├── (dashboard)/
│ │ │ │ ├── planning/
│ │ │ │ ├── development/ # sprints, backlog, kanban, deps
│ │ │ │ ├── knowledge/
│ │ │ │ ├── ai-skills/
│ │ │ │ ├── activity/
│ │ │ │ └── settings/
│ │ │ │ └── whiteboard/
│ │ │ ├── ai-library/
│ │ │ ├── notifications/
│ │ │ └── settings/
│ │ ├── share/[token]/ # public, read-only client progress
│ │ ├── api/ # Route Handlers only where actions don't fit
│ │ │ ├── files/[...]/route.ts # upload/download streaming
│ │ │ └── health/route.ts
│ │ └── layout.tsx
│ │
│ ├── features/ # DOMAIN MODULES (the heart of the app)
│ │ ├── auth/
│ │ │ ├── actions.ts # 'use server' entry points
│ │ │ ├── service.ts # business logic
│ │ │ ├── repository.ts # Prisma access
│ │ │ ├── schema.ts # Zod DTOs
│ │ │ └── components/
│ │ ├── projects/
│ │ ├── planning/ # documents, references, whiteboards, decisions
│ │ ├── development/ # sprints, main tasks, sub tasks, deps, checklists
│ │ ├── knowledge/ # shares `documents` with planning (workspace flag)
│ │ ├── ai-skills/
│ │ ├── contractors/
│ │ ├── notifications/
│ │ ├── activity/
│ │ ├── search/
│ │ └── files/
│ │
│ ├── server/
│ │ ├── db.ts # Prisma client singleton
│ │ ├── auth/
│ │ │ ├── session.ts # create/verify/destroy session cookie
│ │ │ ├── context.ts # getCurrentUser() for RSC + actions
│ │ │ └── rbac.ts # can(user, permission, resource)
│ │ └── jobs/ # background workers
│ │ ├── runner.ts
│ │ ├── deadline-sweep.ts
│ │ └── notification-dispatch.ts
│ │
│ ├── components/ui/ # shadcn components
│ ├── lib/ # utils, constants, enums mirror
│ ├── hooks/ # client hooks (TanStack Query wrappers)
│ ├── types/
│ └── middleware.ts # protects (main) routes, redirects to /login
│
├── docker-compose.yml # PostgreSQL 16
├── .env
├── next.config.ts
└── package.json
Feature module contract — every folder under features/ may expose: actions.ts (server actions) · service.ts (logic) · repository.ts (Prisma) · schema.ts (Zod) · components/ (UI). Adding a new domain = add a new folder. Nothing else in the app needs to change.
6. Data Access
Prisma singleton in
src/server/db.ts(guards against dev hot-reload leaks).Repositories wrap Prisma per aggregate root (Project, Sprint, Task, Document, AISkill, Notification, ActivityLog, …). One repository per aggregate.
Transactions (
prisma.$transaction) live in services, for multi-step operations: project creation (+ default docs + activity), contractor assignment (+ membership + activity + notification), task completion (+ status + notification), share-link generation, sprint creation.Soft delete via
deleted_aton major entities; projects also carryarchived_atARCHIVEDstatus. Audit tables (activity_logs,decision_logs) are append-only.
7. Validation & Type Safety (without tRPC)
Each feature has
schema.tswith Zod DTOs. Server Actionsparse()input at the boundary; the parsed type flows into services.Service and repository signatures are plain typed TS functions — call-site type checking replaces tRPC's wire types.
Prisma generates DB types; services map DB rows → view models returned to RSC.
Shared enums (statuses, priorities, roles) live in
src/lib/enums.tsand mirror the Prisma enums, so UI and server agree without a transport contract.
8. Authentication
Session-based. On login: verify credentials → create
sessionsrow → sethttpOnly,secure,sameSite=laxcookie with the session token.middleware.tsguards(main)/**andshareis public (token-scoped).getCurrentUser()(inserver/auth/context.ts) resolves the session for RSC and Server Actions. Unauthenticated → redirect/login.First login sets
users.is_first_login = true→ contractor onboarding wizard.
9. Authorization (RBAC, future-proof)
Table-driven so new roles (e.g. Project Manager) require zero schema changes — just new rows:
roles(scope =SYSTEM|PROJECT, e.g.developer,contractor,project_manager)permissions(project.create,task.update,credential.read, …)role_permissions(join)users.role_id→ global role;project_members.role_id→ per-project role
can(user, permission, project?) resolves system role + project membership role, unions their permissions, and enforces project scoping (contractors only see assigned projects).
10. Cross-Cutting Concerns
| Concern | Where | Notes |
|---|---|---|
| Activity logging | Service layer → activity_logs |
Append-only; polymorphic entity_type/entity_id; string action so new events need no migration |
| Notifications | features/notifications + server/jobs |
notifications (logical) → notification_logs (per-channel delivery). Providers: Email, WhatsApp, In-App. notification_preferences per user/type/channel |
| Files | features/files + /api/files |
Storage abstraction (local now, s3 later). Polymorphic attachments (attachable_type/attachable_id) |
| Search | features/search |
Title-only (Postgres ILIKE) for Phase 1; pg_trgm/tsvector later without schema change |
| Documents | features/planning + features/knowledge |
One documents table with workspace (PLANNING/KNOWLEDGE) + type; versioned via document_versions |
11. Background Jobs
A lightweight in-process runner (server/jobs/runner.ts) started with the server:
notification-dispatch — drains
PENDINGnotifications, calls providers, writesnotification_logs, marksSENT/FAILED.deadline-sweep — periodic scan for approaching/overdue task deadlines → enqueue notifications.
Can be swapped for a real queue (BullMQ/Redis) later without touching feature code.
12. Error Handling
Actions catch service errors and return a typed
{ ok: false, error }shape (or throw forerror.tsxboundaries on RSC reads).Domain errors are typed (
NotFoundError,ForbiddenError,ValidationError).Client surfaces failures via toasts; forms show field-level Zod errors.
13. Deployment (self-hosted)
git pull → npm ci → prisma migrate deploy → next build → pm2 reload
PostgreSQL via
docker-compose.yml(bound to127.0.0.1:5432).Caddy terminates HTTPS and reverse-proxies to
localhost:3000.PM2 runs
next startwith auto-restart + env injection.Env validated at boot with Zod (
src/lib/env.ts).
14. Extensibility Principles (won't break later)
Additive migrations only — never rename/drop columns in place; add + backfill + deprecate.
Enums grow, never shrink — Postgres
ALTER TYPE ... ADD VALUEis non-breaking.metadata jsonon major entities absorbs new attributes without migrations.Polymorphic links (
attachments,comments,task_dependencies,task_labels,activity_logs) let new entity types plug in with no schema change (integrity enforced in services).Table-driven RBAC — new roles/permissions are data, not code.
Unified documents + versioning — new doc types are enum values, not new tables.
Storage & notification abstractions — swap providers without touching callers.
Multi-tenancy path — add an optional
organization_idFK later; nullable + backfill = non-breaking.



