5bf5dfce71
- Rename src/app/c/[token] → src/app/client/[token] - Update proxy.ts, ClientRow, admin client detail with /client/ path - Add output: "standalone" to next.config.ts for Docker build - Add Dockerfile (multi-stage, node:20-alpine) and .dockerignore - Push schema to Coolify Postgres via SSH tunnel (drizzle-kit push ✓) - Update CLAUDE.md constraint 4 to reflect /client/ route - Add Phase 4 planning artifacts (04-00, 04-RESEARCH, 04-PATTERNS) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
6.1 KiB
6.1 KiB
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.tokenis the only secret. Rotation = single UPDATE. No session store needed.clients.accepted_totalis deliberately denormalized so client API never touchesquote_items.- Approval
approved_atstored as immutable audit trail — disputes resolved by timestamp. commentsuseentity_type + entity_idpolymorphic pair — correct at this scale.paymentsalways 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