---
phase: 01-foundation-client-dashboard
plan: 02
subsystem: database
tags: [drizzle-orm, postgres, schema, migrations, nanoid]

# Dependency graph
requires:
  - 01-01 (drizzle-kit, postgres-js driver, DATABASE_URL in .env.local)
provides:
  - src/db/schema.ts con 10 tabelle complete
  - TypeScript types esportati per tutte le entità (Client, Phase, Task, ecc.)
  - Migration file SQL in src/db/migrations/
  - Schema live su Postgres 16 (Hetzner/Coolify)
affects:
  - 01-03-client-route (usa clients, phases, tasks, deliverables, payments, documents, notes)
  - 01-04-dashboard-ui (usa tutti i types esportati)
  - 01-05-seed-deploy (inserisce dati con i types NewClient, NewPhase, ecc.)

# Tech tracking
tech-stack:
  added: []
  patterns:
    - "ID strategy: text + nanoid() via $defaultFn (non uuid() nativo Postgres) — nanoid genera stringhe 21-char URL-safe, non UUID formato xxxxxxxx-xxxx-xxxx"
    - "drizzle-kit push richiede DATABASE_URL passata esplicitamente come env var (non carica .env.local automaticamente)"
    - "Relations Drizzle definite per tutti gli FK — usabili in query con with: { ... }"

key-files:
  created:
    - src/db/schema.ts (245 righe — 10 tabelle + relations + TypeScript types)
    - src/db/migrations/0000_pretty_typhoid_mary.sql (migration SQL completa)
    - src/db/migrations/meta/ (drizzle-kit metadata)
    - src/db/migrations/relations.ts (relazioni per introspect)
    - src/db/migrations/schema.ts (schema per introspect)
  modified: []

key-decisions:
  - "Usato text + $defaultFn(() => nanoid()) invece di uuid().defaultRandom() — nanoid genera ID URL-safe crittograficamente sicuri (21 char, ~126 bit entropia), non UUID formato PostgreSQL"
  - "drizzle.config.ts dal Plan 01 già corretto (defineConfig + dialect postgresql + url:) — nessuna modifica necessaria"
  - "clients.token: text notNull unique con nanoid — separato dall'id PK, rotabile con single UPDATE"
  - "drizzle-kit push richiede DATABASE_URL come env var esplicita (non auto-load .env.local)"

# Metrics
duration: 15min
completed: 2026-05-13
---

# Phase 1 Plan 02: Drizzle Schema + Migration — 10 tabelle live su Postgres

**Schema Drizzle ORM completo con 10 tabelle, migration SQL generata e schema live sul database Postgres 16 (Hetzner/Coolify). TypeScript strict compila senza errori.**

## Performance

- **Duration:** ~15 min
- **Started:** 2026-05-13T20:21:00Z
- **Completed:** 2026-05-13T20:36:00Z
- **Tasks:** 3/3
- **Files modified:** 6

## Accomplishments

- `src/db/schema.ts` creato con 10 tabelle complete + relations Drizzle + TypeScript types esportati
- Vincoli architetturali LOCKED rispettati: `clients.token` separato dall'id PK (unique, notNull, nanoid), `accepted_total` denormalizzato, `approved_at` nullable (audit trail immutabile), `quote_items` mai esposto al client API
- Migration SQL (`0000_pretty_typhoid_mary.sql`) generata con tutti i `CREATE TABLE` e FK constraints
- `npx drizzle-kit push` eseguito con successo — tutte e 10 le tabelle create su `postgresql://178.104.27.55:5432/clienthub`
- Verifica via `information_schema.tables`: clients, comments, deliverables, documents, notes, payments, phases, quote_items, service_catalog, tasks

## Task Commits

1. **Task 1: Drizzle schema (src/db/schema.ts)** - `1bdbe7a` (feat)
2. **Task 2: Migration generation (drizzle-kit generate)** - `a6ec599` (chore)
3. **Task 3: [BLOCKING] drizzle-kit push → Postgres live** - `abcbb52` (feat)

## Files Created/Modified

- `src/db/schema.ts` — 10 tabelle: clients (token separato + accepted_total), phases, tasks, deliverables (approved_at nullable), comments (polimorfici), payments (da_saldare/inviata/saldato), documents, notes, service_catalog, quote_items
- `src/db/migrations/0000_pretty_typhoid_mary.sql` — Migration SQL completa con CREATE TABLE + FK + UNIQUE constraint su token
- `src/db/migrations/meta/` — Drizzle-kit metadata (snapshot JSON)
- `src/db/migrations/relations.ts` — Relations per introspect
- `src/db/migrations/schema.ts` — Schema per introspect

## Decisions Made

- **ID strategy:** `text + $defaultFn(() => nanoid())` invece di `uuid().defaultRandom()`. La colonna Drizzle `uuid()` si aspetta il formato PostgreSQL `xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx`, mentre `nanoid()` genera stringhe 21-char URL-safe. Usare `text` è corretto e allineato con la decisione architetturale di token crittograficamente sicuro.
- **drizzle.config.ts invariato:** La versione dal Plan 01 usa già `defineConfig`, `dialect: "postgresql"` e `url:` (sintassi aggiornata drizzle-kit v0.31) — nessuna modifica necessaria rispetto alla versione suggerita nel piano (che usava l'API obsoleta `driver: 'pg'`).

## Deviations from Plan

### Auto-fixed Issues

**1. [Rule 1 - Bug] uuid() non compatibile con nanoid() come defaultFn**
- **Found during:** Task 1
- **Issue:** Il piano suggeriva `uuid('id').primaryKey().defaultValue(nanoid())` ma Drizzle `uuid()` si aspetta UUID nel formato `xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx`. nanoid() genera stringhe come `Tcyf3muFXVOX9QO9pBUES` (21 char, non UUID validi). Usare `defaultValue(nanoid())` su una colonna `uuid()` avrebbe causato errori a runtime al primo INSERT.
- **Fix:** Cambiato a `text('id').primaryKey().$defaultFn(() => nanoid())` per tutte le PK e per il campo `token`. Semantica identica (ID crittograficamente sicuro), tipo colonna SQL `text` invece di `uuid`.
- **Files modified:** src/db/schema.ts
- **Commit:** 1bdbe7a

**2. [Rule 1 - Bug] drizzle.config.ts dal piano usa API obsoleta**
- **Found during:** Task 2
- **Issue:** Il piano suggeriva `driver: 'pg'` e `dbCredentials: { connectionString: ... }` — sintassi drizzle-kit <0.30. Il file esistente usa già `defineConfig` con `dialect: "postgresql"` e `dbCredentials: { url: ... }` — sintassi corretta per drizzle-kit 0.31.
- **Fix:** Mantenuto il file esistente senza modifiche (era già corretto).
- **Files modified:** nessuno
- **Commit:** nessuno necessario

**3. [Rule 3 - Blocking] drizzle-kit push non carica .env.local automaticamente**
- **Found during:** Task 3
- **Issue:** `npx drizzle-kit push` fallisce con "connection url required" perché drizzle-kit non carica `.env.local` automaticamente (solo `.env`).
- **Fix:** Passato `DATABASE_URL` esplicitamente come variabile d'ambiente al comando: `DATABASE_URL="..." npx drizzle-kit push`.
- **Files modified:** nessuno (solo comando di esecuzione)
- **Commit:** abcbb52

## Known Stubs

Nessuno. Il piano è infrastrutturale (schema + DB) — nessun componente UI o dato presentato al cliente. Le tabelle sono vuote, ma questo è intenzionale: il seed script è previsto nel Plan 05.

## Threat Surface Scan

Il threat model T-02-001 (unicità token) è mitigato: `CONSTRAINT "clients_token_unique" UNIQUE("token")` è attivo nel database. T-02-002 e T-02-003 sono accettati come da piano.

Nessuna nuova superficie di sicurezza non prevista dal threat model.

## Self-Check

- [x] `src/db/schema.ts` esiste (245 righe, 10 tabelle pgTable + relations + types)
- [x] `src/db/migrations/0000_pretty_typhoid_mary.sql` esiste con 10 CREATE TABLE
- [x] Commit `1bdbe7a` esiste (schema)
- [x] Commit `a6ec599` esiste (migrations)
- [x] Commit `abcbb52` esiste (push)
- [x] 10 tabelle verificate live su Postgres via `information_schema.tables`
- [x] `clients.token` è `text NOT NULL UNIQUE` con nanoid — separato dalla PK
- [x] `approved_at` è `timestamp with time zone` nullable
- [x] TypeScript strict: `npm run build` — zero errori TypeScript

## Self-Check: PASSED

## Next Phase Readiness

- Plan 03 (Middleware + route `/c/[token]`) può partire — lo schema è live e i types sono importabili
- Import pattern: `import { clients, phases, tasks, ... } from '@/db/schema'`
- Import types: `import type { Client, Phase, Task, ... } from '@/db/schema'`

---
*Phase: 01-foundation-client-dashboard*
*Completed: 2026-05-13*