Files
tower/docs/superpowers/plans/2026-05-27-monorepo-foundation.md
T
maaz519 d33b4e40b8 fix: use type-only Baileys import and raw status code to fix Jest ESM issue
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>
2026-05-27 17:40:24 +05:30

1751 lines
39 KiB
Markdown

# 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<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`:
```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<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**
```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<typeof createLogger>;
```
- [ ] **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>(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>(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(<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`**
```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: ['<rootDir>/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 (
<html lang="en">
<body className="bg-white text-gray-900 antialiased">{children}</body>
</html>
);
}
```
Create `apps/web/app/page.tsx`:
```typescript
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**
```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: <N>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`.