--- 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*