---
phase: "01-foundation-client-dashboard"
plan: 02
type: execute
wave: 2
depends_on:
  - "01-01"
files_modified:
  - src/db/schema.ts
  - drizzle.config.ts
  - .env.local
autonomous: true
requirements:
  - DASH-01
  - DASH-02
  - DASH-03
  - DASH-04

must_haves:
  truths:
    - "Drizzle schema is complete and matches the data model from ARCHITECTURE.md"
    - "All 11 tables are defined: clients, phases, tasks, deliverables, comments, payments, documents, notes, service_catalog, quote_items"
    - "Token field on clients is a separate UUID, not the primary key"
    - "approved_at on deliverables is TIMESTAMPTZ"
    - "drizzle-kit push has been run and database schema is live"
    - "TypeScript types are exported from schema.ts for use in API routes"
  artifacts:
    - path: "src/db/schema.ts"
      provides: "Complete Drizzle ORM schema definition for all entities"
      min_lines: 200
      contains: "export const clients = pgTable"
    - path: "drizzle.config.ts"
      provides: "Drizzle Kit configuration pointing to src/db/schema.ts"
      contains: "schema:"
    - path: "src/db/migrations/"
      provides: "Migration files generated by drizzle-kit"
      min_files: 1
  key_links:
    - from: "src/db/schema.ts"
      to: "clients table"
      via: "pgTable definition"
      pattern: "export const clients.*pgTable"
    - from: "src/db/schema.ts"
      to: "token field"
      via: "uuid().unique()"
      pattern: "token.*uuid.*unique"
    - from: "drizzle-kit push"
      to: "Postgres on Coolify"
      via: "DATABASE_URL"
      pattern: "DATABASE_URL"

---

<objective>
**Database Schema + Drizzle Migrations:** Define the complete data model in Drizzle ORM, generate database migrations, and push the schema to Coolify Postgres. This plan creates the schema that all subsequent plans depend on.

Purpose: Establish the single source of truth for data shape. Enforces critical decisions: token as separate field, accepted_total denormalized, approved_at immutable, ClientView vs. AdminView separation in queries.

Output: `src/db/schema.ts` with all 11 tables fully defined, migration files, and Postgres schema live on Coolify.
</objective>

<execution_context>
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
@$HOME/.claude/get-shit-done/templates/summary.md
</execution_context>

<context>
@.planning/research/ARCHITECTURE.md
@.planning/phases/01-foundation-client-dashboard/01-CONTEXT.md
@.planning/phases/01-foundation-client-dashboard/01-01-SUMMARY.md
</context>

<tasks>

<task type="auto">
  <name>Task 1: Create Drizzle schema definition (src/db/schema.ts) with all 11 tables</name>
  <files>
    src/db/schema.ts
  </files>
  <read_first>
    .planning/research/ARCHITECTURE.md (Data Model section, lines 69-142)
  </read_first>
  <action>
    Create `src/db/schema.ts` with the following tables (exact order, exact field names):
    
    ```typescript
    import { pgTable, text, uuid, integer, numeric, timestamp, boolean, unique, index } from 'drizzle-orm/pg-core';
    import { relations } from 'drizzle-orm';
    import { nanoid } from 'nanoid';
    
    // ============ CLIENTS ============
    export const clients = pgTable('clients', {
      id: uuid('id').primaryKey().defaultValue(nanoid()),
      name: text('name').notNull(),
      brand_name: text('brand_name').notNull(),
      brief: text('brief').notNull(),
      token: uuid('token').notNull().unique().defaultValue(nanoid()),
      accepted_total: numeric('accepted_total', { precision: 10, scale: 2 }).default('0'),
      created_at: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
    });
    
    // ============ PHASES ============
    export const phases = pgTable('phases', {
      id: uuid('id').primaryKey().defaultValue(nanoid()),
      client_id: uuid('client_id').notNull().references(() => clients.id, { onDelete: 'cascade' }),
      title: text('title').notNull(),
      sort_order: integer('sort_order').notNull().default(0),
      status: text('status').notNull().default('upcoming'), // upcoming | active | done
    });
    
    // ============ TASKS ============
    export const tasks = pgTable('tasks', {
      id: uuid('id').primaryKey().defaultValue(nanoid()),
      phase_id: uuid('phase_id').notNull().references(() => phases.id, { onDelete: 'cascade' }),
      title: text('title').notNull(),
      description: text('description'),
      status: text('status').notNull().default('todo'), // todo | in_progress | done
      sort_order: integer('sort_order').notNull().default(0),
    });
    
    // ============ DELIVERABLES ============
    export const deliverables = pgTable('deliverables', {
      id: uuid('id').primaryKey().defaultValue(nanoid()),
      task_id: uuid('task_id').notNull().references(() => tasks.id, { onDelete: 'cascade' }),
      title: text('title').notNull(),
      url: text('url'),
      status: text('status').notNull().default('pending'), // pending | submitted | approved
      approved_at: timestamp('approved_at', { withTimezone: true }), // immutable audit trail
    });
    
    // ============ COMMENTS ============
    export const comments = pgTable('comments', {
      id: uuid('id').primaryKey().defaultValue(nanoid()),
      entity_type: text('entity_type').notNull(), // task | deliverable
      entity_id: uuid('entity_id').notNull(),
      author: text('author').notNull(), // client | admin
      body: text('body').notNull(),
      created_at: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
    });
    
    // ============ PAYMENTS ============
    export const payments = pgTable('payments', {
      id: uuid('id').primaryKey().defaultValue(nanoid()),
      client_id: uuid('client_id').notNull().references(() => clients.id, { onDelete: 'cascade' }),
      label: text('label').notNull(), // "Acconto 50%" | "Saldo 50%"
      amount: numeric('amount', { precision: 10, scale: 2 }).notNull(),
      status: text('status').notNull().default('da_saldare'), // da_saldare | inviata | saldato
      paid_at: timestamp('paid_at', { withTimezone: true }),
    });
    
    // ============ DOCUMENTS ============
    export const documents = pgTable('documents', {
      id: uuid('id').primaryKey().defaultValue(nanoid()),
      client_id: uuid('client_id').notNull().references(() => clients.id, { onDelete: 'cascade' }),
      label: text('label').notNull(),
      url: text('url').notNull(),
      created_at: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
    });
    
    // ============ NOTES (Decision Log) ============
    export const notes = pgTable('notes', {
      id: uuid('id').primaryKey().defaultValue(nanoid()),
      client_id: uuid('client_id').notNull().references(() => clients.id, { onDelete: 'cascade' }),
      body: text('body').notNull(),
      created_at: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
    });
    
    // ============ SERVICE CATALOG ============
    export const service_catalog = pgTable('service_catalog', {
      id: uuid('id').primaryKey().defaultValue(nanoid()),
      name: text('name').notNull(),
      description: text('description'),
      unit_price: numeric('unit_price', { precision: 10, scale: 2 }).notNull(),
      active: boolean('active').notNull().default(true),
    });
    
    // ============ QUOTE ITEMS ============
    export const quote_items = pgTable('quote_items', {
      id: uuid('id').primaryKey().defaultValue(nanoid()),
      client_id: uuid('client_id').notNull().references(() => clients.id, { onDelete: 'cascade' }),
      service_id: uuid('service_id').notNull().references(() => service_catalog.id, { onDelete: 'restrict' }),
      quantity: numeric('quantity', { precision: 10, scale: 2 }).notNull(),
      unit_price: numeric('unit_price', { precision: 10, scale: 2 }).notNull(),
      subtotal: numeric('subtotal', { precision: 10, scale: 2 }).notNull(),
    });
    
    // ============ RELATIONS ============
    export const clientsRelations = relations(clients, ({ many }) => ({
      phases: many(phases),
      payments: many(payments),
      documents: many(documents),
      notes: many(notes),
      quote_items: many(quote_items),
    }));
    
    export const phasesRelations = relations(phases, ({ one, many }) => ({
      client: one(clients, { fields: [phases.client_id], references: [clients.id] }),
      tasks: many(tasks),
    }));
    
    export const tasksRelations = relations(tasks, ({ one, many }) => ({
      phase: one(phases, { fields: [tasks.phase_id], references: [phases.id] }),
      deliverables: many(deliverables),
    }));
    
    export const deliverablesRelations = relations(deliverables, ({ one }) => ({
      task: one(tasks, { fields: [deliverables.task_id], references: [tasks.id] }),
    }));
    ```
    
    Notes:
    - Use `nanoid()` for all UUID primary keys (not SQL-generated UUIDs) — this ensures consistent, cryptographically secure IDs
    - Token is `uuid().notNull().unique()` — separate from id, rotatable
    - `approved_at` is nullable (no approval initially)
    - Relations use cascading deletes for data integrity
    - All timestamp fields use `withTimezone: true`
  </action>
  <verify>
    <automated>test -f src/db/schema.ts && echo "schema.ts exists"</automated>
    <automated>grep -c "export const" src/db/schema.ts | grep -q "1[1-9]\|2[0-9]" && echo "Multiple table exports found"</automated>
    <automated>grep -q "token.*uuid.*unique" src/db/schema.ts && echo "Token field is separate and unique"</automated>
    <automated>grep -q "approved_at.*timestamp" src/db/schema.ts && echo "approved_at field exists"</automated>
    <automated>grep -q "accepted_total" src/db/schema.ts && echo "accepted_total denormalized field exists"</automated>
    <automated>npm run build 2>&1 | grep -v "warning" | grep -q "error" && echo "TypeScript errors found" || echo "TypeScript compiles"</automated>
  </verify>
  <acceptance_criteria>
    - `src/db/schema.ts` exists with all 11 tables defined
    - All table exports are present: clients, phases, tasks, deliverables, comments, payments, documents, notes, service_catalog, quote_items
    - Token field is separate from id PK and marked as unique
    - Relations are defined for all foreign keys
    - TypeScript compiles without errors
  </acceptance_criteria>
</task>

<task type="auto">
  <name>Task 2: Create drizzle.config.ts and generate migrations</name>
  <files>
    drizzle.config.ts
    src/db/migrations/*
  </files>
  <read_first>
    src/db/schema.ts
    .env.local
  </read_first>
  <action>
    Create `drizzle.config.ts` in project root:
    
    ```typescript
    import type { Config } from 'drizzle-kit';
    
    export default {
      schema: './src/db/schema.ts',
      out: './src/db/migrations',
      driver: 'pg',
      dbCredentials: {
        connectionString: process.env.DATABASE_URL!,
      },
    } satisfies Config;
    ```
    
    Run migration generation:
    ```
    npx drizzle-kit generate
    ```
    
    This creates `src/db/migrations/` directory with a numbered migration file (e.g., `0000_initial_schema.sql`).
    
    Verify the generated SQL contains:
    - All 11 CREATE TABLE statements
    - Foreign key constraints
    - Unique constraints on token
  </action>
  <verify>
    <automated>test -f drizzle.config.ts && echo "drizzle.config.ts created"</automated>
    <automated>test -d src/db/migrations && ls src/db/migrations/*.sql 2>/dev/null | wc -l | grep -q "[1-9]" && echo "Migration files generated"</automated>
    <automated>grep -l "CREATE TABLE" src/db/migrations/*.sql | wc -l | grep -q "[1-9]" && echo "SQL migration contains CREATE TABLE"</automated>
  </verify>
  <acceptance_criteria>
    - `drizzle.config.ts` exists with correct driver (pg) and schema path
    - `src/db/migrations/` directory exists with at least one .sql file
    - Generated SQL file contains CREATE TABLE statements for all 11 tables
  </acceptance_criteria>
</task>

<task type="auto" gate="blocking">
  <name>Task 3: [BLOCKING] Run drizzle-kit push to apply schema to Coolify Postgres</name>
  <files>
    None (schema is pushed to DB, not local files)
  </files>
  <read_first>
    .env.local (verify DATABASE_URL is set)
    src/db/migrations/ (ensure migrations exist)
  </read_first>
  <action>
    Before running push, verify DATABASE_URL is set in .env.local:
    ```
    cat .env.local | grep DATABASE_URL
    ```
    
    If DATABASE_URL is not yet available (Coolify not configured), STOP here and ask executor to provide Coolify credentials. This task cannot proceed without a valid connection string.
    
    Once DATABASE_URL is confirmed:
    ```
    npx drizzle-kit push
    ```
    
    Drizzle will connect to the database and apply all migrations.
    
    If push succeeds, you will see:
    ```
    ✓ All migrations have been successfully applied
    ```
    
    If the database schema was already created, drizzle-kit will detect it and skip unchanged tables.
  </action>
  <verify>
    <automated>if grep -q "^DATABASE_URL=postgresql://" .env.local; then echo "DATABASE_URL is set"; else echo "DATABASE_URL NOT SET"; fi</automated>
    <automated>npx drizzle-kit push 2>&1 | grep -q "successfully\|already\|applied" && echo "Schema push completed"</automated>
  </verify>
  <acceptance_criteria>
    - DATABASE_URL env var is set in .env.local
    - `npx drizzle-kit push` runs without connection errors
    - Schema is created in Coolify Postgres (all 11 tables exist)
    - Executor can confirm with: `npx drizzle-kit introspect` (shows all tables)
  </acceptance_criteria>
</task>

</tasks>

<threat_model>
## Trust Boundaries

| Boundary | Description |
|----------|-------------|
| Migration files → Database | Schema migrations are deployed via drizzle-kit push; any schema change is version-controlled |
| Schema definition → ORM runtime | TypeScript schema is the source of truth; Drizzle generates types from schema, not from introspection |
| Token field → Access control | Token is marked unique and separate from PK; enforced by DB constraints |

## STRIDE Threat Register

| Threat ID | Category | Component | Disposition | Mitigation Plan |
|-----------|----------|-----------|-------------|-----------------|
| T-02-001 | Tampering | Token field uniqueness | mitigate | Database enforces UNIQUE constraint on token field; no client can have duplicate token |
| T-02-002 | Information Disclosure | Schema version history | accept | Migrations are version-controlled in git; leaking migration files does not expose secrets (passwords in .env.local only) |
| T-02-003 | Denial of Service | quote_items table | accept | Admin-only; client API never queries it; no data loss from client-side DOS attacks |

</threat_model>

<verification>
After plan execution:
1. Run `npx drizzle-kit push` → "successfully applied" message
2. Run `npx drizzle-kit introspect` → lists all 11 tables
3. Check `src/db/migrations/` → at least one .sql file exists
4. Check `src/db/schema.ts` → all tables are exported
5. Verify TypeScript: `npm run build` → no errors
</verification>

<success_criteria>
- Drizzle schema is defined and exported from `src/db/schema.ts`
- All 11 tables are created in Coolify Postgres
- Token field is unique and separate from id
- Migrations are version-controlled in git
- TypeScript types are available for import in API routes
- Ready to proceed to Plan 03 (Middleware + Client Portal route)
</success_criteria>

<output>
After completion, create `.planning/phases/01-foundation-client-dashboard/01-02-SUMMARY.md`
</output>
