Skip to main content

Command Palette

Search for a command to run...

Architecture (Restructured)

Updated
9 min readView as Markdown

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_at on major entities; projects also carry archived_at

    • ARCHIVED status. Audit tables (activity_logs, decision_logs) are append-only.

7. Validation & Type Safety (without tRPC)

  • Each feature has schema.ts with Zod DTOs. Server Actions parse() 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.ts and mirror the Prisma enums, so UI and server agree without a transport contract.


8. Authentication

  • Session-based. On login: verify credentials → create sessions row → set httpOnly, secure, sameSite=lax cookie with the session token.

  • middleware.ts guards (main)/** and share is public (token-scoped).

  • getCurrentUser() (in server/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 PENDING notifications, calls providers, writes notification_logs, marks SENT/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 for error.tsx boundaries 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 to 127.0.0.1:5432).

  • Caddy terminates HTTPS and reverse-proxies to localhost:3000.

  • PM2 runs next start with auto-restart + env injection.

  • Env validated at boot with Zod (src/lib/env.ts).


14. Extensibility Principles (won't break later)

  1. Additive migrations only — never rename/drop columns in place; add + backfill + deprecate.

  2. Enums grow, never shrink — Postgres ALTER TYPE ... ADD VALUE is non-breaking.

  3. metadata json on major entities absorbs new attributes without migrations.

  4. Polymorphic links (attachments, comments, task_dependencies, task_labels, activity_logs) let new entity types plug in with no schema change (integrity enforced in services).

  5. Table-driven RBAC — new roles/permissions are data, not code.

  6. Unified documents + versioning — new doc types are enum values, not new tables.

  7. Storage & notification abstractions — swap providers without touching callers.

  8. Multi-tenancy path — add an optional organization_id FK later; nullable + backfill = non-breaking.