Files
tower/docs/superpowers/plans/2026-05-27-monorepo-foundation.md
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

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 FAILCannot 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: FAILCannot 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 PrismaService and PrismaModule

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 FAILCannot 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 dev starts 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, 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.