Replaces DisconnectReason enum import with type-only WASocket import and uses 401 directly instead of DisconnectReason.loggedOut. Baileys is an ES module that cannot be executed in Jest's CommonJS mode, so removing the value import (keeping only type imports) prevents ts-jest from trying to execute the module. Also updated session-pool.test.ts to verify end() is called with the expected Boom error object instead of undefined. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
39 KiB
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
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
git init
Create package.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:
packages:
- 'apps/*'
- 'packages/*'
Create turbo.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:
{
"compilerOptions": {
"target": "ES2021",
"module": "commonjs",
"lib": ["ES2021"],
"strict": true,
"skipLibCheck": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"resolveJsonModule": true,
"esModuleInterop": true
}
}
Create .prettierrc:
{
"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
pnpm install
Expected: node_modules/.modules.yaml created, no errors.
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:
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:
export * from './message';
- Step 2: Write package config
Create packages/types/package.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:
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "./dist",
"rootDir": "./src"
},
"include": ["src"]
}
- Step 3: Build and verify TypeScript compiles
pnpm --filter @tower/types build
Expected: packages/types/dist/ created, index.js and index.d.ts present, no TS errors.
- Step 4: Commit
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:
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<string, string>;
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:
{
"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:
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "./dist",
"rootDir": "./src"
},
"include": ["src"]
}
Create packages/config/jest.config.js:
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
testMatch: ['**/*.test.ts'],
};
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:
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<typeof envSchema>;
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
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
pnpm --filter @tower/config build
Expected: packages/config/dist/ created with no TS errors.
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:
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<typeof createLogger>;
- Step 2: Write package config
Create packages/logger/package.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:
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "./dist",
"rootDir": "./src"
},
"include": ["src"]
}
- Step 3: Build and verify
pnpm --filter @tower/logger install
pnpm --filter @tower/logger build
Expected: packages/logger/dist/ created with no TS errors.
- Step 4: Commit
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:
// UI component library — populated in later plans
export {};
Create packages/ui/package.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:
{
"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:
// External SDK — populated in later plans
export {};
Create packages/sdk/package.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:
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "./dist",
"rootDir": "./src"
},
"include": ["src"]
}
- Step 3: Install and commit
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
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
# 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
cp .env.example .env
docker compose up -d
Wait ~10 seconds, then:
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:
docker compose exec postgres pg_isready -U tower -d tower_dev
Expected: localhost:5432 - accepting connections
Verify Redis:
docker compose exec redis redis-cli ping
Expected: PONG
- Step 4: Commit
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
{
"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:
{
"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:
{
"$schema": "https://json.schemastore.org/nest-cli",
"collection": "@nestjs/schematics",
"sourceRoot": "src",
"compilerOptions": {
"deleteOutDir": true
}
}
Create apps/api/jest.config.js:
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:
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:
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
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
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:
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>(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:
pnpm --filter @tower/api test
Expected: FAIL — Cannot find module './prisma.service'.
- Step 2: Write the Prisma schema
Create apps/api/prisma/schema.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:
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
PrismaServiceandPrismaModule
Create apps/api/src/prisma/prisma.service.ts:
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:
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
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
pnpm --filter @tower/api build
Expected: no TS errors.
- Step 7: Commit
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:
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>(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:
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:
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:
import { Module } from '@nestjs/common';
import { HealthController } from './health.controller';
@Module({
controllers: [HealthController],
})
export class HealthModule {}
- Step 4: Run tests — all pass
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
pnpm --filter @tower/api dev &
Wait 3 seconds, then:
curl http://localhost:3001/health
Expected:
{"status":"ok","service":"tower-api","timestamp":"2026-05-27T..."}
Kill the background process: kill %1
- Step 6: Commit
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:
import { render, screen } from '@testing-library/react';
import Home from './page';
describe('Home page', () => {
it('renders the TOWER heading', () => {
render(<Home />);
expect(screen.getByRole('heading', { name: /insignia tower/i })).toBeInTheDocument();
});
it('renders the platform tagline', () => {
render(<Home />);
expect(screen.getByText(/community knowledge infrastructure/i)).toBeInTheDocument();
});
});
- Step 2: Write
apps/web/package.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:
{
"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:
const nextJest = require('next/jest');
const createJestConfig = nextJest({ dir: './' });
module.exports = createJestConfig({
setupFilesAfterEnv: ['<rootDir>/jest.setup.ts'],
testEnvironment: 'jest-environment-jsdom',
testMatch: ['**/*.test.tsx', '**/*.test.ts'],
});
Create apps/web/jest.setup.ts:
import '@testing-library/jest-dom';
- Step 3: Write Next.js and Tailwind configs
Create apps/web/next.config.ts:
import type { NextConfig } from 'next';
const nextConfig: NextConfig = {
transpilePackages: ['@tower/ui'],
};
export default nextConfig;
Create apps/web/postcss.config.js:
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:
@import "tailwindcss";
Create apps/web/app/layout.tsx:
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 (
<html lang="en">
<body className="bg-white text-gray-900 antialiased">{children}</body>
</html>
);
}
Create apps/web/app/page.tsx:
export default function Home() {
return (
<main className="flex min-h-screen flex-col items-center justify-center gap-4">
<h1 className="text-4xl font-bold tracking-tight">Insignia TOWER</h1>
<p className="text-lg text-gray-500">Community Knowledge Infrastructure Platform</p>
</main>
);
}
- Step 5: Install, run tests, build and commit
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
pnpm --filter @tower/web build
Expected: Next.js build succeeds — Route (app) / listed, no errors.
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:
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:
{
"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:
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "./dist",
"rootDir": "./src"
},
"include": ["src"]
}
- Step 3: Build and verify
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:
node apps/worker/dist/main.js &
sleep 1
kill %1
Expected log: TOWER worker starting... then TOWER worker shutting down gracefully.
- Step 4: Commit
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
pnpm build
Expected: Turborepo builds packages in dependency order, then apps. Final output:
Tasks: 8 successful, 8 total
Cached: 0 cached, 8 total
Time: <N>s
No errors. If there are any TS errors, fix them before proceeding.
- Step 2: Run all tests across the workspace
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 devstarts all apps
pnpm dev
In another terminal, verify:
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
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,ConsentRecorddefined in schema.prisma (Task 8) and mirrored as interfaces in@tower/types(Task 2) — names consistent.MessageStatusandApprovalDecisionenums match between Prisma schema and@tower/types.PrismaServiceimported inprisma.service.spec.tsmatches the export inprisma.service.ts.HealthControllerimported inhealth.controller.spec.tsmatches the export inhealth.controller.ts.