d33b4e40b8
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>
1751 lines
39 KiB
Markdown
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`.
|