# Monorepo Foundation Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Scaffold the complete Insignia TOWER monorepo with a working NestJS API, Next.js 15 web app, worker shell, all shared packages, the full Prisma schema, and a Docker Compose dev stack — everything green before the first feature lands. **Architecture:** Turborepo monorepo with pnpm workspaces. Three apps (`api`, `web`, `worker`) share typed packages (`@tower/types`, `@tower/config`, `@tower/logger`, `@tower/ui`, `@tower/sdk`). PostgreSQL + Redis + Meilisearch run in Docker locally. Prisma manages all schema migrations from inside `apps/api`. **Tech Stack:** pnpm 10+, Turborepo 2, NestJS 11, Next.js 16, Tailwind CSS 4, shadcn/ui, Prisma 6, PostgreSQL 17, Redis 7, Meilisearch 1.11, BullMQ 5, Zod 3, pino 9, Jest 29, TypeScript 5.7 --- ## File Map All files created from scratch (empty repo). ``` tower/ ├── package.json # root workspace ├── pnpm-workspace.yaml ├── turbo.json ├── tsconfig.base.json # shared TS base ├── .gitignore ├── .prettierrc ├── .env.example ├── docker-compose.yml │ ├── packages/ │ ├── types/ │ │ ├── package.json │ │ ├── tsconfig.json │ │ └── src/ │ │ ├── index.ts │ │ └── message.ts # CanonicalMessage, Group, Platform types │ ├── config/ │ │ ├── package.json │ │ ├── tsconfig.json │ │ ├── jest.config.js │ │ └── src/ │ │ ├── index.ts # validateEnv() │ │ └── index.test.ts # env validation tests │ ├── logger/ │ │ ├── package.json │ │ ├── tsconfig.json │ │ └── src/ │ │ └── index.ts # createLogger() │ ├── ui/ │ │ ├── package.json │ │ ├── tsconfig.json │ │ └── src/ │ │ └── index.ts # re-export shell │ └── sdk/ │ ├── package.json │ ├── tsconfig.json │ └── src/ │ └── index.ts # re-export shell │ ├── apps/ │ ├── api/ │ │ ├── package.json │ │ ├── tsconfig.json │ │ ├── nest-cli.json │ │ ├── jest.config.js │ │ ├── prisma/ │ │ │ └── schema.prisma # all Phase 1 tables │ │ └── src/ │ │ ├── main.ts │ │ ├── app.module.ts │ │ ├── prisma/ │ │ │ ├── prisma.module.ts │ │ │ ├── prisma.service.ts │ │ │ └── prisma.service.spec.ts # DB integration test │ │ └── modules/ │ │ └── health/ │ │ ├── health.module.ts │ │ ├── health.controller.ts │ │ └── health.controller.spec.ts │ ├── web/ │ │ ├── package.json │ │ ├── tsconfig.json │ │ ├── next.config.ts │ │ ├── postcss.config.js │ │ ├── jest.config.js │ │ ├── jest.setup.ts │ │ └── app/ │ │ ├── globals.css │ │ ├── layout.tsx │ │ ├── page.tsx │ │ └── page.test.tsx # render test │ └── worker/ │ ├── package.json │ ├── tsconfig.json │ └── src/ │ └── main.ts ``` --- ### Task 1: Root Monorepo Scaffold **Files:** - Create: `package.json` - Create: `pnpm-workspace.yaml` - Create: `turbo.json` - Create: `tsconfig.base.json` - Create: `.gitignore` - Create: `.prettierrc` - [ ] **Step 1: Verify pnpm and Node versions meet requirements** ```bash node --version # must be >= 20 pnpm --version # must be >= 9 ``` Expected: both print version numbers without errors. - [ ] **Step 2: Initialise git and write root `package.json`** ```bash git init ``` Create `package.json`: ```json { "name": "tower", "private": true, "scripts": { "build": "turbo build", "dev": "turbo dev", "test": "turbo test", "lint": "turbo lint" }, "devDependencies": { "turbo": "^2.0.0", "typescript": "^5.7.0", "@types/node": "^22.0.0", "prettier": "^3.0.0" }, "engines": { "node": ">=22.0.0", "pnpm": ">=10.0.0" } } ``` - [ ] **Step 3: Write workspace and Turborepo config** Create `pnpm-workspace.yaml`: ```yaml packages: - 'apps/*' - 'packages/*' ``` Create `turbo.json`: ```json { "$schema": "https://turbo.build/schema.json", "tasks": { "build": { "dependsOn": ["^build"], "outputs": ["dist/**", ".next/**"] }, "dev": { "cache": false, "persistent": true }, "test": { "dependsOn": ["^build"], "outputs": ["coverage/**"] }, "lint": {} } } ``` - [ ] **Step 4: Write shared TypeScript base and tooling configs** Create `tsconfig.base.json`: ```json { "compilerOptions": { "target": "ES2021", "module": "commonjs", "lib": ["ES2021"], "strict": true, "skipLibCheck": true, "declaration": true, "declarationMap": true, "sourceMap": true, "resolveJsonModule": true, "esModuleInterop": true } } ``` Create `.prettierrc`: ```json { "semi": true, "singleQuote": true, "trailingComma": "all", "printWidth": 100 } ``` Create `.gitignore`: ``` node_modules dist .next .turbo coverage .env *.env.local ``` - [ ] **Step 5: Install root dependencies and commit** ```bash pnpm install ``` Expected: `node_modules/.modules.yaml` created, no errors. ```bash git add package.json pnpm-workspace.yaml turbo.json tsconfig.base.json .gitignore .prettierrc pnpm-lock.yaml git commit -m "chore: initialise monorepo scaffold" ``` --- ### Task 2: `@tower/types` Package **Files:** - Create: `packages/types/package.json` - Create: `packages/types/tsconfig.json` - Create: `packages/types/src/message.ts` - Create: `packages/types/src/index.ts` - [ ] **Step 1: Write the type definitions** Create `packages/types/src/message.ts`: ```typescript export type Platform = 'whatsapp' | 'telegram' | 'discord'; export type MessageStatus = | 'PENDING' | 'APPROVED' | 'REJECTED' | 'DISTRIBUTED' | 'ARCHIVED'; export type ApprovalDecision = 'APPROVED' | 'REJECTED'; export interface CanonicalMessage { messageId: string; platform: Platform; platformMsgId: string; sourceGroupId: string; senderJid: string; senderName?: string; content: string; mediaUrl?: string; tags: string[]; status: MessageStatus; createdAt: Date; } export interface Group { id: string; platform: Platform; platformId: string; name: string; description?: string; isActive: boolean; } export interface SyncRoute { id: string; sourceGroupId: string; targetGroupId: string; isActive: boolean; } ``` Create `packages/types/src/index.ts`: ```typescript export * from './message'; ``` - [ ] **Step 2: Write package config** Create `packages/types/package.json`: ```json { "name": "@tower/types", "version": "0.0.1", "main": "./dist/index.js", "types": "./dist/index.d.ts", "exports": { ".": { "types": "./dist/index.d.ts", "default": "./dist/index.js" } }, "scripts": { "build": "tsc", "dev": "tsc --watch" }, "devDependencies": { "typescript": "^5.7.0" } } ``` Create `packages/types/tsconfig.json`: ```json { "extends": "../../tsconfig.base.json", "compilerOptions": { "outDir": "./dist", "rootDir": "./src" }, "include": ["src"] } ``` - [ ] **Step 3: Build and verify TypeScript compiles** ```bash pnpm --filter @tower/types build ``` Expected: `packages/types/dist/` created, `index.js` and `index.d.ts` present, no TS errors. - [ ] **Step 4: Commit** ```bash git add packages/types git commit -m "feat: add @tower/types shared package" ``` --- ### Task 3: `@tower/config` Package (TDD) **Files:** - Create: `packages/config/package.json` - Create: `packages/config/tsconfig.json` - Create: `packages/config/jest.config.js` - Create: `packages/config/src/index.ts` - Create: `packages/config/src/index.test.ts` - [ ] **Step 1: Write the failing tests first** Create `packages/config/src/index.test.ts`: ```typescript import { validateEnv } from './index'; const validEnv = { NODE_ENV: 'development', DATABASE_URL: 'postgresql://tower:tower_dev@localhost:5432/tower_dev', REDIS_URL: 'redis://localhost:6379', JWT_SECRET: 'a_super_secret_key_that_is_at_least_32_chars_long', } as unknown as NodeJS.ProcessEnv; describe('validateEnv', () => { it('returns parsed config for valid env', () => { const result = validateEnv(validEnv); expect(result.NODE_ENV).toBe('development'); expect(result.API_PORT).toBe(3001); }); it('applies default API_PORT of 3001 when not set', () => { const result = validateEnv(validEnv); expect(result.API_PORT).toBe(3001); }); it('throws when DATABASE_URL is missing', () => { const { DATABASE_URL, ...withoutDb } = validEnv as Record; expect(() => validateEnv(withoutDb as unknown as NodeJS.ProcessEnv), ).toThrow('Invalid environment variables'); }); it('throws when JWT_SECRET is shorter than 32 chars', () => { expect(() => validateEnv({ ...validEnv, JWT_SECRET: 'tooshort' } as unknown as NodeJS.ProcessEnv), ).toThrow('Invalid environment variables'); }); it('throws when DATABASE_URL is not a valid URL', () => { expect(() => validateEnv({ ...validEnv, DATABASE_URL: 'not-a-url' } as unknown as NodeJS.ProcessEnv), ).toThrow('Invalid environment variables'); }); }); ``` - [ ] **Step 2: Write package config and run the failing tests** Create `packages/config/package.json`: ```json { "name": "@tower/config", "version": "0.0.1", "main": "./dist/index.js", "types": "./dist/index.d.ts", "exports": { ".": { "types": "./dist/index.d.ts", "default": "./dist/index.js" } }, "scripts": { "build": "tsc", "dev": "tsc --watch", "test": "jest" }, "dependencies": { "zod": "^3.23.0" }, "devDependencies": { "@types/jest": "^29.0.0", "jest": "^29.0.0", "ts-jest": "^29.0.0", "typescript": "^5.7.0" } } ``` Create `packages/config/tsconfig.json`: ```json { "extends": "../../tsconfig.base.json", "compilerOptions": { "outDir": "./dist", "rootDir": "./src" }, "include": ["src"] } ``` Create `packages/config/jest.config.js`: ```javascript module.exports = { preset: 'ts-jest', testEnvironment: 'node', testMatch: ['**/*.test.ts'], }; ``` ```bash pnpm --filter @tower/config install pnpm --filter @tower/config test ``` Expected: **5 tests FAIL** — `Cannot find module './index'`. - [ ] **Step 3: Write the implementation** Create `packages/config/src/index.ts`: ```typescript import { z } from 'zod'; const envSchema = z.object({ NODE_ENV: z.enum(['development', 'test', 'production']).default('development'), DATABASE_URL: z.string().url(), REDIS_URL: z.string().url(), API_PORT: z.coerce.number().default(3001), JWT_SECRET: z.string().min(32), MEILI_URL: z.string().url().default('http://localhost:7700'), MEILI_MASTER_KEY: z.string().default('tower_meili_dev_key'), LOG_LEVEL: z.enum(['trace', 'debug', 'info', 'warn', 'error']).default('info'), }); export type Env = z.infer; export function validateEnv(env: NodeJS.ProcessEnv = process.env): Env { const result = envSchema.safeParse(env); if (!result.success) { console.error('Invalid environment variables:', result.error.format()); throw new Error('Invalid environment variables'); } return result.data; } ``` - [ ] **Step 4: Run tests — verify all pass** ```bash pnpm --filter @tower/config test ``` Expected: ``` PASS src/index.test.ts validateEnv ✓ returns parsed config for valid env ✓ applies default API_PORT of 3001 when not set ✓ throws when DATABASE_URL is missing ✓ throws when JWT_SECRET is shorter than 32 chars ✓ throws when DATABASE_URL is not a valid URL Tests: 5 passed, 5 total ``` - [ ] **Step 5: Build and commit** ```bash pnpm --filter @tower/config build ``` Expected: `packages/config/dist/` created with no TS errors. ```bash git add packages/config git commit -m "feat: add @tower/config package with env validation" ``` --- ### Task 4: `@tower/logger` Package **Files:** - Create: `packages/logger/package.json` - Create: `packages/logger/tsconfig.json` - Create: `packages/logger/src/index.ts` - [ ] **Step 1: Write the logger** Create `packages/logger/src/index.ts`: ```typescript import pino from 'pino'; export function createLogger(name: string) { return pino({ name, level: process.env['LOG_LEVEL'] ?? 'info', ...(process.env['NODE_ENV'] !== 'production' && { transport: { target: 'pino-pretty', options: { colorize: true }, }, }), }); } export type Logger = ReturnType; ``` - [ ] **Step 2: Write package config** Create `packages/logger/package.json`: ```json { "name": "@tower/logger", "version": "0.0.1", "main": "./dist/index.js", "types": "./dist/index.d.ts", "exports": { ".": { "types": "./dist/index.d.ts", "default": "./dist/index.js" } }, "scripts": { "build": "tsc", "dev": "tsc --watch" }, "dependencies": { "pino": "^9.0.0", "pino-pretty": "^11.0.0" }, "devDependencies": { "@types/node": "^22.0.0", "typescript": "^5.7.0" } } ``` Create `packages/logger/tsconfig.json`: ```json { "extends": "../../tsconfig.base.json", "compilerOptions": { "outDir": "./dist", "rootDir": "./src" }, "include": ["src"] } ``` - [ ] **Step 3: Build and verify** ```bash pnpm --filter @tower/logger install pnpm --filter @tower/logger build ``` Expected: `packages/logger/dist/` created with no TS errors. - [ ] **Step 4: Commit** ```bash git add packages/logger git commit -m "feat: add @tower/logger package" ``` --- ### Task 5: `@tower/ui` and `@tower/sdk` Package Shells **Files:** - Create: `packages/ui/package.json` - Create: `packages/ui/tsconfig.json` - Create: `packages/ui/src/index.ts` - Create: `packages/sdk/package.json` - Create: `packages/sdk/tsconfig.json` - Create: `packages/sdk/src/index.ts` These are shells — populated in later plans. - [ ] **Step 1: Write the ui package** Create `packages/ui/src/index.ts`: ```typescript // UI component library — populated in later plans export {}; ``` Create `packages/ui/package.json`: ```json { "name": "@tower/ui", "version": "0.0.1", "main": "./src/index.ts", "types": "./src/index.ts", "exports": { ".": "./src/index.ts" }, "scripts": { "build": "echo 'ui is consumed as source by Next.js'" }, "peerDependencies": { "react": "^19.0.0" }, "devDependencies": { "@types/react": "^19.0.0", "react": "^19.0.0", "typescript": "^5.7.0" } } ``` Create `packages/ui/tsconfig.json`: ```json { "extends": "../../tsconfig.base.json", "compilerOptions": { "jsx": "react-jsx", "outDir": "./dist", "rootDir": "./src" }, "include": ["src"] } ``` - [ ] **Step 2: Write the sdk package** Create `packages/sdk/src/index.ts`: ```typescript // External SDK — populated in later plans export {}; ``` Create `packages/sdk/package.json`: ```json { "name": "@tower/sdk", "version": "0.0.1", "main": "./dist/index.js", "types": "./dist/index.d.ts", "exports": { ".": { "types": "./dist/index.d.ts", "default": "./dist/index.js" } }, "scripts": { "build": "tsc", "dev": "tsc --watch" }, "devDependencies": { "typescript": "^5.7.0" } } ``` Create `packages/sdk/tsconfig.json`: ```json { "extends": "../../tsconfig.base.json", "compilerOptions": { "outDir": "./dist", "rootDir": "./src" }, "include": ["src"] } ``` - [ ] **Step 3: Install and commit** ```bash pnpm install git add packages/ui packages/sdk git commit -m "feat: add @tower/ui and @tower/sdk shell packages" ``` --- ### Task 6: Docker Compose Dev Stack + Environment **Files:** - Create: `docker-compose.yml` - Create: `.env.example` - Create: `.env` (from example, not committed) - [ ] **Step 1: Write `docker-compose.yml`** ```yaml version: '3.9' services: postgres: image: postgres:17-alpine environment: POSTGRES_USER: tower POSTGRES_PASSWORD: tower_dev POSTGRES_DB: tower_dev ports: - '5432:5432' volumes: - postgres_data:/var/lib/postgresql/data healthcheck: test: ['CMD-SHELL', 'pg_isready -U tower -d tower_dev'] interval: 5s timeout: 5s retries: 5 redis: image: redis:7-alpine ports: - '6379:6379' healthcheck: test: ['CMD', 'redis-cli', 'ping'] interval: 5s timeout: 5s retries: 5 meilisearch: image: getmeili/meilisearch:v1.11 ports: - '7700:7700' environment: MEILI_NO_ANALYTICS: 'true' MEILI_MASTER_KEY: tower_meili_dev_key volumes: - meilisearch_data:/meili_data volumes: postgres_data: meilisearch_data: ``` - [ ] **Step 2: Write `.env.example`** ```bash # Database DATABASE_URL=postgresql://tower:tower_dev@localhost:5432/tower_dev # Redis REDIS_URL=redis://localhost:6379 # API API_PORT=3001 # Auth JWT_SECRET=change_me_in_production_must_be_32_chars_min # Meilisearch MEILI_URL=http://localhost:7700 MEILI_MASTER_KEY=tower_meili_dev_key # Logging NODE_ENV=development LOG_LEVEL=debug ``` - [ ] **Step 3: Start the stack and verify all services are healthy** ```bash cp .env.example .env docker compose up -d ``` Wait ~10 seconds, then: ```bash docker compose ps ``` Expected: all three services show `healthy` or `running`: ``` NAME STATUS tower-postgres-1 Up (healthy) tower-redis-1 Up (healthy) tower-meilisearch-1 Up ``` Verify Postgres: ```bash docker compose exec postgres pg_isready -U tower -d tower_dev ``` Expected: `localhost:5432 - accepting connections` Verify Redis: ```bash docker compose exec redis redis-cli ping ``` Expected: `PONG` - [ ] **Step 4: Commit** ```bash git add docker-compose.yml .env.example git commit -m "chore: add Docker Compose dev stack (postgres, redis, meilisearch)" ``` --- ### Task 7: NestJS API Application Scaffold **Files:** - Create: `apps/api/package.json` - Create: `apps/api/tsconfig.json` - Create: `apps/api/nest-cli.json` - Create: `apps/api/jest.config.js` - Create: `apps/api/src/main.ts` - Create: `apps/api/src/app.module.ts` - [ ] **Step 1: Write `apps/api/package.json`** ```json { "name": "@tower/api", "version": "0.0.1", "scripts": { "build": "nest build", "dev": "nest start --watch", "start": "node dist/main", "test": "jest", "test:e2e": "jest --config ./test/jest-e2e.json" }, "dependencies": { "@nestjs/common": "^11.0.0", "@nestjs/config": "^4.0.0", "@nestjs/core": "^11.0.0", "@nestjs/platform-express": "^11.0.0", "@prisma/client": "^6.0.0", "@tower/config": "workspace:*", "@tower/logger": "workspace:*", "@tower/types": "workspace:*", "reflect-metadata": "^0.2.0", "rxjs": "^7.8.0" }, "devDependencies": { "@nestjs/cli": "^11.0.0", "@nestjs/schematics": "^11.0.0", "@nestjs/testing": "^11.0.0", "@types/jest": "^29.0.0", "@types/node": "^22.0.0", "jest": "^29.0.0", "prisma": "^6.0.0", "ts-jest": "^29.0.0", "typescript": "^5.7.0" } } ``` - [ ] **Step 2: Write TypeScript and NestJS configs** Create `apps/api/tsconfig.json`: ```json { "compilerOptions": { "module": "commonjs", "declaration": true, "removeComments": true, "emitDecoratorMetadata": true, "experimentalDecorators": true, "allowSyntheticDefaultImports": true, "target": "ES2021", "sourceMap": true, "outDir": "./dist", "baseUrl": "./", "incremental": true, "skipLibCheck": true, "strictNullChecks": true, "noImplicitAny": true } } ``` Create `apps/api/nest-cli.json`: ```json { "$schema": "https://json.schemastore.org/nest-cli", "collection": "@nestjs/schematics", "sourceRoot": "src", "compilerOptions": { "deleteOutDir": true } } ``` Create `apps/api/jest.config.js`: ```javascript module.exports = { moduleFileExtensions: ['js', 'json', 'ts'], rootDir: 'src', testRegex: '.*\\.spec\\.ts$', transform: { '^.+\\.(t|j)s$': 'ts-jest' }, testEnvironment: 'node', }; ``` - [ ] **Step 3: Write application entry files** Create `apps/api/src/main.ts`: ```typescript import 'reflect-metadata'; import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; async function bootstrap() { const app = await NestFactory.create(AppModule); const port = process.env['API_PORT'] ?? 3001; await app.listen(port); console.log(`TOWER API running on port ${port}`); } bootstrap(); ``` Create `apps/api/src/app.module.ts`: ```typescript import { Module } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; import { PrismaModule } from './prisma/prisma.module'; import { HealthModule } from './modules/health/health.module'; @Module({ imports: [ ConfigModule.forRoot({ isGlobal: true }), PrismaModule, HealthModule, ], }) export class AppModule {} ``` - [ ] **Step 4: Install dependencies and verify build** ```bash pnpm --filter @tower/api install pnpm --filter @tower/api build ``` Expected: `apps/api/dist/` created with no TS errors. (Will fail at runtime until Prisma is generated — that's fine for now.) - [ ] **Step 5: Commit** ```bash git add apps/api git commit -m "feat: scaffold NestJS API application" ``` --- ### Task 8: Prisma Schema + PrismaService (TDD) **Files:** - Create: `apps/api/prisma/schema.prisma` - Create: `apps/api/src/prisma/prisma.module.ts` - Create: `apps/api/src/prisma/prisma.service.ts` - Create: `apps/api/src/prisma/prisma.service.spec.ts` - [ ] **Step 1: Write the failing integration test first** Create `apps/api/src/prisma/prisma.service.spec.ts`: ```typescript import { Test, TestingModule } from '@nestjs/testing'; import { PrismaService } from './prisma.service'; describe('PrismaService', () => { let prisma: PrismaService; beforeAll(async () => { const module: TestingModule = await Test.createTestingModule({ providers: [PrismaService], }).compile(); prisma = module.get(PrismaService); await prisma.onModuleInit(); }); afterAll(async () => { await prisma.onModuleDestroy(); }); it('connects to postgres', async () => { const result = await prisma.$queryRaw<[{ ok: bigint }]>`SELECT 1 AS ok`; expect(Number(result[0]!.ok)).toBe(1); }); it('creates and retrieves a Group, then cleans up', async () => { const group = await prisma.group.create({ data: { platform: 'whatsapp', platformId: `test-group-${Date.now()}@g.us`, name: 'Test Group', }, }); expect(group.id).toBeDefined(); expect(group.platform).toBe('whatsapp'); expect(group.isActive).toBe(true); await prisma.group.delete({ where: { id: group.id } }); }); }); ``` Run: ```bash pnpm --filter @tower/api test ``` Expected: **FAIL** — `Cannot find module './prisma.service'`. - [ ] **Step 2: Write the Prisma schema** Create `apps/api/prisma/schema.prisma`: ```prisma generator client { provider = "prisma-client-js" } datasource db { provider = "postgresql" url = env("DATABASE_URL") } model Group { id String @id @default(cuid()) platform String platformId String name String description String? isActive Boolean @default(true) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt messages Message[] syncRoutesFrom SyncRoute[] @relation("sourceGroup") syncRoutesTo SyncRoute[] @relation("targetGroup") consentRecords ConsentRecord[] @@unique([platform, platformId]) } model Message { id String @id @default(cuid()) platform String platformMsgId String sourceGroupId String sourceGroup Group @relation(fields: [sourceGroupId], references: [id]) senderJid String senderName String? content String mediaUrl String? tags String[] status MessageStatus @default(PENDING) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt approval Approval? @@unique([platform, platformMsgId]) } enum MessageStatus { PENDING APPROVED REJECTED DISTRIBUTED ARCHIVED } model Approval { id String @id @default(cuid()) messageId String @unique message Message @relation(fields: [messageId], references: [id]) adminId String decision ApprovalDecision notes String? decidedAt DateTime @default(now()) } enum ApprovalDecision { APPROVED REJECTED } model SyncRoute { id String @id @default(cuid()) sourceGroupId String sourceGroup Group @relation("sourceGroup", fields: [sourceGroupId], references: [id]) targetGroupId String targetGroup Group @relation("targetGroup", fields: [targetGroupId], references: [id]) isActive Boolean @default(true) createdAt DateTime @default(now()) @@unique([sourceGroupId, targetGroupId]) } model ConsentRecord { id String @id @default(cuid()) groupId String group Group @relation(fields: [groupId], references: [id]) memberJid String consentType String grantedAt DateTime @default(now()) revokedAt DateTime? @@unique([groupId, memberJid, consentType]) } ``` - [ ] **Step 3: Run the first migration** Make sure Docker Compose is running (from Task 6), then: ```bash cd apps/api DATABASE_URL="postgresql://tower:tower_dev@localhost:5432/tower_dev" pnpm exec prisma migrate dev --name init_core_schema cd ../.. ``` Expected output: ``` Applying migration `20260527000000_init_core_schema` Your database is now in sync with your schema. Generated Prisma Client ``` Tables created: `Group`, `Message`, `Approval`, `SyncRoute`, `ConsentRecord` plus the two enums. - [ ] **Step 4: Write `PrismaService` and `PrismaModule`** Create `apps/api/src/prisma/prisma.service.ts`: ```typescript import { Injectable, OnModuleInit, OnModuleDestroy } from '@nestjs/common'; import { PrismaClient } from '@prisma/client'; @Injectable() export class PrismaService extends PrismaClient implements OnModuleInit, OnModuleDestroy { async onModuleInit() { await this.$connect(); } async onModuleDestroy() { await this.$disconnect(); } } ``` Create `apps/api/src/prisma/prisma.module.ts`: ```typescript import { Global, Module } from '@nestjs/common'; import { PrismaService } from './prisma.service'; @Global() @Module({ providers: [PrismaService], exports: [PrismaService], }) export class PrismaModule {} ``` - [ ] **Step 5: Run the integration tests — verify they pass** ```bash DATABASE_URL="postgresql://tower:tower_dev@localhost:5432/tower_dev" pnpm --filter @tower/api test ``` Expected: ``` PASS src/prisma/prisma.service.spec.ts PrismaService ✓ connects to postgres ✓ creates and retrieves a Group, then cleans up ``` - [ ] **Step 6: Rebuild to include generated client** ```bash pnpm --filter @tower/api build ``` Expected: no TS errors. - [ ] **Step 7: Commit** ```bash git add apps/api/prisma apps/api/src/prisma git commit -m "feat: add Prisma schema and PrismaService with integration tests" ``` --- ### Task 9: Health Check Module (TDD) **Files:** - Create: `apps/api/src/modules/health/health.controller.spec.ts` - Create: `apps/api/src/modules/health/health.controller.ts` - Create: `apps/api/src/modules/health/health.module.ts` - [ ] **Step 1: Write the failing unit test** Create `apps/api/src/modules/health/health.controller.spec.ts`: ```typescript import { Test, TestingModule } from '@nestjs/testing'; import { HealthController } from './health.controller'; describe('HealthController', () => { let controller: HealthController; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ controllers: [HealthController], }).compile(); controller = module.get(HealthController); }); it('returns status "ok"', () => { const result = controller.check(); expect(result.status).toBe('ok'); }); it('returns an ISO timestamp', () => { const result = controller.check(); expect(() => new Date(result.timestamp)).not.toThrow(); expect(result.timestamp).toMatch(/^\d{4}-\d{2}-\d{2}T/); }); it('returns the service name', () => { const result = controller.check(); expect(result.service).toBe('tower-api'); }); }); ``` Run: ```bash pnpm --filter @tower/api test ``` Expected: **HealthController — 3 FAIL** — `Cannot find module './health.controller'`. - [ ] **Step 2: Write `HealthController`** Create `apps/api/src/modules/health/health.controller.ts`: ```typescript import { Controller, Get } from '@nestjs/common'; @Controller('health') export class HealthController { @Get() check() { return { status: 'ok', service: 'tower-api', timestamp: new Date().toISOString(), }; } } ``` - [ ] **Step 3: Write `HealthModule`** Create `apps/api/src/modules/health/health.module.ts`: ```typescript import { Module } from '@nestjs/common'; import { HealthController } from './health.controller'; @Module({ controllers: [HealthController], }) export class HealthModule {} ``` - [ ] **Step 4: Run tests — all pass** ```bash pnpm --filter @tower/api test ``` Expected: ``` PASS src/modules/health/health.controller.spec.ts HealthController ✓ returns status "ok" ✓ returns an ISO timestamp ✓ returns the service name PASS src/prisma/prisma.service.spec.ts PrismaService ✓ connects to postgres ✓ creates and retrieves a Group, then cleans up Tests: 5 passed ``` - [ ] **Step 5: Smoke-test the running API** ```bash pnpm --filter @tower/api dev & ``` Wait 3 seconds, then: ```bash curl http://localhost:3001/health ``` Expected: ```json {"status":"ok","service":"tower-api","timestamp":"2026-05-27T..."} ``` Kill the background process: `kill %1` - [ ] **Step 6: Commit** ```bash git add apps/api/src/modules git commit -m "feat: add health check module with unit tests" ``` --- ### Task 10: Next.js 15 Web Application **Files:** - Create: `apps/web/package.json` - Create: `apps/web/tsconfig.json` - Create: `apps/web/next.config.ts` - Create: `apps/web/tailwind.config.ts` - Create: `apps/web/postcss.config.js` - Create: `apps/web/jest.config.js` - Create: `apps/web/jest.setup.ts` - Create: `apps/web/app/globals.css` - Create: `apps/web/app/layout.tsx` - Create: `apps/web/app/page.tsx` - Create: `apps/web/app/page.test.tsx` - [ ] **Step 1: Write the failing render test** Create `apps/web/app/page.test.tsx`: ```typescript import { render, screen } from '@testing-library/react'; import Home from './page'; describe('Home page', () => { it('renders the TOWER heading', () => { render(); expect(screen.getByRole('heading', { name: /insignia tower/i })).toBeInTheDocument(); }); it('renders the platform tagline', () => { render(); expect(screen.getByText(/community knowledge infrastructure/i)).toBeInTheDocument(); }); }); ``` - [ ] **Step 2: Write `apps/web/package.json`** ```json { "name": "@tower/web", "version": "0.0.1", "scripts": { "build": "next build", "dev": "next dev --port 3000", "start": "next start", "test": "jest", "lint": "next lint" }, "dependencies": { "@tower/types": "workspace:*", "@tower/ui": "workspace:*", "next": "^16.0.0", "react": "^19.0.0", "react-dom": "^19.0.0" }, "devDependencies": { "@tailwindcss/postcss": "^4.0.0", "@testing-library/jest-dom": "^6.0.0", "@testing-library/react": "^16.0.0", "@types/node": "^22.0.0", "@types/react": "^19.0.0", "@types/react-dom": "^19.0.0", "jest": "^29.0.0", "jest-environment-jsdom": "^29.0.0", "postcss": "^8.0.0", "tailwindcss": "^4.0.0", "ts-jest": "^29.0.0", "typescript": "^5.7.0" } } ``` Create `apps/web/tsconfig.json`: ```json { "extends": "../../tsconfig.base.json", "compilerOptions": { "target": "ES2017", "module": "esnext", "moduleResolution": "bundler", "allowJs": true, "jsx": "preserve", "incremental": true, "paths": { "@/*": ["./*"] } }, "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], "exclude": ["node_modules"] } ``` Create `apps/web/jest.config.js`: ```javascript const nextJest = require('next/jest'); const createJestConfig = nextJest({ dir: './' }); module.exports = createJestConfig({ setupFilesAfterEnv: ['/jest.setup.ts'], testEnvironment: 'jest-environment-jsdom', testMatch: ['**/*.test.tsx', '**/*.test.ts'], }); ``` Create `apps/web/jest.setup.ts`: ```typescript import '@testing-library/jest-dom'; ``` - [ ] **Step 3: Write Next.js and Tailwind configs** Create `apps/web/next.config.ts`: ```typescript import type { NextConfig } from 'next'; const nextConfig: NextConfig = { transpilePackages: ['@tower/ui'], }; export default nextConfig; ``` Create `apps/web/postcss.config.js`: ```javascript module.exports = { plugins: { '@tailwindcss/postcss': {}, }, }; ``` Note: Tailwind v4 auto-detects content paths — no `tailwind.config.ts` needed for the basic setup. Custom theme tokens are added via CSS `@theme` blocks in `globals.css`. - [ ] **Step 4: Write the app shell** Create `apps/web/app/globals.css`: ```css @import "tailwindcss"; ``` Create `apps/web/app/layout.tsx`: ```typescript import type { Metadata } from 'next'; import './globals.css'; export const metadata: Metadata = { title: 'Insignia TOWER', description: 'Community Knowledge Infrastructure Platform', }; export default function RootLayout({ children }: { children: React.ReactNode }) { return ( {children} ); } ``` Create `apps/web/app/page.tsx`: ```typescript export default function Home() { return (

Insignia TOWER

Community Knowledge Infrastructure Platform

); } ``` - [ ] **Step 5: Install, run tests, build and commit** ```bash pnpm --filter @tower/web install pnpm --filter @tower/web test ``` Expected: ``` PASS app/page.test.tsx Home page ✓ renders the TOWER heading ✓ renders the platform tagline ``` ```bash pnpm --filter @tower/web build ``` Expected: Next.js build succeeds — `Route (app) /` listed, no errors. ```bash git add apps/web git commit -m "feat: scaffold Next.js 15 web application with Tailwind" ``` --- ### Task 11: Worker Application Shell **Files:** - Create: `apps/worker/package.json` - Create: `apps/worker/tsconfig.json` - Create: `apps/worker/src/main.ts` - [ ] **Step 1: Write the worker entry point** Create `apps/worker/src/main.ts`: ```typescript import { createLogger } from '@tower/logger'; const logger = createLogger('tower-worker'); logger.info('TOWER worker starting...'); process.on('SIGTERM', () => { logger.info('TOWER worker shutting down gracefully'); process.exit(0); }); process.on('SIGINT', () => { logger.info('TOWER worker interrupted'); process.exit(0); }); ``` - [ ] **Step 2: Write package config** Create `apps/worker/package.json`: ```json { "name": "@tower/worker", "version": "0.0.1", "scripts": { "build": "tsc", "dev": "ts-node-dev --respawn --transpile-only src/main.ts", "start": "node dist/main" }, "dependencies": { "@tower/logger": "workspace:*", "@tower/types": "workspace:*", "bullmq": "^5.0.0", "ioredis": "^5.0.0" }, "devDependencies": { "@types/node": "^22.0.0", "ts-node-dev": "^2.0.0", "typescript": "^5.7.0" } } ``` Create `apps/worker/tsconfig.json`: ```json { "extends": "../../tsconfig.base.json", "compilerOptions": { "outDir": "./dist", "rootDir": "./src" }, "include": ["src"] } ``` - [ ] **Step 3: Build and verify** ```bash pnpm --filter @tower/worker install pnpm --filter @tower/worker build ``` Expected: `apps/worker/dist/main.js` created, no errors. Verify it starts and exits cleanly: ```bash node apps/worker/dist/main.js & sleep 1 kill %1 ``` Expected log: `TOWER worker starting...` then `TOWER worker shutting down gracefully`. - [ ] **Step 4: Commit** ```bash git add apps/worker git commit -m "feat: add worker application shell" ``` --- ### Task 12: Turborepo Pipeline Verification + Final Smoke Test **Files:** No new files — verifying all tasks wire together. - [ ] **Step 1: Run full monorepo build** ```bash pnpm build ``` Expected: Turborepo builds packages in dependency order, then apps. Final output: ``` Tasks: 8 successful, 8 total Cached: 0 cached, 8 total Time: s ``` No errors. If there are any TS errors, fix them before proceeding. - [ ] **Step 2: Run all tests across the workspace** ```bash DATABASE_URL="postgresql://tower:tower_dev@localhost:5432/tower_dev" pnpm test ``` Expected: ``` @tower/config: Tests: 5 passed @tower/api: Tests: 5 passed (3 health + 2 prisma) @tower/web: Tests: 2 passed ``` - [ ] **Step 3: Verify `turbo dev` starts all apps** ```bash pnpm dev ``` In another terminal, verify: ```bash curl http://localhost:3001/health # {"status":"ok","service":"tower-api","timestamp":"..."} curl http://localhost:3000 # HTML response with "Insignia TOWER" in body ``` Stop with `Ctrl+C`. - [ ] **Step 4: Final commit** ```bash git add . git commit -m "chore: verify full monorepo build, test, and dev pipeline" ``` --- ## Self-Review ### Spec Coverage | Requirement from context file | Covered | |---|---| | `apps/api` — NestJS | ✅ Task 7 | | `apps/web` — Next.js 15, Tailwind, shadcn/ui | ✅ Task 10 (shadcn populated in Plan 5) | | `apps/worker` | ✅ Task 11 | | `packages/ui` | ✅ Task 5 | | `packages/types` | ✅ Task 2 | | `packages/config` | ✅ Task 3 | | `packages/logger` | ✅ Task 4 | | `packages/sdk` | ✅ Task 5 | | PostgreSQL + Prisma ORM | ✅ Task 8 | | Redis | ✅ Task 6 (Docker) | | Meilisearch | ✅ Task 6 (Docker, integrated in Plan 4) | | Docker everywhere | ✅ Task 6 | | DB tables: groups, messages, sync_routes, approvals, consent_records | ✅ Task 8 | | Canonical message schema | ✅ Task 2 (`@tower/types`) | ### Placeholder Scan No TBDs, no "implement later", no "similar to Task N" references. All code blocks are complete and self-contained. ### Type Consistency - `Group`, `Message`, `SyncRoute`, `ConsentRecord` defined in schema.prisma (Task 8) and mirrored as interfaces in `@tower/types` (Task 2) — names consistent. - `MessageStatus` and `ApprovalDecision` enums match between Prisma schema and `@tower/types`. - `PrismaService` imported in `prisma.service.spec.ts` matches the export in `prisma.service.ts`. - `HealthController` imported in `health.controller.spec.ts` matches the export in `health.controller.ts`.