# Pitfalls — ClientHub Freelancer Client Portal

**Domain:** Freelancer client portal with secret-link access, solo developer, Vercel deploy
**Researched:** 2026-05-09
**Confidence:** HIGH

---

## Critical Pitfalls

### 1. Secret Links That Are Guessable or Enumerable

**What goes wrong:** If client tokens are generated from names, sequential IDs, or short strings, they can be enumerated. `/client/mario-rossi` or `/client/3` are not secrets.

**Prevention:**
- Generate tokens as cryptographically random UUIDs (v4) or nanoid (21 chars / ~126 bits of entropy)
- Never derive from name, company, or sequential ID
- Never log or display full tokens in admin analytics

**Warning signs:** Client slug contains the client's name. Token under 20 characters. Link can be reconstructed from info the client already knows.

**Phase mapping:** Phase 1 — data model + routing. Cannot be retrofitted after links are distributed.

---

### 2. Client API Exposes Admin Data (Hidden in UI Only)

**What goes wrong:** Developers fetch all client data and conditionally render fields. A technical client opens DevTools and sees the full quote breakdown — the exact thing the product prevents.

**Prevention:**
- Define two distinct data shapes: `ClientView` and `AdminView`
- Client API routes (`/api/c/[token]/`) return `ClientView` only — enforced server-side, not in the UI
- `accepted_total` goes on the `clients` row. Client API never queries `quote_items`

**Warning signs:** Client API response includes `lineItems` filtered in the frontend. "Client sees only total" enforced with `display: none`.

**Phase mapping:** Data model and API shape — Phase 1.

---

### 3. Data Loss from Vercel Filesystem / In-Memory Storage

**What goes wrong:** Vercel serverless functions are stateless. File writes to local filesystem and in-memory state are lost between invocations. SQLite on disk silently vanishes after cold start. Bug only manifests in production.

**Prevention:**
- External persistent DB from day one: Neon (Postgres free tier)
- Never write to `fs` for persistent data on Vercel
- Validate persistence in the first production deploy

**Warning signs:** Project stores data in a `data/` JSON directory. SQLite without a remote adapter. Data changes in one request not visible in the next.

**Phase mapping:** Day-one infrastructure decision — Phase 1, before any real data is entered.

---

### 4. Over-Engineering Before the First Client Uses It

**What goes wrong:** Building the Claude onboarding flow and quote generator before a single client has opened their dashboard. Real clients still managed via email while the portal is "almost ready."

**Prevention:**
- Hard success criterion for Phase 1: one client link is shareable and works
- Phase 1 ships read-only client dashboard only
- Collect direct feedback from one real client before adding features

**Warning signs:** >2 weeks pass without a shareable client link. Claude integration started before admin edit UI exists.

**Phase mapping:** Enforced by roadmap phase ordering.

---

### 5. Token Is the Primary Key (Unrotatable)

**What goes wrong:** A client forwards their link. The link appears in a screenshot. There is no mechanism to rotate without losing data or breaking bookmarks. Worse if token = primary key: rotation requires a migration.

**Prevention:**
- Data model: stable internal UUID as PK; secret token is a separate, independently updatable field
- Build "Regenerate link" in admin area during Phase 2 — it's a single field UPDATE
- Overwriting the token field is sufficient to invalidate the old link

**Warning signs:** Token is used as PK of the client record. No admin affordance to regenerate a link.

**Phase mapping:** Data model separation — Phase 1. Admin rotation UI — Phase 2.

---

### 6. Client Approval Has No Immutable Record

**What goes wrong:** Client clicks "Approve." Later: "I never approved that." If approval is a boolean with no timestamp, there is no evidence. Weakens your position in commercial disputes.

**Prevention:**
- Store `approved_at` timestamp alongside the approval boolean — from day one
- Display approval timestamp in admin view
- Approvals are immutable for clients — no "undo" button

**Warning signs:** Approval is a boolean column with no timestamp. Client-visible "undo approval" button exists.

**Phase mapping:** Schema — Phase 1. Display in admin — Phase 2.

---

## Moderate Pitfalls

### 7. Payment Status Without a Valid State Machine

Payment has three states (da_saldare / inviata / saldato) for two payments. If transitions are not enforced, the dashboard shows contradictory states.

**Prevention:** Enforce valid transitions: `da_saldare → inviata → saldato`. Admin UI offers only valid next states.

**Phase mapping:** Admin UI — Phase 2.

---

### 8. Google Drive Links That Rot

Document links break when sharing settings change or the Drive account changes. Client sees a broken link to their own deliverable.

**Prevention:** Store a display name alongside the URL so broken links are visible in admin. Build a simple "update link" affordance in Phase 2.

---

### 9. Admin Area With No Real Access Control

`/admin` is a secret route with no authentication. A client who guesses the URL accesses all client data.

**Prevention:** Add Next.js middleware check against `ADMIN_PASSWORD` env variable before Phase 2 ships. Never rely on security through obscurity for a route that contains all client data.

**Phase mapping:** Phase 2, before admin area contains real data.

---

### 10. Comments Scope Creeping Into Threading

"Clients can leave comments" becomes complex when replies, read/unread state, and notifications are added. Can double Phase 1 scope.

**Prevention:** Phase 1 ships comments as a flat append-only list per task. No threading, no replies, no email notifications.

---

## Minor Pitfalls

### 11. DNS Configuration as a Last-Minute Task

`welcomeclient.iamcavalli.net` requires a CNAME to Vercel's DNS. Propagation takes minutes to hours. Doing this the day of a client demo misses the deadline.

**Prevention:** Configure DNS in Phase 1 before the UI is complete. Verify propagation independently.

---

### 12. Mobile Responsiveness as an Afterthought

Clients open the link on their phone from a shared message. A broken mobile layout is the first impression.

**Prevention:** Use Tailwind mobile-first defaults from the start. Test on a real phone before any link is sent.

---

### 13. No Empty States

A new client record with no tasks shows a blank page. The client assumes something is broken.

**Prevention:** Design minimal empty states for no-tasks, no-documents, no-comments.

---

## Phase-Specific Warnings Summary

| Phase Topic | Likely Pitfall | Mitigation |
|---|---|---|
| Client token generation | Guessable slug from name | Crypto-random UUID/nanoid, never name-derived |
| Client-facing API | Admin data in JSON response | `ClientView` type enforced server-side |
| Storage choice | Vercel filesystem not persistent | External DB (Neon) before first data write |
| Admin area access | No real auth | Middleware check before Phase 2 ships |
| Approval recording | Boolean-only, no audit trail | Store `approved_at` from day one |
| Token in data model | Token = PK, unrotatable | Separate stable ID from rotatable token field |
| Phase ordering | Claude flow before dashboard | Enforce: client view → admin edit → Claude |
| Comments | Threading scope creep | Flat list in Phase 1 |
| DNS | Last-minute failure | Configure and verify in Phase 1 |
