# Phase 0 — Scaffold 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:** Stand up the WhatApp Inhouse monorepo — database schema, shared
clients, backend/worker/web app skeletons, authentication, Docker, and CI — so
that Phase 1 can begin on a running, tested, featureless platform.

**Architecture:** A pnpm + Turborepo TypeScript monorepo. `apps/api` (NestJS)
serves the REST/webhook/internal API; `apps/worker` (BullMQ) runs background
jobs; `apps/web` (React + Vite) is the admin console. `packages/db` holds the
Prisma schema; `packages/shared` holds shared types, Zod schemas, the Meta and
n8n clients, and the secret-encryption helper; `packages/config` holds shared
build config. Everything runs in Docker Compose on the InMotion VPS.

**Tech Stack:** Node 22 LTS, pnpm 9, TypeScript 5, Turborepo 2, NestJS 11,
Prisma 7 (PostgreSQL), React 19 + Vite 6, BullMQ 5 (Redis), Vitest 3, Zod,
Argon2, GitHub Actions.

**References:** `../docs/design.md`, `../docs/data-model.md`,
`../docs/integrations.md`, `../specs/08-admin-auth.md`.

**Conventions:**
- Every task ends in a commit. Commit messages: `feat:` / `chore:` / `test:` +
  a short description.
- Tests use Vitest. Run from the repo root with `pnpm test`.
- No secret value is ever committed. `.env` is gitignored; `.env.example` holds
  the keys with empty/placeholder values.
- After each task, run `pnpm install` if dependencies changed and verify the
  task's stated check passes before committing.

---

## Task 1: Initialize the monorepo

**Files:**
- Create: `package.json`, `pnpm-workspace.yaml`, `turbo.json`, `tsconfig.json`,
  `.gitignore`, `.env.example`, `.npmrc`, `.editorconfig`

- [ ] **Step 1: Create the workspace manifest**

`pnpm-workspace.yaml`:
```yaml
packages:
  - "apps/*"
  - "packages/*"
```

- [ ] **Step 2: Create the root `package.json`**

```json
{
  "name": "whatapp-inhouse",
  "private": true,
  "packageManager": "pnpm@9.15.0",
  "engines": { "node": ">=22" },
  "scripts": {
    "build": "turbo run build",
    "dev": "turbo run dev",
    "lint": "turbo run lint",
    "typecheck": "turbo run typecheck",
    "test": "turbo run test"
  },
  "devDependencies": {
    "turbo": "^2.3.0",
    "typescript": "^5.7.0",
    "prettier": "^3.4.0"
  }
}
```

- [ ] **Step 3: Create `turbo.json`**

```json
{
  "$schema": "https://turbo.build/schema.json",
  "tasks": {
    "build": { "dependsOn": ["^build"], "outputs": ["dist/**"] },
    "dev": { "cache": false, "persistent": true },
    "lint": {},
    "typecheck": { "dependsOn": ["^build"] },
    "test": { "dependsOn": ["^build"] }
  }
}
```

- [ ] **Step 4: Create the root `tsconfig.json`** (base config extended by every
  package — strict mode on):

```json
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "declaration": true,
    "sourceMap": true
  }
}
```

- [ ] **Step 5: Create `.gitignore`**

```
node_modules/
dist/
.env
.env.*.local
*.log
.turbo/
coverage/
packages/db/prisma/migrations/dev.db*
```

- [ ] **Step 6: Create `.env.example`** with every key the platform reads, all
  values empty:

```
# --- Core ---
NODE_ENV=development
PUBLIC_BASE_URL=https://whatapp.silveroakglobal.ae
# --- Database / Redis ---
DATABASE_URL=postgresql://whatapp:whatapp@localhost:5432/whatapp
REDIS_URL=redis://localhost:6379
# --- Security ---
JWT_SECRET=
CONFIG_ENCRYPTION_KEY=
# --- API ---
API_PORT=3000
WEB_ORIGIN=http://localhost:5173
```

- [ ] **Step 7: Create `.npmrc`** (`auto-install-peers=true`,
  `strict-peer-dependencies=false`) and a standard `.editorconfig`.

- [ ] **Step 8: Verify and commit**

Run: `pnpm install`
Expected: completes with no errors; a `pnpm-lock.yaml` is created.

```bash
git add -A
git commit -m "chore: initialize pnpm + turborepo monorepo"
```

---

## Task 2: Shared build config (`packages/config`)

**Files:**
- Create: `packages/config/package.json`, `packages/config/tsconfig.base.json`,
  `packages/config/eslint.config.mjs`

- [ ] **Step 1: Create `packages/config/package.json`**

```json
{
  "name": "@whatapp/config",
  "version": "0.0.0",
  "private": true,
  "files": ["tsconfig.base.json", "eslint.config.mjs"],
  "devDependencies": {
    "eslint": "^9.17.0",
    "typescript-eslint": "^8.18.0"
  }
}
```

- [ ] **Step 2: Create `tsconfig.base.json`** extending the root tsconfig, and a
  flat-config `eslint.config.mjs` using `typescript-eslint` recommended rules.
  Each package's own `tsconfig.json` extends `@whatapp/config/tsconfig.base.json`.

- [ ] **Step 3: Verify and commit**

Run: `pnpm install`
Expected: `@whatapp/config` resolves as a workspace package.

```bash
git add -A
git commit -m "chore: add shared tsconfig and eslint config package"
```

---

## Task 3: Database package — Prisma schema (`packages/db`)

**Files:**
- Create: `packages/db/package.json`, `packages/db/tsconfig.json`,
  `packages/db/prisma/schema.prisma`, `packages/db/src/index.ts`

- [ ] **Step 1: Create `packages/db/package.json`**

```json
{
  "name": "@whatapp/db",
  "version": "0.0.0",
  "private": true,
  "main": "./src/index.ts",
  "scripts": {
    "build": "tsc",
    "typecheck": "tsc --noEmit",
    "db:generate": "prisma generate",
    "db:migrate": "prisma migrate dev",
    "db:deploy": "prisma migrate deploy",
    "db:seed": "tsx prisma/seed.ts"
  },
  "dependencies": { "@prisma/client": "^7.0.0" },
  "devDependencies": { "prisma": "^7.0.0", "tsx": "^4.19.0" }
}
```

- [ ] **Step 2: Create `prisma/schema.prisma`**

Write the **complete schema exactly as specified in `../docs/data-model.md`**
(the `## Schema` section — all enums and models). Add the generator/datasource
header:

```prisma
generator client {
  provider = "prisma-client-js"
}
datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}
```

- [ ] **Step 3: Create `src/index.ts`** exporting a singleton Prisma client:

```typescript
import { PrismaClient } from "@prisma/client";
export const prisma = new PrismaClient();
export * from "@prisma/client";
```

- [ ] **Step 4: Generate the client and create the initial migration**

Run (with Postgres available — see Task 4; run Task 4 first if needed):
```
pnpm --filter @whatapp/db db:generate
pnpm --filter @whatapp/db exec prisma migrate dev --name init
```
Expected: a migration is created under `prisma/migrations/` and applies cleanly;
`prisma generate` reports the client was generated.

- [ ] **Step 5: Commit**

```bash
git add -A
git commit -m "feat: add Prisma schema and initial migration"
```

> Note: Task 4 provisions the database. If running strictly in order, do Task 4
> before Task 3 Step 4, or run Step 4 after Task 4.

---

## Task 4: Dev infrastructure — Docker Compose for Postgres & Redis

**Files:**
- Create: `docker-compose.yml`

- [ ] **Step 1: Create `docker-compose.yml`** with `postgres:16` and `redis:7`
  services, named volumes, healthchecks, and ports `5432`/`6379`. Postgres env:
  user `whatapp`, password `whatapp`, db `whatapp` — matching `.env.example`'s
  `DATABASE_URL`. (App services are added in Task 13.)

- [ ] **Step 2: Verify**

Run: `docker compose up -d postgres redis`
Then: `docker compose ps`
Expected: both containers are `healthy`.

- [ ] **Step 3: Commit**

```bash
git add docker-compose.yml
git commit -m "chore: add Docker Compose for Postgres and Redis"
```

---

## Task 5: Shared package & the secret-encryption helper (`packages/shared`)

**Files:**
- Create: `packages/shared/package.json`, `packages/shared/tsconfig.json`,
  `packages/shared/vitest.config.ts`, `packages/shared/src/index.ts`
- Create: `packages/shared/src/crypto/encryption.ts`
- Test: `packages/shared/src/crypto/encryption.test.ts`

- [ ] **Step 1: Create the package** — `package.json` (`@whatapp/shared`,
  `main: ./src/index.ts`, scripts `build`/`typecheck`/`test`, deps `zod`,
  devDeps `vitest`), `tsconfig.json` extending the base, and a `vitest.config.ts`.

- [ ] **Step 2: Write the failing test** — `src/crypto/encryption.test.ts`:

```typescript
import { describe, it, expect } from "vitest";
import { encryptSecret, decryptSecret } from "./encryption";

const KEY = "0123456789abcdef0123456789abcdef"; // 32 bytes

describe("encryption", () => {
  it("round-trips a value", () => {
    const enc = encryptSecret("super-secret-token", KEY);
    expect(enc).not.toContain("super-secret-token");
    expect(decryptSecret(enc, KEY)).toBe("super-secret-token");
  });

  it("rejects tampered ciphertext", () => {
    const enc = encryptSecret("value", KEY);
    const tampered = enc.slice(0, -2) + "00";
    expect(() => decryptSecret(tampered, KEY)).toThrow();
  });
});
```

- [ ] **Step 3: Run the test, verify it fails**

Run: `pnpm --filter @whatapp/shared test`
Expected: FAIL — `encryption` module not found.

- [ ] **Step 4: Implement `src/crypto/encryption.ts`** — AES-256-GCM using
  Node's `crypto`: `encryptSecret(plain, key)` generates a random 12-byte IV,
  encrypts, and returns `base64(iv).base64(authTag).base64(ciphertext)`;
  `decryptSecret(blob, key)` reverses it and throws if the auth tag fails. The
  key is the 32-char `CONFIG_ENCRYPTION_KEY`.

- [ ] **Step 5: Run the test, verify it passes**

Run: `pnpm --filter @whatapp/shared test`
Expected: PASS (2 tests).

- [ ] **Step 6: Commit**

```bash
git add -A
git commit -m "feat: add shared package and AES-GCM secret encryption helper"
```

---

## Task 6: Meta WhatsApp API client (`packages/shared`)

**Files:**
- Create: `packages/shared/src/meta/types.ts`,
  `packages/shared/src/meta/message-builder.ts`,
  `packages/shared/src/meta/meta-client.ts`
- Test: `packages/shared/src/meta/message-builder.test.ts`

- [ ] **Step 1: Define types** in `meta/types.ts` — `MetaConfig`
  (`{ apiVersion, phoneNumberId, wabaId, accessToken, appSecret, verifyToken }`),
  an `OutboundMessage` union (text / template / image / document / interactive),
  and `MetaSendResult` (`{ ok, wamid?, error? }`). See `../docs/integrations.md`
  §1.2.

- [ ] **Step 2: Write the failing test** — `meta/message-builder.test.ts`:

```typescript
import { describe, it, expect } from "vitest";
import { buildMessageBody } from "./message-builder";

describe("buildMessageBody", () => {
  it("builds a text body", () => {
    const body = buildMessageBody({ to: "9715551234", type: "text",
      content: { text: "Hi" } });
    expect(body).toEqual({
      messaging_product: "whatsapp", recipient_type: "individual",
      to: "9715551234", type: "text", text: { body: "Hi" } });
  });

  it("builds a template body with body params", () => {
    const body = buildMessageBody({ to: "9715551234", type: "template",
      content: { name: "welcome", language: "en", bodyParams: ["Ahmed"] } });
    expect(body.type).toBe("template");
    expect(body.template.name).toBe("welcome");
    expect(body.template.components[0].parameters[0].text).toBe("Ahmed");
  });
});
```

- [ ] **Step 3: Run the test, verify it fails**

Run: `pnpm --filter @whatapp/shared test message-builder`
Expected: FAIL — `message-builder` not found.

- [ ] **Step 4: Implement `meta/message-builder.ts`** — `buildMessageBody`
  takes an `OutboundMessage` and returns the exact Graph API JSON body for each
  type, per `../docs/integrations.md` §1.2.

- [ ] **Step 5: Run the test, verify it passes**

Run: `pnpm --filter @whatapp/shared test message-builder`
Expected: PASS.

- [ ] **Step 6: Implement `meta/meta-client.ts`** — a `MetaClient` class
  constructed with `MetaConfig`. Methods: `sendMessage(msg)` (`POST`
  `/{phoneNumberId}/messages`, returns `MetaSendResult` — never throws on a
  normal Meta error, parses `messages[0].id` or `error`), `verifySignature(rawBody,
  signatureHeader)` (HMAC-SHA256 with `appSecret`), `getMediaUrl(mediaId)`,
  `downloadMedia(url)`, `submitTemplate(template)`, `listTemplates()`. Use the
  global `fetch`. Network calls are not unit-tested here; they are integration
  points exercised in Phase 1.

- [ ] **Step 7: Export and commit** — export the Meta client from
  `src/index.ts`.

```bash
git add -A
git commit -m "feat: add Meta WhatsApp API client"
```

---

## Task 7: n8n API client (`packages/shared`)

**Files:**
- Create: `packages/shared/src/n8n/n8n-client.ts`,
  `packages/shared/src/n8n/types.ts`
- Test: `packages/shared/src/n8n/n8n-client.test.ts`

- [ ] **Step 1: Define types** — `N8nConfig` (`{ baseUrl, apiKey,
  callbackSecret }`), `N8nWorkflow`, `N8nExecution`.

- [ ] **Step 2: Write the failing test** — mock `fetch` and assert
  `listWorkflows()` calls `{baseUrl}/api/v1/workflows` with the
  `X-N8N-API-KEY` header and maps the response:

```typescript
import { describe, it, expect, vi } from "vitest";
import { N8nClient } from "./n8n-client";

describe("N8nClient", () => {
  it("lists workflows with the api key header", async () => {
    const fetchMock = vi.fn().mockResolvedValue({
      ok: true, json: async () => ({ data: [{ id: "w1", name: "Flow",
        active: true }] }) });
    vi.stubGlobal("fetch", fetchMock);
    const client = new N8nClient({ baseUrl: "https://n8n.test",
      apiKey: "k", callbackSecret: "s" });
    const flows = await client.listWorkflows();
    expect(fetchMock).toHaveBeenCalledWith(
      "https://n8n.test/api/v1/workflows",
      expect.objectContaining({ headers: expect.objectContaining({
        "X-N8N-API-KEY": "k" }) }));
    expect(flows[0]).toMatchObject({ id: "w1", name: "Flow", active: true });
  });
});
```

- [ ] **Step 3: Run the test, verify it fails**

Run: `pnpm --filter @whatapp/shared test n8n-client`
Expected: FAIL — `n8n-client` not found.

- [ ] **Step 4: Implement `n8n/n8n-client.ts`** — an `N8nClient` class with
  `listWorkflows()`, `getWorkflow(id)`, `setActive(id, active)`,
  `listExecutions(workflowId?)`, and `triggerWebhook(url, payload)` (a plain
  `POST` to the flow's webhook URL). It **never** calls `PUT /workflows/:id`.
  See `../docs/integrations.md` §2.

- [ ] **Step 5: Run the test, verify it passes**

Run: `pnpm --filter @whatapp/shared test n8n-client`
Expected: PASS.

- [ ] **Step 6: Export and commit**

```bash
git add -A
git commit -m "feat: add n8n API client"
```

---

## Task 8: API app skeleton (`apps/api`)

**Files:**
- Create: the NestJS app under `apps/api/` — `package.json`, `tsconfig.json`,
  `nest-cli.json`, `src/main.ts`, `src/app.module.ts`,
  `src/config/config.module.ts`, `src/prisma/prisma.module.ts` +
  `prisma.service.ts`, `src/health/health.controller.ts`

- [ ] **Step 1: Scaffold the NestJS app** — create `apps/api` as a NestJS 11
  project (name `@whatapp/api`). Add workspace deps `@whatapp/db`,
  `@whatapp/shared`, `@whatapp/config`. Add `@nestjs/config`, `zod`.

- [ ] **Step 2: Config module** — load env via `@nestjs/config`, validate it
  with a Zod schema covering every key in `.env.example`; the app fails fast on
  a missing/invalid required variable.

- [ ] **Step 3: Prisma module** — a `PrismaService` wrapping the `@whatapp/db`
  client with `onModuleInit` connect / `onModuleDestroy` disconnect, exported by
  a global `PrismaModule`.

- [ ] **Step 4: Health endpoint** — `GET /health` returns
  `{ status: "ok", db: <boolean> }`, where `db` is the result of a
  `SELECT 1`. CORS is enabled for `WEB_ORIGIN`. The app listens on `API_PORT`.

- [ ] **Step 5: Verify**

Run (Postgres up): `pnpm --filter @whatapp/api dev`
Then: `curl http://localhost:3000/health`
Expected: `{"status":"ok","db":true}`.

- [ ] **Step 6: Commit**

```bash
git add -A
git commit -m "feat: scaffold NestJS API with config, Prisma, health endpoint"
```

---

## Task 9: Authentication & roles (`apps/api`)

Implements `../specs/08-admin-auth.md` FR-08.1–08.6 (auth skeleton).

**Files:**
- Create: `apps/api/src/auth/` — `auth.module.ts`, `auth.service.ts`,
  `auth.controller.ts`, `password.ts`, `jwt.strategy.ts`,
  `roles.guard.ts`, `roles.decorator.ts`
- Test: `apps/api/src/auth/password.test.ts`, `auth.service.test.ts`

- [ ] **Step 1: Add deps** — `argon2`, `@nestjs/jwt`, `@nestjs/passport`,
  `passport`, `passport-jwt`.

- [ ] **Step 2: Write the failing password test** — `auth/password.test.ts`:

```typescript
import { describe, it, expect } from "vitest";
import { hashPassword, verifyPassword } from "./password";

describe("password", () => {
  it("hashes and verifies", async () => {
    const hash = await hashPassword("correct horse");
    expect(hash).not.toContain("correct horse");
    expect(await verifyPassword(hash, "correct horse")).toBe(true);
    expect(await verifyPassword(hash, "wrong")).toBe(false);
  });
});
```

- [ ] **Step 3: Run it, verify it fails** —
  `pnpm --filter @whatapp/api test password` → FAIL.

- [ ] **Step 4: Implement `auth/password.ts`** — `hashPassword` /
  `verifyPassword` using `argon2` (argon2id). Run the test → PASS.

- [ ] **Step 5: Write the failing auth-service test** — `auth.service.test.ts`:
  with a mocked Prisma `user.findUnique`, assert `login()` returns a token for
  correct credentials, throws `UnauthorizedException` for a wrong password, and
  throws for an `isActive=false` user.

- [ ] **Step 6: Run it, verify it fails.**

- [ ] **Step 7: Implement auth** — `AuthService.login(email, password)`
  verifies the user and password, checks `isActive`, updates `lastLoginAt`, and
  returns a JWT (signed with `JWT_SECRET`, payload `{ sub, role }`).
  `AuthController` exposes `POST /api/auth/login`, `POST /api/auth/logout`,
  `GET /api/auth/me`. `JwtStrategy` validates the token. `RolesGuard` +
  `@Roles()` decorator enforce `admin`/`marketing`/`viewer` per
  `../specs/08-admin-auth.md` FR-08.5. Apply a global JWT guard with a
  `@Public()` escape for `login`, `/health`, and webhooks.

- [ ] **Step 8: Run the tests, verify they pass.**

- [ ] **Step 9: Commit**

```bash
git add -A
git commit -m "feat: add authentication, JWT, and role-based authorization"
```

---

## Task 10: Connections module (`apps/api`)

Implements `../specs/08-admin-auth.md` FR-08.7–08.10.

**Files:**
- Create: `apps/api/src/connections/` — `connections.module.ts`,
  `connections.service.ts`, `connections.controller.ts`, `dto.ts`
- Test: `apps/api/src/connections/connections.service.test.ts`

- [ ] **Step 1: Write the failing test** — assert that creating a connection
  encrypts `secrets` before persisting (the stored blob does not contain the raw
  value) and that reading it back through the service decrypts correctly, while
  the controller-facing serializer masks secret values.

- [ ] **Step 2: Run it, verify it fails.**

- [ ] **Step 3: Implement the service** — `ConnectionsService` uses
  `encryptSecret`/`decryptSecret` (`@whatapp/shared`, key from
  `CONFIG_ENCRYPTION_KEY`) so `connections.secrets` is always encrypted at rest;
  a `getDecrypted(provider)` helper returns usable config for other modules; a
  serializer masks secrets for API responses.

- [ ] **Step 4: Implement the controller** — `GET/POST/PATCH /api/connections`
  (admin only) and `POST /api/connections/:id/test` (a stub returning
  `{ ok: true }` for now; real provider checks are added with each provider's
  phase). Validate request bodies with Zod DTOs.

- [ ] **Step 5: Run the tests, verify they pass.**

- [ ] **Step 6: Commit**

```bash
git add -A
git commit -m "feat: add encrypted connections (credentials) module"
```

---

## Task 11: Worker app skeleton (`apps/worker`)

**Files:**
- Create: `apps/worker/` — `package.json`, `tsconfig.json`, `src/main.ts`,
  `src/queues.ts`

- [ ] **Step 1: Scaffold** — `@whatapp/worker`, deps `bullmq`, `ioredis`,
  workspace deps `@whatapp/db`, `@whatapp/shared`. Scripts `dev`/`build`/
  `typecheck`/`test`.

- [ ] **Step 2: Queue setup** — `src/queues.ts` defines a shared Redis
  connection (from `REDIS_URL`) and exports queue names as constants
  (`webhook-processing`, `media-download`, `campaign-send`, `flow-dispatch`,
  `alerts`). `src/main.ts` starts a BullMQ `Worker` for each queue with a no-op
  processor that logs the job — real processors are added in later phases.

- [ ] **Step 3: Verify**

Run (Redis up): `pnpm --filter @whatapp/worker dev`
Expected: the process starts and logs that all workers are listening; no errors.

- [ ] **Step 4: Commit**

```bash
git add -A
git commit -m "feat: scaffold BullMQ worker app"
```

---

## Task 12: Web app skeleton (`apps/web`)

**Files:**
- Create: `apps/web/` — a React 19 + Vite 6 + TypeScript app: `package.json`,
  `vite.config.ts`, `index.html`, `src/main.tsx`, `src/App.tsx`,
  `src/lib/api.ts`, `src/routes/Login.tsx`, `src/routes/Dashboard.tsx`,
  `src/components/AppShell.tsx`

- [ ] **Step 1: Scaffold** the Vite React-TS app as `@whatapp/web`. Add a
  router (`react-router`) and a data layer (`@tanstack/react-query`). Configure
  the dev server to proxy `/api` to `http://localhost:3000`.

- [ ] **Step 2: API client** — `src/lib/api.ts`: a thin `fetch` wrapper that
  sends credentials, attaches the auth token, and throws typed errors.

- [ ] **Step 3: Auth flow** — `Login.tsx` posts to `/api/auth/login` and stores
  the token; an auth context guards routes; logging in lands on `Dashboard.tsx`.

- [ ] **Step 4: App shell** — `AppShell.tsx`: a sidebar with the eventual module
  navigation (Dashboard, Inbox, Contacts, Templates, Campaigns, Automation,
  Analytics, Settings) — links may be placeholders for now — and a header with
  the current user and logout. Navigation items are role-aware (hidden for roles
  that lack access).

- [ ] **Step 5: Verify**

Run (API up): `pnpm --filter @whatapp/web dev`
Open the dev URL: the login page renders; logging in with a seeded user (Task
15) reaches the dashboard; logout works.

- [ ] **Step 6: Commit**

```bash
git add -A
git commit -m "feat: scaffold React admin console with login and app shell"
```

---

## Task 13: Dockerfiles & full Compose

**Files:**
- Create: `apps/api/Dockerfile`, `apps/worker/Dockerfile`,
  `apps/web/Dockerfile`, `.dockerignore`
- Modify: `docker-compose.yml`

- [ ] **Step 1: Dockerfiles** — multi-stage builds for `api` and `worker`
  (Node 22, `pnpm install --frozen-lockfile`, `turbo build`, run the built
  output). For `web`, build the static bundle and serve it with `nginx`.

- [ ] **Step 2: Extend `docker-compose.yml`** — add `api`, `worker`, and `web`
  services depending on healthy `postgres`/`redis`, reading env from `.env`. The
  `api` runs `prisma migrate deploy` on start before booting.

- [ ] **Step 3: Verify**

Run: `docker compose up --build`
Expected: all five services start; `curl http://localhost:3000/health` returns
`{"status":"ok","db":true}`; the web app loads.

- [ ] **Step 4: Commit**

```bash
git add -A
git commit -m "chore: add Dockerfiles and full-stack Docker Compose"
```

---

## Task 14: CI pipeline

**Files:**
- Create: `.github/workflows/ci.yml`

- [ ] **Step 1: Create the workflow** — on push and pull request: set up Node 22
  and pnpm, `pnpm install --frozen-lockfile`, start a Postgres service container,
  run `prisma migrate deploy`, then `pnpm lint`, `pnpm typecheck`, `pnpm test`,
  `pnpm build`. The job fails if any step fails.

- [ ] **Step 2: Verify**

Run locally to mirror CI: `pnpm install --frozen-lockfile && pnpm lint &&
pnpm typecheck && pnpm test && pnpm build`
Expected: all pass.

- [ ] **Step 3: Commit**

```bash
git add -A
git commit -m "ci: add lint, typecheck, test, and build pipeline"
```

---

## Task 15: Database seed & final verification

**Files:**
- Create: `packages/db/prisma/seed.ts`

- [ ] **Step 1: Seed script** — `seed.ts` creates one `admin` user from
  `SEED_ADMIN_EMAIL` / `SEED_ADMIN_PASSWORD` env vars (hashed with the same
  `argon2` helper — import logic consistent with `apps/api/src/auth/password.ts`;
  if needed, move `hashPassword` into `@whatapp/shared` so both use one
  implementation). It is idempotent (upsert by email). Add `SEED_ADMIN_EMAIL`
  and `SEED_ADMIN_PASSWORD` to `.env.example`.

- [ ] **Step 2: Run the seed**

Run: `pnpm --filter @whatapp/db db:seed`
Expected: an admin user exists; re-running does not error or duplicate.

- [ ] **Step 3: Full verification**

Run, in order, and confirm each:
- `pnpm lint` → passes
- `pnpm typecheck` → passes
- `pnpm test` → all tests pass
- `pnpm build` → all packages build
- `docker compose up --build` → all services healthy; `GET /health` ok; login
  with the seeded admin works in the web app.

- [ ] **Step 4: Commit**

```bash
git add -A
git commit -m "feat: add database seed and complete Phase 0 scaffold"
```

---

## Self-review checklist (run before declaring Phase 0 done)

- [ ] Every `apps/*` and `packages/*` package builds and typechecks.
- [ ] `pnpm test` passes; encryption, password, message-builder, and n8n-client
  tests all run.
- [ ] `GET /health` returns `db: true` against the Dockerized Postgres.
- [ ] A seeded admin can log into the web console; a `viewer` is blocked from
  write endpoints (verify the guard).
- [ ] `.env` is gitignored; `.env.example` lists every key; no secret is
  committed anywhere in the repo.
- [ ] CI is green.

When all boxes are ticked, tick **Phase 0** in `ROADMAP.md`, then proceed to
Phase 1 using the per-phase process in `../CLAUDE.md`.
