2123dc9d00
- 01-02: wave corrected from 1 to 2 (has depends_on: ["01-01"]) - 01-03: middleware rewritten to Edge-compatible fetch pattern; internal API route app/api/internal/validate-token/route.ts handles DB query in Node.js runtime; tasks/deliverables queries scoped with inArray(); accepted_total null-coalesced - 01-04: Task 1 and Task 6 merged → 5 tasks total (was 6, exceeded threshold) - STATE.md: updated to reflect Phase 1 planning verified, ready for execution Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
15 KiB
15 KiB
phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, must_haves
| phase | plan | type | wave | depends_on | files_modified | autonomous | requirements | must_haves | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 01-foundation-client-dashboard | 02 | execute | 2 |
|
|
true |
|
|
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.
<execution_context> @$HOME/.claude/get-shit-done/workflows/execute-plan.md @$HOME/.claude/get-shit-done/templates/summary.md </execution_context>
@.planning/research/ARCHITECTURE.md @.planning/phases/01-foundation-client-dashboard/01-CONTEXT.md @.planning/phases/01-foundation-client-dashboard/01-01-SUMMARY.md Task 1: Create Drizzle schema definition (src/db/schema.ts) with all 11 tables src/db/schema.ts .planning/research/ARCHITECTURE.md (Data Model section, lines 69-142) 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`
test -f src/db/schema.ts && echo "schema.ts exists"
grep -c "export const" src/db/schema.ts | grep -q "1[1-9]\|2[0-9]" && echo "Multiple table exports found"
grep -q "token.*uuid.*unique" src/db/schema.ts && echo "Token field is separate and unique"
grep -q "approved_at.*timestamp" src/db/schema.ts && echo "approved_at field exists"
grep -q "accepted_total" src/db/schema.ts && echo "accepted_total denormalized field exists"
npm run build 2>&1 | grep -v "warning" | grep -q "error" && echo "TypeScript errors found" || echo "TypeScript compiles"
- `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
Task 2: Create drizzle.config.ts and generate migrations
drizzle.config.ts
src/db/migrations/*
src/db/schema.ts
.env.local
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
test -f drizzle.config.ts && echo "drizzle.config.ts created"
test -d src/db/migrations && ls src/db/migrations/*.sql 2>/dev/null | wc -l | grep -q "[1-9]" && echo "Migration files generated"
grep -l "CREATE TABLE" src/db/migrations/*.sql | wc -l | grep -q "[1-9]" && echo "SQL migration contains CREATE TABLE"
- `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
Task 3: [BLOCKING] Run drizzle-kit push to apply schema to Coolify Postgres
None (schema is pushed to DB, not local files)
.env.local (verify DATABASE_URL is set)
src/db/migrations/ (ensure migrations exist)
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.
if grep -q "^DATABASE_URL=postgresql://" .env.local; then echo "DATABASE_URL is set"; else echo "DATABASE_URL NOT SET"; fi
npx drizzle-kit push 2>&1 | grep -q "successfully\|already\|applied" && echo "Schema push completed"
- 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)
<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>
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<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>