# Architecture — ClientHub Freelancer Client Portal **Project:** ClientHub (welcomeclient.iamcavalli.net) **Researched:** 2026-05-09 **Confidence:** HIGH --- ## Component Boundaries Single Next.js application on Vercel. No separate backend. All server logic lives in Route Handlers (`/api/**`). One Postgres database (Neon serverless) accessed via Drizzle ORM. Admin auth via env-var secret + cookie. Client access via UUID token in URL — no auth library needed for clients. | Component | Responsibility | Communicates With | |-----------|---------------|-------------------| | Client Portal `/c/[token]` | Read-only view: status, phases, tasks, deliverables, payments, documents | API Routes (GET only) | | Admin Dashboard `/admin` | List all clients with status summary | API Routes (full CRUD) | | Admin Client Workspace `/admin/clients/[id]` | Edit phases, tasks, deliverables, payments, documents | API Routes (full CRUD) | | Service Catalog Manager `/admin/catalog` | CRUD on service items + unit prices | API Routes (catalog entity) | | Quote Builder `/admin/clients/[id]/quote` | Compose quote from catalog items, write `accepted_total` to client row | Catalog + API Routes | | Comments System | Client posts on task/deliverable; admin replies | API Route POST | | Approval Flow | Client PATCHes a deliverable to `approved` | API Route, validates token ownership | | API Routes `/api/**` | Validate token or admin session; query/mutate DB; return JSON | Postgres only | | Database | Single source of truth | API Routes only — never queried from browser | --- ## Data Flow **Client reading their dashboard:** ``` Browser → GET /c/[token] → Next.js server component → DB: clients WHERE token = [token] → 404 if missing → JOIN: project + phases + tasks + deliverables + payments + documents → Omit: quote_items, service prices → Render read-only portal ``` **Client posting a comment:** ``` Browser → POST /api/comments { token, entity_type, entity_id, body } → Validate token → write comment { author: 'client' } → 201 → re-fetch thread ``` **Client approving a deliverable:** ``` Browser → PATCH /api/deliverables/[id]/approve { token } → Validate token owns deliverable → set status='approved', approved_at=now() → Return updated deliverable ``` **Admin editing:** ``` Browser (admin) → PATCH /api/admin/tasks/[id] + admin cookie → Validate session → update row → return updated task ``` **Quote building:** ``` Admin UI selects services → computes line items → POST /api/admin/clients/[id]/quote { line_items[], accepted_total } → Write quote_items rows + write clients.accepted_total (denormalized) → Client portal reads clients.accepted_total — never touches quote_items ``` --- ## Data Model ``` clients id UUID PK name TEXT brand_name TEXT brief TEXT token UUID UNIQUE ← the secret link key (separate from PK!) accepted_total NUMERIC ← denormalized; only price client ever sees created_at TIMESTAMPTZ phases id UUID PK client_id UUID → clients.id title TEXT sort_order INT status TEXT (upcoming | active | done) tasks id UUID PK phase_id UUID → phases.id title TEXT description TEXT status TEXT (todo | in_progress | done) sort_order INT deliverables id UUID PK task_id UUID → tasks.id title TEXT url TEXT ← external link (Google Drive, PDF, etc.) status TEXT (pending | submitted | approved) approved_at TIMESTAMPTZ ← immutable audit trail comments id UUID PK entity_type TEXT (task | deliverable) entity_id UUID author TEXT (client | admin) body TEXT created_at TIMESTAMPTZ payments id UUID PK client_id UUID → clients.id label TEXT ("Acconto 50%" / "Saldo 50%") amount NUMERIC status TEXT (da_saldare | inviata | saldato) paid_at TIMESTAMPTZ documents id UUID PK client_id UUID → clients.id label TEXT url TEXT created_at TIMESTAMPTZ service_catalog id UUID PK name TEXT description TEXT unit_price NUMERIC active BOOLEAN quote_items id UUID PK client_id UUID → clients.id service_id UUID → service_catalog.id quantity NUMERIC unit_price NUMERIC ← snapshot at time of quote subtotal NUMERIC -- NEVER exposed via client API ``` **Key design decisions:** - `clients.token` is the only secret. Rotation = single UPDATE. No session store needed. - `clients.accepted_total` is deliberately denormalized so client API never touches `quote_items`. - Approval `approved_at` stored as immutable audit trail — disputes resolved by timestamp. - `comments` use `entity_type + entity_id` polymorphic pair — correct at this scale. - `payments` always two rows per client (created when quote is finalized). --- ## Suggested Build Order ``` 1. DB schema + migrations └─ everything depends on this 2. API: token lookup + project read (GET only) └─ unblocks client portal 3. Client portal UI /c/[token] └─ the core deliverable; clients need this first 4. Admin auth middleware (env-var secret, cookie check) └─ gate before admin routes go live 5. Admin: client list + client workspace CRUD └─ phases, tasks, status, documents, payments 6. Comments system + deliverable approval └─ depends on both client portal and admin workspace 7. Service catalog CRUD ← can run parallel with step 5 └─ independent of client-facing features 8. Quote builder └─ depends on catalog + client entity 9. Claude onboarding flow (v2) └─ depends on all CRUD APIs being complete ``` --- ## Roadmap Implications - Phase 1: DB schema + token API + client portal (all three coupled) - Phase 2: Admin auth + CRUD management + comments + approvals - Phase 3: Service catalog + quote builder - Phase 4 (v2): Claude onboarding flow