# ClientHub — Walking Skeleton (Phase 1)

**Project:** ClientHub — Freelancer Client Portal  
**Phase:** 01 — Foundation & Client Dashboard  
**Date:** 2026-05-13  
**Status:** Blueprint (decisions below are LOCKED for all subsequent phases)

---

## Project Architecture — Locked Decisions

This Walking Skeleton establishes the architectural foundation for all future phases. These decisions are **immutable** without explicit user approval.

### Core Stack

| Layer | Technology | Why | Locked? |
|-------|-----------|-----|---------|
| **Framework** | Next.js 15 (App Router, TypeScript, src/) | Server Components + Edge Middleware for performance; Vercel-native | ✅ YES |
| **Database** | Postgres on Coolify (Hetzner), via `postgres-js` driver | Self-hosted (no Neon/Supabase cost); persistent via external DB | ✅ YES |
| **ORM** | Drizzle ORM with postgres-js | Zero-cost serverless driver; schema-as-code migrations | ✅ YES |
| **UI** | Tailwind CSS v4 + shadcn/ui components | Utility-first, copied components, mobile-first | ✅ YES |
| **Auth (Admin)** | Auth.js v4 Credentials provider (Phase 2) | Single admin account, JWT cookie | ✅ YES |
| **Auth (Client)** | Custom Next.js Middleware + token validation | No session store needed; token in URL | ✅ YES |
| **Token Generation** | nanoid (21 chars) | Cryptographically secure, URL-safe, non-enumerable | ✅ YES |
| **Deployment** | Vercel (Hobby plan) + custom subdomain | Native Next.js; auto-SSL; single deploy command | ✅ YES |

### Data Model — Locked Entities

All tables below **must** exist and maintain these field definitions. Modifications require explicit approval.

```
clients
  id              UUID PK (stable, never changes)
  name            TEXT
  brand_name      TEXT
  brief           TEXT
  token           UUID UNIQUE  ← SEPARATE from PK, rotatable
  accepted_total  NUMERIC      ← denormalized, only price client sees
  created_at      TIMESTAMPTZ

phases
  id              UUID PK
  client_id       UUID FK → clients.id
  title           TEXT
  sort_order      INT
  status          TEXT  (upcoming | active | done)

tasks
  id              UUID PK
  phase_id        UUID FK → phases.id
  title           TEXT
  description     TEXT
  status          TEXT  (todo | in_progress | done)
  sort_order      INT

deliverables
  id              UUID PK
  task_id         UUID FK → tasks.id
  title           TEXT
  url             TEXT
  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 FK → 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 FK → clients.id
  label           TEXT
  url             TEXT  ← external links only, no file uploads
  created_at      TIMESTAMPTZ

notes
  id              UUID PK
  client_id       UUID FK → clients.id
  body            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 FK → clients.id
  service_id      UUID FK → service_catalog.id
  quantity        NUMERIC
  unit_price      NUMERIC
  subtotal        NUMERIC
  ← NEVER exposed via client API
```

### Critical Design Principles — Locked

1. **`clients.token` is NOT the primary key.** Data is keyed by stable UUID `id`. Token is a separate, rotatable field. Rotation is a single UPDATE statement.

2. **Client API never exposes `quote_items`.** Server-side filtering enforces this; not a UI trick. The `accepted_total` field is the only price the client API returns.

3. **`deliverables.approved_at` is immutable.** Once set, it cannot be unset. Provides an audit trail for disputes.

4. **Two independent auth systems:**
   - `/c/[token]/*` → Middleware validates token, 404 on miss
   - `/admin/*` → Auth.js session check (Phase 2)
   - No overlap; no shared session store

5. **No file hosting in v1.** Documents are external URLs only (Google Drive, PDFs, Figma links). File uploads → Phase 3+.

6. **No email in v1.** Deliverables are dashboard links, not email attachments. Email integration → Phase 2+.

### Directory Structure — Locked

```
IAMCAVALLI/
├── src/
│   ├── app/
│   │   ├── c/[token]/
│   │   │   ├── page.tsx          ← Client dashboard route
│   │   │   └── layout.tsx
│   │   ├── admin/                ← Phase 2 (protected by middleware)
│   │   │   ├── page.tsx          ← Admin dashboard
│   │   │   ├── clients/
│   │   │   │   ├── page.tsx
│   │   │   │   └── [id]/
│   │   │   ├── catalog/
│   │   │   └── ...
│   │   ├── layout.tsx
│   │   └── globals.css
│   ├── components/
│   │   ├── ui/                   ← shadcn/ui components
│   │   ├── client-dashboard.tsx
│   │   ├── phase-timeline.tsx
│   │   ├── payment-status.tsx
│   │   ├── documents-section.tsx
│   │   ├── notes-section.tsx
│   │   └── ...
│   ├── db/
│   │   ├── schema.ts             ← Drizzle schema (source of truth)
│   │   ├── migrations/           ← Generated by drizzle-kit
│   │   └── index.ts              ← db client export
│   ├── lib/
│   │   ├── client-view.ts        ← ClientView type + queries
│   │   ├── auth.ts               ← Phase 2: Auth helpers
│   │   └── ...
│   └── middleware.ts             ← Token validation at edge
├── scripts/
│   ├── seed.ts                   ← Insert first test client
│   └── ...
├── .env.local                    ← DATABASE_URL, secrets
├── drizzle.config.ts
├── next.config.ts
├── tailwind.config.ts
├── tsconfig.json
├── package.json
└── .planning/
    ├── ROADMAP.md
    ├── REQUIREMENTS.md
    ├── STATE.md
    └── phases/
        └── 01-foundation-client-dashboard/
            ├── 01-CONTEXT.md
            ├── 01-DISCUSSION-LOG.md
            ├── 01-01-PLAN.md
            ├── 01-02-PLAN.md
            ├── 01-03-PLAN.md
            ├── 01-04-PLAN.md
            ├── 01-05-PLAN.md
            └── SKELETON.md
```

### Deployment — Locked

- **Host:** Vercel (Hobby plan, $0/month for Phase 1 scale)
- **Domain:** welcomeclient.iamcavalli.net (CNAME to Vercel DNS)
- **Database:** Postgres on Coolify (existing Hetzner server, Simone manages)
- **Environment:** DATABASE_URL injected via Vercel Secrets
- **SSL/TLS:** Vercel auto-provisioning for custom domain

### API Routes Structure (Phase 2+)

Routes created in Phase 2 will follow this pattern:

**Client-facing routes** (`/api/c/[token]/...`):
- No authentication library needed
- Middleware validates token
- Routes return ClientView shape only

**Admin routes** (`/api/admin/...`):
- Require Auth.js session
- Access full AdminView including quote_items
- CRUD operations on all entities

### UI Layer Principles — Locked

- **Light & clean visual style:** White backgrounds, strong typography, subtle gray accents
- **Mobile-first design:** Tailwind defaults ensure responsive behavior
- **Semantic HTML:** Proper heading hierarchy, accessible form controls
- **No client-side state management libraries:** Server Components + Server Actions for Phase 1-2
- **Progress visualization:** Global bar (top) + per-phase bars (sections) + task status badges
- **Brand consistency:** iamcavalli logo in corner, client brand_name prominent

### Security Assumptions — Locked

1. **Database credentials are secrets:** DATABASE_URL never logged, committed, or exposed
2. **Tokens are non-enumerable:** 21-character nanoid cannot be guessed
3. **Client API is isolated:** Admin data never leaks to `/c/[token]/*` routes
4. **Admin password** (Phase 2): env var `ADMIN_PASSWORD` protects `/admin/*` before Auth.js is added
5. **No PII in logs:** Payment amounts and tokens never logged to Vercel logs

---

## What This Skeleton Delivers

After Phase 1 execution:

✅ **Functional client portal:**
- One client can open their secret link on any device
- Dashboard shows project phases, tasks, status, payments, documents, decision log
- No login required; link is the secret

✅ **Production-ready infrastructure:**
- Database is live on Coolify Postgres
- Custom domain is verified and HTTPS-enabled
- Application is deployed on Vercel
- One-command deploy pipeline (`git push → Vercel auto-build`)

✅ **Developer-friendly codebase:**
- TypeScript with strict mode
- Drizzle ORM manages schema as code
- Git-tracked migrations (reproducible database state)
- One seed script to populate test data
- No manual SQL; no database browser required

✅ **Foundation for Phase 2:**
- Data model is stable and comprehensive
- Admin CRUD can be built without schema changes
- Auth.js integration point is clear
- Comments and approvals schema already exists

---

## Phase 1 → Phase 2 Contract

Phase 2 will extend this skeleton by:

1. **Admin authentication:** Middleware check + Auth.js session on `/admin/*` routes
2. **CRUD operations:** Forms and API routes to edit clients, phases, tasks, deliverables, payments
3. **Comments & approvals:** Client-facing UI for commenting and approving deliverables
4. **Admin workspace:** Dashboard to manage all clients with state summary and quick actions
5. **Payment management:** Update payment status, send payment reminders

**No schema changes required.** All Phase 2 features fit into the existing data model.

---

## Validation Checklist (End of Phase 1)

- [ ] Next.js 15 application compiles without TypeScript errors
- [ ] Database schema is live on Coolify Postgres (all 11 tables)
- [ ] Middleware validates tokens at edge
- [ ] Client portal route renders complete dashboard with seeded data
- [ ] Seed script inserts test client and prints shareable link
- [ ] DNS CNAME is live: welcomeclient.iamcavalli.net → Vercel
- [ ] Application is deployed on Vercel (accessible via https://welcomeclient.iamcavalli.net/)
- [ ] Invalid tokens return 404 (no information leakage)
- [ ] Payment amounts are NOT visible on client dashboard (only status)
- [ ] Mobile layout is responsive and readable
- [ ] All DASH-01 through DASH-10 requirements are satisfied (except DASH-05, DASH-06 which are Phase 2)

---

## Future Extensibility Notes

This skeleton is designed for:

- **Phase 2:** Admin CRUD + comments + approvals (no schema changes)
- **Phase 3:** Service catalog + quote builder (admin-only, client sees only total)
- **Phase 4 (v2):** Claude AI onboarding flow (optional; may defer indefinitely)
- **Beyond:** Multi-team support, real file uploads, email automation (major schema rework)

The current design is intentionally simple. Future phases should resist scope creep and maintain the "client sees only what they need" principle.

---

**Skeleton locked:** 2026-05-13  
**Next checkpoint:** Phase 2 planning (`/gsd-plan-phase 2`)
