52 KiB
Phase 01 Foundation & Deployment 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: Create the buildable and deployable foundation for the Travel Planner: Angular PWA, NestJS API and worker, PostgreSQL/Redis infrastructure, health checks, a single-port production edge, and TeamCity-ready build/deploy scripts.
Architecture: Use a pnpm workspace with frontend and backend. The backend is one NestJS codebase with separate API and worker entry points plus shared libraries. Production compiles Angular into the edge image; only edge publishes a host TCP port and reverse-proxies /api/* to api:3000. PostgreSQL and Redis are internal Docker services. TeamCity invokes repository-owned scripts to validate, build immutable images, migrate, deploy, health-check, and roll back.
Tech Stack: Node.js 24 LTS, pnpm, TypeScript, Angular 22 + Vitest + Angular PWA service worker, NestJS 11 + Jest + Terminus, PostgreSQL 18.4, Redis 8.8.1, Docker Compose, Nginx edge, TeamCity.
Global Constraints
- PostgreSQL is the source of truth.
- Do not add Mistral, agent logic, OIDC, trips, or business entities in Phase 01.
- Exactly one TCP port from production Compose may be published to the host.
- Only
edgemay contain a Composeports:mapping. api,worker,postgres, andredismust have no host-published ports.- Do not use
network_mode: host. - The edge serves Angular static files and proxies
/api/*toapi:3000. - Production does not require host port 80.
- Node baseline is the 24 LTS line; Angular is 22.x; NestJS is 11.x.
- Pin PostgreSQL production image to
postgres:18.4-alpineand Redis production image toredis:8.8.1-alpinein the initial deployment configuration. - JavaScript dependency versions are frozen by
pnpm-lock.yaml. - Secrets are not committed or baked into images.
- TeamCity deployment uses immutable
IMAGE_TAGvalues and does not rundocker compose downas a routine deployment step. - Live/paid providers do not exist in this phase.
File Structure Locked by This Phase
travel-planner/
├── package.json # workspace commands and Node/pnpm contract
├── pnpm-workspace.yaml # frontend/backend workspace membership
├── pnpm-lock.yaml
├── .editorconfig
├── .gitignore
├── .env.example # non-secret configuration names/defaults
├── README.md # developer startup and production topology
├── compose.yml # production stack; one published edge port
├── compose.dev.yml # PostgreSQL/Redis developer dependencies only
├── frontend/
│ ├── angular.json
│ ├── package.json
│ ├── ngsw-config.json
│ └── src/
│ ├── app/
│ │ ├── app.component.ts
│ │ ├── app.component.html
│ │ └── app.component.spec.ts
│ ├── index.html
│ └── manifest.webmanifest
├── backend/
│ ├── package.json
│ ├── eslint.config.mjs
│ ├── nest-cli.json
│ ├── tsconfig.json
│ ├── apps/
│ │ ├── api/src/
│ │ │ ├── main.ts
│ │ │ ├── migration.ts
│ │ │ ├── api.module.ts
│ │ │ └── health/
│ │ │ ├── health.controller.ts
│ │ │ ├── health.controller.spec.ts
│ │ │ ├── health.module.ts
│ │ │ └── readiness.service.ts
│ │ └── worker/src/
│ │ ├── main.ts
│ │ ├── worker.module.ts
│ │ └── worker.bootstrap.spec.ts
│ └── libs/
│ ├── configuration/src/
│ │ ├── environment.ts
│ │ ├── environment.spec.ts
│ │ └── index.ts
│ └── infrastructure/src/
│ ├── postgres/postgres.module.ts
│ ├── redis/redis.module.ts
│ └── index.ts
├── docker/
│ ├── api.Dockerfile
│ ├── worker.Dockerfile
│ ├── edge.Dockerfile
│ └── edge/
│ ├── nginx.conf
│ └── default.conf.template
├── scripts/
│ ├── verify-compose-invariants.mjs
│ └── teamcity/
│ ├── validate.sh
│ ├── build-images.sh
│ ├── deploy.sh
│ ├── rollback.sh
│ └── smoke.sh
├── tests/
│ └── compose-invariants.test.mjs
└── docs/
├── architecture/
│ └── deployment.md
└── superpowers/
└── plans/
├── 2026-08-17-travel-planner-master-roadmap.md
└── 2026-08-17-phase-01-foundation-deployment.md
Task 1: Establish the pnpm Workspace and Version Contract
Files:
- Create:
package.json - Create:
pnpm-workspace.yaml - Create:
.editorconfig - Create:
.gitignore - Create:
.env.example - Create:
README.md - Test: workspace commands executed from repository root
Interfaces:
-
Consumes: none.
-
Produces: root commands
pnpm lint,pnpm test,pnpm build,pnpm dev:infra,pnpm dev:infra:down; workspace locationsfrontendandbackend. -
Step 1: Create the root workspace manifest
Create package.json:
{
"name": "travel-planner",
"private": true,
"packageManager": "pnpm@10.15.0",
"engines": {
"node": ">=24.15.0 <25"
},
"scripts": {
"lint": "pnpm -r --if-present run lint",
"test": "pnpm -r --if-present run test",
"build": "pnpm -r --if-present run build",
"dev:infra": "docker compose -f compose.dev.yml up -d --wait",
"dev:infra:down": "docker compose -f compose.dev.yml down",
"test:compose": "node --test tests/compose-invariants.test.mjs"
}
}
- Step 2: Declare the workspaces and repository defaults
Create pnpm-workspace.yaml:
packages:
- frontend
- backend
Create .editorconfig:
root = true
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
indent_style = space
indent_size = 2
trim_trailing_whitespace = true
[*.md]
trim_trailing_whitespace = false
Create .gitignore with at least:
node_modules/
dist/
.angular/
coverage/
.env
.env.*
!.env.example
/data/
*.log
.DS_Store
- Step 3: Define non-secret environment names
Create .env.example:
APP_HTTPS_PORT=443
IMAGE_TAG=local
REGISTRY=local
POSTGRES_IMAGE_TAG=18.4-alpine
REDIS_IMAGE_TAG=8.8.1-alpine
POSTGRES_DB=travel_planner
POSTGRES_USER=travel_planner
POSTGRES_PASSWORD=change-me-outside-source-control
DATABASE_URL=postgresql://travel_planner:change-me-outside-source-control@postgres:5432/travel_planner
REDIS_URL=redis://redis:6379
APP_VERSION=dev
TEAMCITY_BUILD_NUMBER=local
SOURCE_REVISION=local
TLS_CERT_FILE=/etc/travel-planner/tls/tls.crt
TLS_KEY_FILE=/etc/travel-planner/tls/tls.key
Add to README.md an explicit warning that .env.example contains examples only and production secrets live on the deployment host.
- Step 4: Verify the root package contract
Run:
node --version
corepack enable
pnpm --version
pnpm install
Expected:
-
Node satisfies
>=24.15.0 <25. -
pnpm installsucceeds and createspnpm-lock.yaml. -
Step 5: Commit
git add package.json pnpm-workspace.yaml pnpm-lock.yaml .editorconfig .gitignore .env.example README.md
git commit -m "chore: establish travel planner workspace"
Task 2: Scaffold the NestJS API and Worker Entry Points
Files:
- Create:
backend/package.json - Create:
backend/nest-cli.json - Create:
backend/tsconfig.json - Create:
backend/apps/api/tsconfig.app.json - Create:
backend/apps/api/src/main.ts - Create:
backend/apps/api/src/api.module.ts - Create:
backend/apps/worker/tsconfig.app.json - Create:
backend/apps/worker/src/main.ts - Create:
backend/apps/worker/src/worker.module.ts - Test:
backend/apps/worker/src/worker.bootstrap.spec.ts
Interfaces:
-
Consumes: Node/pnpm root contract from Task 1.
-
Produces:
pnpm --filter backend build:api,build:worker,start:api,start:worker; API listens on internal port3000; worker creates a Nest application context and exposes no HTTP listener. -
Step 1: Write the worker bootstrap test first
Create backend/apps/worker/src/worker.bootstrap.spec.ts:
import { describe, expect, it, jest } from '@jest/globals';
import { bootstrapWorker } from './main';
describe('bootstrapWorker', () => {
it('creates an application context without starting an HTTP listener', async () => {
const close = jest.fn().mockResolvedValue(undefined);
const enableShutdownHooks = jest.fn();
const appContext = { close, enableShutdownHooks };
const createContext = jest.fn().mockResolvedValue(appContext);
const app = await bootstrapWorker(createContext as never);
expect(createContext).toHaveBeenCalledTimes(1);
expect(enableShutdownHooks).toHaveBeenCalledTimes(1);
expect(app).toBe(appContext);
});
});
- Step 2: Run the focused test and verify failure
Run:
pnpm --filter backend test -- apps/worker/src/worker.bootstrap.spec.ts
Expected: FAIL because bootstrapWorker and backend test configuration do not yet exist.
- Step 3: Create the backend package and Nest configuration
Create backend/package.json with NestJS 11 packages, Jest + SWC, TypeScript, and scripts:
{
"name": "backend",
"private": true,
"scripts": {
"build": "pnpm run build:api && pnpm run build:worker",
"build:api": "nest build api",
"build:worker": "nest build worker",
"start:api": "node dist/apps/api/main.js",
"start:worker": "node dist/apps/worker/main.js",
"lint": "eslint \"{apps,libs}/**/*.ts\"",
"test": "jest --runInBand"
},
"dependencies": {
"@nestjs/common": "^11.0.0",
"@nestjs/core": "^11.0.0",
"@nestjs/platform-express": "^11.0.0",
"reflect-metadata": "^0.2.2",
"rxjs": "^7.8.0"
},
"devDependencies": {
"@jest/globals": "^30.0.0",
"@nestjs/cli": "^11.0.0",
"@nestjs/testing": "^11.0.0",
"@types/jest": "^30.0.0",
"@types/node": "^24.0.0",
"@eslint/js": "^9.0.0",
"@swc/core": "^1.0.0",
"@swc/jest": "^0.2.39",
"eslint": "^9.0.0",
"jest": "^30.0.0",
"typescript": "^6.0.0",
"typescript-eslint": "^8.0.0"
},
"jest": {
"moduleFileExtensions": ["js", "json", "ts"],
"rootDir": ".",
"testRegex": ".*\\.spec\\.ts$",
"transform": {
"^.+\\.(t|j)s$": [
"@swc/jest",
{
"jsc": {
"parser": {"syntax": "typescript", "decorators": true},
"transform": {"legacyDecorator": true, "decoratorMetadata": true}
},
"module": {"type": "commonjs"}
}
]
},
"collectCoverageFrom": ["apps/**/*.ts", "libs/**/*.ts"],
"coverageDirectory": "coverage",
"testEnvironment": "node"
}
}
Create backend/nest-cli.json:
{
"$schema": "https://json.schemastore.org/nest-cli",
"collection": "@nestjs/schematics",
"monorepo": true,
"root": "apps/api",
"sourceRoot": "apps/api/src",
"compilerOptions": {"deleteOutDir": true},
"projects": {
"api": {
"type": "application",
"root": "apps/api",
"entryFile": "main",
"sourceRoot": "apps/api/src",
"compilerOptions": {"tsConfigPath": "apps/api/tsconfig.app.json"}
},
"worker": {
"type": "application",
"root": "apps/worker",
"entryFile": "main",
"sourceRoot": "apps/worker/src",
"compilerOptions": {"tsConfigPath": "apps/worker/tsconfig.app.json"}
}
}
}
Create backend/tsconfig.json:
{
"compilerOptions": {
"module": "commonjs",
"declaration": true,
"removeComments": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"allowSyntheticDefaultImports": true,
"target": "ES2023",
"sourceMap": true,
"outDir": "./dist",
"baseUrl": "./",
"incremental": true,
"skipLibCheck": true,
"strict": true,
"paths": {
"@travel/configuration": ["libs/configuration/src"],
"@travel/infrastructure": ["libs/infrastructure/src"]
}
}
}
Create both backend/apps/api/tsconfig.app.json and backend/apps/worker/tsconfig.app.json with the appropriate output exclusions:
{
"extends": "../../tsconfig.json",
"compilerOptions": {"declaration": false},
"exclude": ["node_modules", "dist", "test", "**/*.spec.ts"]
}
Create backend/eslint.config.mjs:
import eslint from '@eslint/js';
import tseslint from 'typescript-eslint';
export default tseslint.config(
{ ignores: ['dist/**', 'coverage/**'] },
eslint.configs.recommended,
...tseslint.configs.recommended,
);
Run:
pnpm install
- Step 4: Implement the API and worker bootstraps
Create backend/apps/api/src/api.module.ts:
import { Module } from '@nestjs/common';
@Module({})
export class ApiModule {}
Create backend/apps/api/src/main.ts:
import { NestFactory } from '@nestjs/core';
import { ApiModule } from './api.module';
export async function bootstrapApi(): Promise<void> {
const app = await NestFactory.create(ApiModule);
app.enableShutdownHooks();
app.setGlobalPrefix('api/v1');
await app.listen(3000, '0.0.0.0');
}
if (require.main === module) {
void bootstrapApi();
}
Create backend/apps/worker/src/worker.module.ts:
import { Module } from '@nestjs/common';
@Module({})
export class WorkerModule {}
Create backend/apps/worker/src/main.ts:
import { INestApplicationContext, Type } from '@nestjs/common';
import { NestFactory } from '@nestjs/core';
import { WorkerModule } from './worker.module';
type ContextFactory = (module: Type<unknown>) => Promise<INestApplicationContext>;
export async function bootstrapWorker(
createContext: ContextFactory = (module) => NestFactory.createApplicationContext(module),
): Promise<INestApplicationContext> {
const app = await createContext(WorkerModule);
app.enableShutdownHooks();
return app;
}
if (require.main === module) {
void bootstrapWorker();
}
- Step 5: Run tests and builds
Run:
pnpm --filter backend test -- apps/worker/src/worker.bootstrap.spec.ts
pnpm --filter backend build
Expected: PASS; both API and worker build outputs exist.
- Step 6: Commit
git add backend pnpm-lock.yaml
git commit -m "feat: add nestjs api and worker skeletons"
Task 3: Add Configuration Parsing with Fail-Fast Validation
Files:
- Create:
backend/libs/configuration/src/environment.ts - Create:
backend/libs/configuration/src/environment.spec.ts - Create:
backend/libs/configuration/src/index.ts - Modify:
backend/apps/api/src/api.module.ts - Modify:
backend/apps/worker/src/worker.module.ts
Interfaces:
-
Consumes: process environment variables.
-
Produces:
loadEnvironment(env: NodeJS.ProcessEnv): AppEnvironmentwithdatabaseUrl,redisUrl, and safe build metadata. -
Step 1: Write failing environment tests
Create backend/libs/configuration/src/environment.spec.ts:
import { loadEnvironment } from './environment';
describe('loadEnvironment', () => {
it('requires database and redis URLs', () => {
expect(() => loadEnvironment({})).toThrow('DATABASE_URL');
});
it('returns build metadata without secrets', () => {
expect(loadEnvironment({
DATABASE_URL: 'postgresql://u:p@postgres:5432/db',
REDIS_URL: 'redis://redis:6379',
APP_VERSION: '1.2.3',
TEAMCITY_BUILD_NUMBER: '42',
SOURCE_REVISION: 'abc123',
})).toEqual({
databaseUrl: 'postgresql://u:p@postgres:5432/db',
redisUrl: 'redis://redis:6379',
appVersion: '1.2.3',
teamCityBuildNumber: '42',
sourceRevision: 'abc123',
});
});
});
- Step 2: Run the test and verify failure
pnpm --filter backend test -- libs/configuration/src/environment.spec.ts
Expected: FAIL because loadEnvironment does not exist.
- Step 3: Implement minimal validated configuration
Create environment.ts:
export interface AppEnvironment {
databaseUrl: string;
redisUrl: string;
appVersion: string;
teamCityBuildNumber: string;
sourceRevision: string;
}
function required(env: NodeJS.ProcessEnv, name: string): string {
const value = env[name]?.trim();
if (!value) throw new Error(`Missing required environment variable: ${name}`);
return value;
}
export function loadEnvironment(env: NodeJS.ProcessEnv): AppEnvironment {
return {
databaseUrl: required(env, 'DATABASE_URL'),
redisUrl: required(env, 'REDIS_URL'),
appVersion: env.APP_VERSION?.trim() || 'dev',
teamCityBuildNumber: env.TEAMCITY_BUILD_NUMBER?.trim() || 'local',
sourceRevision: env.SOURCE_REVISION?.trim() || 'local',
};
}
Export it from index.ts together with a typed provider:
import { loadEnvironment } from './environment';
export * from './environment';
export const APP_ENVIRONMENT = Symbol('APP_ENVIRONMENT');
export const appEnvironmentProvider = {
provide: APP_ENVIRONMENT,
useFactory: () => loadEnvironment(process.env),
};
- Step 4: Wire validation into both application roots
Add appEnvironmentProvider to the providers of both ApiModule and WorkerModule and export it from a small shared configuration module if Nest module reuse is needed. Do not expose the raw process.env object through dependency injection.
- Step 5: Run tests and build
pnpm --filter backend test -- libs/configuration/src/environment.spec.ts
DATABASE_URL=postgresql://u:p@localhost:5432/db REDIS_URL=redis://localhost:6379 pnpm --filter backend build
Expected: PASS.
- Step 6: Commit
git add backend/libs/configuration backend/apps/api/src/api.module.ts backend/apps/worker/src/worker.module.ts
git commit -m "feat: validate backend runtime configuration"
Task 4: Scaffold the Angular 22 PWA with a Minimal Shell
Files:
- Create:
frontend/**via Angular CLI 22 - Modify:
frontend/src/app/app.component.ts - Modify:
frontend/src/app/app.component.html - Modify:
frontend/src/app/app.component.spec.ts - Create/Modify:
frontend/ngsw-config.json - Create/Modify:
frontend/src/manifest.webmanifest
Interfaces:
-
Consumes: root pnpm workspace.
-
Produces: static production bundle in
frontend/dist/frontend/browser; installable PWA shell; root app titleTravel Planner. -
Step 1: Generate the Angular application
Run from repository root:
pnpm dlx @angular/cli@22 new frontend --routing --style=scss --standalone --strict --skip-git --package-manager=pnpm --test-runner=vitest
cd frontend
pnpm exec ng add @angular/pwa --project frontend --skip-confirmation
cd ..
pnpm install
Expected: Angular workspace exists with Vitest tests and PWA service-worker configuration.
- Step 2: Replace the generated component test with product-shell expectations
Update frontend/src/app/app.component.spec.ts to assert:
import { TestBed } from '@angular/core/testing';
import { AppComponent } from './app.component';
describe('AppComponent', () => {
it('renders the Travel Planner shell', async () => {
await TestBed.configureTestingModule({ imports: [AppComponent] }).compileComponents();
const fixture = TestBed.createComponent(AppComponent);
await fixture.whenStable();
expect(fixture.nativeElement.textContent).toContain('Travel Planner');
expect(fixture.nativeElement.textContent).toContain('Reisen planen, gemeinsam entscheiden.');
});
});
- Step 3: Run the test and verify it fails against the generated template
pnpm --filter frontend test -- --watch=false
Expected: FAIL on the product copy assertion.
- Step 4: Implement the minimal app shell
Set AppComponent to a standalone component with no business state. Use this template:
<main class="app-shell">
<h1>Travel Planner</h1>
<p>Reisen planen, gemeinsam entscheiden.</p>
<router-outlet />
</main>
Keep styling minimal; Phase 11 owns final UI polish.
- Step 5: Verify test, production build, and PWA artifacts
pnpm --filter frontend test -- --watch=false
pnpm --filter frontend build
find frontend/dist -name ngsw.json -o -name manifest.webmanifest
Expected: tests PASS; production build succeeds; service-worker/PWA artifacts exist.
- Step 6: Commit
git add frontend pnpm-lock.yaml
git commit -m "feat: add angular pwa shell"
Task 5: Add PostgreSQL and Redis Development Infrastructure
Files:
- Create:
compose.dev.yml - Modify:
README.md - Test: manual Compose health verification in this task; invariant automation is added in Task 8.
Interfaces:
-
Consumes:
.env.examplenames. -
Produces: development services
postgreson container port 5432 andredison container port 6379, both healthy before dependent integration work. -
Step 1: Create the development Compose file
Create compose.dev.yml:
services:
postgres:
image: postgres:18.4-alpine
environment:
POSTGRES_DB: travel_planner
POSTGRES_USER: travel_planner
POSTGRES_PASSWORD: travel_planner_dev
ports:
- "127.0.0.1:5432:5432"
healthcheck:
test: ["CMD-SHELL", "pg_isready -U travel_planner -d travel_planner"]
interval: 2s
timeout: 3s
retries: 20
volumes:
- travel_postgres_dev:/var/lib/postgresql
redis:
image: redis:8.8.1-alpine
ports:
- "127.0.0.1:6379:6379"
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 2s
timeout: 3s
retries: 20
volumes:
travel_postgres_dev:
This file is development-only. The one-public-port invariant applies to production compose.yml, not this local dependency convenience file.
- Step 2: Start development infrastructure
pnpm dev:infra
docker compose -f compose.dev.yml ps
Expected: both services show healthy.
- Step 3: Verify connectivity
docker compose -f compose.dev.yml exec -T postgres pg_isready -U travel_planner -d travel_planner
docker compose -f compose.dev.yml exec -T redis redis-cli ping
Expected: PostgreSQL accepts connections; Redis returns PONG.
- Step 4: Document developer startup
Add to README.md the exact commands:
pnpm install
pnpm dev:infra
DATABASE_URL=postgresql://travel_planner:travel_planner_dev@localhost:5432/travel_planner REDIS_URL=redis://localhost:6379 pnpm --filter backend start:api
pnpm --filter frontend start
- Step 5: Commit
git add compose.dev.yml README.md
git commit -m "chore: add postgres and redis development services"
Task 6: Implement Liveness and Dependency-Aware Readiness
Files:
- Create:
backend/libs/infrastructure/src/postgres/postgres.module.ts - Create:
backend/libs/infrastructure/src/redis/redis.module.ts - Create:
backend/libs/infrastructure/src/index.ts - Create:
backend/apps/api/src/health/health.module.ts - Create:
backend/apps/api/src/health/readiness.service.ts - Create:
backend/apps/api/src/health/health.controller.ts - Create:
backend/apps/api/src/health/health.controller.spec.ts - Modify:
backend/apps/api/src/api.module.ts - Modify:
backend/apps/worker/src/worker.module.ts - Modify:
backend/apps/api/src/main.ts - Modify:
backend/package.json
Interfaces:
-
Consumes:
DATABASE_URL,REDIS_URLfrom configuration. -
Produces:
GET /health/livereturns 200 without checking external providers;GET /health/readyreturns 200 only when PostgreSQL and Redis are reachable; these routes remain outside/api/v1so infrastructure probes are stable. -
Step 1: Write failing controller tests
Create health.controller.spec.ts with a mocked readiness service:
import { Test } from '@nestjs/testing';
import { HealthController } from './health.controller';
import { ReadinessService } from './readiness.service';
describe('HealthController', () => {
it('returns liveness without dependency checks', async () => {
const readiness = { check: jest.fn() };
const moduleRef = await Test.createTestingModule({
controllers: [HealthController],
providers: [{ provide: ReadinessService, useValue: readiness }],
}).compile();
expect(moduleRef.get(HealthController).live()).toEqual({ status: 'ok' });
expect(readiness.check).not.toHaveBeenCalled();
});
it('delegates readiness to dependency checks', async () => {
const readiness = { check: jest.fn().mockResolvedValue({ status: 'ok' }) };
const moduleRef = await Test.createTestingModule({
controllers: [HealthController],
providers: [{ provide: ReadinessService, useValue: readiness }],
}).compile();
await expect(moduleRef.get(HealthController).ready()).resolves.toEqual({ status: 'ok' });
expect(readiness.check).toHaveBeenCalledTimes(1);
});
});
- Step 2: Run and verify failure
pnpm --filter backend test -- apps/api/src/health/health.controller.spec.ts
Expected: FAIL because health classes do not exist.
- Step 3: Add infrastructure dependencies
Install:
pnpm --filter backend add @nestjs/terminus pg ioredis
pnpm --filter backend add -D @types/pg
Implement POSTGRES_POOL and REDIS_CLIENT providers. The essential provider factories are:
export const POSTGRES_POOL = Symbol('POSTGRES_POOL');
export const REDIS_CLIENT = Symbol('REDIS_CLIENT');
export const postgresPoolProvider = {
provide: POSTGRES_POOL,
inject: [APP_ENVIRONMENT],
useFactory: (env: AppEnvironment) => new Pool({ connectionString: env.databaseUrl }),
};
export const redisClientProvider = {
provide: REDIS_CLIENT,
inject: [APP_ENVIRONMENT],
useFactory: (env: AppEnvironment) => new Redis(env.redisUrl, { lazyConnect: false }),
};
Wrap each provider in a Nest module and add lifecycle providers implementing OnModuleDestroy so the PostgreSQL pool calls end() and Redis calls quit(). Import both infrastructure modules in ApiModule and WorkerModule. Export typed injection tokens from libs/infrastructure/src/index.ts.
- Step 4: Implement readiness checks
ReadinessService.check() must execute:
await postgresPool.query('SELECT 1');
const pong = await redis.ping();
if (pong !== 'PONG') throw new Error('Redis ping failed');
return { status: 'ok' as const };
HealthController must define exactly:
@Get('/health/live')
live(): { status: 'ok' }
@Get('/health/ready')
ready(): Promise<{ status: 'ok' }>
Configure these infrastructure paths outside the global /api/v1 prefix in apps/api/src/main.ts:
import { RequestMethod } from '@nestjs/common';
app.setGlobalPrefix('api/v1', {
exclude: [
{ path: 'health/live', method: RequestMethod.GET },
{ path: 'health/ready', method: RequestMethod.GET },
],
});
Use @Controller() on HealthController with @Get('health/live') and @Get('health/ready').
- Step 5: Run unit tests
pnpm --filter backend test -- apps/api/src/health/health.controller.spec.ts
Expected: PASS.
- Step 6: Run API against development infrastructure
pnpm dev:infra
DATABASE_URL=postgresql://travel_planner:travel_planner_dev@localhost:5432/travel_planner REDIS_URL=redis://localhost:6379 pnpm --filter backend build:api
DATABASE_URL=postgresql://travel_planner:travel_planner_dev@localhost:5432/travel_planner REDIS_URL=redis://localhost:6379 node backend/dist/apps/api/main.js &
API_PID=$!
sleep 2
curl --fail http://127.0.0.1:3000/health/live
curl --fail http://127.0.0.1:3000/health/ready
kill "$API_PID"
Expected: both endpoints return HTTP 200 with {"status":"ok"}.
- Step 7: Commit
git add backend pnpm-lock.yaml
git commit -m "feat: add api liveness and readiness checks"
Task 7: Build Production API, Worker, and Edge Images
Files:
- Create:
docker/api.Dockerfile - Create:
docker/worker.Dockerfile - Create:
docker/edge.Dockerfile - Create:
docker/edge/nginx.conf - Create:
docker/edge/default.conf.template - Create:
compose.yml - Create:
docs/architecture/deployment.md
Interfaces:
-
Consumes: built backend and frontend workspaces; environment names from Task 1.
-
Produces: images
travel-api:${IMAGE_TAG},travel-worker:${IMAGE_TAG},travel-edge:${IMAGE_TAG}; production service DNS namesapi,worker,postgres,redis; edge HTTPS endpoint; internal API atapi:3000. -
Step 1: Create multi-stage API and worker Dockerfiles
Create docker/api.Dockerfile:
FROM node:24.18.0-bookworm-slim AS build
RUN corepack enable && corepack prepare pnpm@10.15.0 --activate
WORKDIR /app
COPY package.json pnpm-workspace.yaml pnpm-lock.yaml ./
COPY backend/package.json backend/package.json
COPY frontend/package.json frontend/package.json
RUN pnpm install --frozen-lockfile
COPY backend backend
RUN pnpm --filter backend build:api
FROM node:24.18.0-bookworm-slim AS runtime
ENV NODE_ENV=production
WORKDIR /app
COPY --from=build /app/node_modules ./node_modules
COPY --from=build /app/backend/node_modules ./backend/node_modules
COPY --from=build /app/backend/package.json ./backend/package.json
COPY --from=build /app/backend/dist ./backend/dist
USER node
EXPOSE 3000
CMD ["node", "backend/dist/apps/api/main.js"]
Create docker/worker.Dockerfile:
FROM node:24.18.0-bookworm-slim AS build
RUN corepack enable && corepack prepare pnpm@10.15.0 --activate
WORKDIR /app
COPY package.json pnpm-workspace.yaml pnpm-lock.yaml ./
COPY backend/package.json backend/package.json
COPY frontend/package.json frontend/package.json
RUN pnpm install --frozen-lockfile
COPY backend backend
RUN pnpm --filter backend build:worker
FROM node:24.18.0-bookworm-slim AS runtime
ENV NODE_ENV=production
WORKDIR /app
COPY --from=build /app/node_modules ./node_modules
COPY --from=build /app/backend/node_modules ./backend/node_modules
COPY --from=build /app/backend/package.json ./backend/package.json
COPY --from=build /app/backend/dist ./backend/dist
USER node
CMD ["node", "backend/dist/apps/worker/main.js"]
Do not add EXPOSE to the worker image. Neither image contains secrets.
- Step 2: Create the edge image
Create docker/edge.Dockerfile:
FROM node:24.18.0-bookworm-slim AS frontend-build
RUN corepack enable && corepack prepare pnpm@10.15.0 --activate
WORKDIR /app
COPY package.json pnpm-workspace.yaml pnpm-lock.yaml ./
COPY frontend/package.json frontend/package.json
COPY backend/package.json backend/package.json
RUN pnpm install --frozen-lockfile
COPY frontend frontend
RUN pnpm --filter frontend build
FROM nginx:1.29.8-alpine
COPY docker/edge/nginx.conf /etc/nginx/nginx.conf
COPY docker/edge/default.conf.template /etc/nginx/templates/default.conf.template
COPY --from=frontend-build /app/frontend/dist/frontend/browser /usr/share/nginx/html
EXPOSE 443
Create docker/edge/nginx.conf:
user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log notice;
pid /var/run/nginx.pid;
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
sendfile on;
keepalive_timeout 65;
include /etc/nginx/conf.d/*.conf;
}
The edge image listens on container port 443 only and expects TLS certificate/key mounts at /run/tls/tls.crt and /run/tls/tls.key.
- Step 3: Configure Nginx routing
default.conf.template must define:
server {
listen 443 ssl;
server_name _;
ssl_certificate /run/tls/tls.crt;
ssl_certificate_key /run/tls/tls.key;
root /usr/share/nginx/html;
index index.html;
location /api/ {
proxy_pass http://api:3000;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto https;
proxy_buffering off;
}
location /health/ {
proxy_pass http://api:3000;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto https;
}
location / {
try_files $uri $uri/ /index.html;
}
}
Do not add a port-80 server block.
- Step 4: Create the production Compose topology
Create compose.yml:
services:
edge:
image: "${REGISTRY}/travel-edge:${IMAGE_TAG}"
build:
context: .
dockerfile: docker/edge.Dockerfile
ports:
- "${APP_HTTPS_PORT:-443}:443"
volumes:
- "${TLS_CERT_FILE}:/run/tls/tls.crt:ro"
- "${TLS_KEY_FILE}:/run/tls/tls.key:ro"
depends_on:
api:
condition: service_healthy
networks: [travel]
restart: unless-stopped
api:
image: "${REGISTRY}/travel-api:${IMAGE_TAG}"
build:
context: .
dockerfile: docker/api.Dockerfile
expose:
- "3000"
environment:
DATABASE_URL: "postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB}"
REDIS_URL: "redis://redis:6379"
APP_VERSION: "${APP_VERSION:-dev}"
TEAMCITY_BUILD_NUMBER: "${TEAMCITY_BUILD_NUMBER:-local}"
SOURCE_REVISION: "${SOURCE_REVISION:-local}"
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
healthcheck:
test: ["CMD-SHELL", "node -e \"fetch('http://127.0.0.1:3000/health/ready').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))\""]
interval: 5s
timeout: 3s
retries: 20
start_period: 10s
networks: [travel]
restart: unless-stopped
worker:
image: "${REGISTRY}/travel-worker:${IMAGE_TAG}"
build:
context: .
dockerfile: docker/worker.Dockerfile
environment:
DATABASE_URL: "postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB}"
REDIS_URL: "redis://redis:6379"
APP_VERSION: "${APP_VERSION:-dev}"
TEAMCITY_BUILD_NUMBER: "${TEAMCITY_BUILD_NUMBER:-local}"
SOURCE_REVISION: "${SOURCE_REVISION:-local}"
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
networks: [travel]
restart: unless-stopped
postgres:
image: "postgres:${POSTGRES_IMAGE_TAG:-18.4-alpine}"
environment:
POSTGRES_DB: "${POSTGRES_DB}"
POSTGRES_USER: "${POSTGRES_USER}"
POSTGRES_PASSWORD: "${POSTGRES_PASSWORD}"
expose:
- "5432"
volumes:
- postgres_data:/var/lib/postgresql
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
interval: 5s
timeout: 3s
retries: 20
networks: [travel]
restart: unless-stopped
redis:
image: "redis:${REDIS_IMAGE_TAG:-8.8.1-alpine}"
expose:
- "6379"
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 3s
retries: 20
networks: [travel]
restart: unless-stopped
volumes:
postgres_data:
networks:
travel:
driver: bridge
worker has neither ports nor expose because it accepts no inbound traffic. The PostgreSQL 18 volume is mounted at /var/lib/postgresql, matching the official PostgreSQL 18 image layout. The edge is the only service with ports:. Do not add port 80 or network_mode: host.
- Step 5: Build all images locally
IMAGE_TAG=phase01 \
REGISTRY=local \
POSTGRES_DB=travel_planner \
POSTGRES_USER=travel_planner \
POSTGRES_PASSWORD=build-only \
TLS_CERT_FILE=/tmp/not-used-during-build.crt \
TLS_KEY_FILE=/tmp/not-used-during-build.key \
docker compose -f compose.yml build edge api worker
Expected: all three application images build successfully from one revision.
- Step 6: Document production topology and TLS contract
In docs/architecture/deployment.md, document:
-
only edge publishes a host port;
-
production requires a certificate and key mounted at the configured paths;
-
no port 80 requirement;
-
PostgreSQL/Redis/API are inaccessible from the host through Compose-published ports;
-
outbound API/worker egress remains allowed;
-
TeamCity supplies immutable image tags.
-
Step 7: Commit
git add docker compose.yml docs/architecture/deployment.md
git commit -m "feat: add single-port production docker topology"
Task 8: Enforce the Single-Published-Port Invariant Automatically
Files:
- Create:
scripts/verify-compose-invariants.mjs - Create:
tests/compose-invariants.test.mjs - Modify:
package.json - Modify: root dev dependencies/lockfile to include
yaml
Interfaces:
-
Consumes:
compose.yml. -
Produces:
verifyComposeInvariants(compose)function andpnpm test:compose; TeamCity can reject topology regressions before image build/deploy. -
Step 1: Add YAML parser and write failing invariant test
Install:
pnpm add -Dw yaml
Create tests/compose-invariants.test.mjs:
import test from 'node:test';
import assert from 'node:assert/strict';
import fs from 'node:fs';
import { parse } from 'yaml';
import { verifyComposeInvariants } from '../scripts/verify-compose-invariants.mjs';
test('production compose publishes exactly one edge port', () => {
const compose = parse(fs.readFileSync(new URL('../compose.yml', import.meta.url), 'utf8'));
assert.doesNotThrow(() => verifyComposeInvariants(compose));
});
test('invariant rejects a host port on postgres', () => {
const invalid = {
services: {
edge: { ports: ['443:443'] },
postgres: { ports: ['5432:5432'] },
},
};
assert.throws(() => verifyComposeInvariants(invalid), /postgres.*ports/i);
});
- Step 2: Run the test and verify failure
pnpm test:compose
Expected: FAIL because verifyComposeInvariants does not exist.
- Step 3: Implement the invariant validator
Create scripts/verify-compose-invariants.mjs:
export function verifyComposeInvariants(compose) {
const services = compose?.services ?? {};
const serviceNames = Object.keys(services);
const published = serviceNames.filter((name) => Array.isArray(services[name]?.ports) && services[name].ports.length > 0);
if (published.length !== 1 || published[0] !== 'edge') {
throw new Error(`Only edge may define ports; found: ${published.join(', ') || 'none'}`);
}
if (services.edge.ports.length !== 1) {
throw new Error(`edge must publish exactly one port; found ${services.edge.ports.length}`);
}
for (const [name, service] of Object.entries(services)) {
if (name !== 'edge' && Array.isArray(service.ports) && service.ports.length > 0) {
throw new Error(`${name} must not define ports`);
}
if (service.network_mode === 'host') {
throw new Error(`${name} must not use network_mode: host`);
}
}
}
- Step 4: Run the invariant test
pnpm test:compose
Expected: PASS.
- Step 5: Commit
git add package.json pnpm-lock.yaml scripts/verify-compose-invariants.mjs tests/compose-invariants.test.mjs
git commit -m "test: enforce production docker network invariants"
Task 9: Add TeamCity-Owned Repository Scripts for Validate, Build, Deploy, Smoke, and Rollback
Files:
- Create:
scripts/teamcity/validate.sh - Create:
scripts/teamcity/build-images.sh - Create:
scripts/teamcity/smoke.sh - Create:
scripts/teamcity/deploy.sh - Create:
scripts/teamcity/rollback.sh - Modify:
README.md - Modify:
docs/architecture/deployment.md
Interfaces:
-
Consumes: TeamCity environment variables
BUILD_NUMBER,BUILD_VCS_NUMBER, registry credentials supplied externally, deployment-host.env, and immutableIMAGE_TAG. -
Produces: stable shell entry points that an existing TeamCity pipeline can call without duplicating deployment logic.
-
Step 1: Create validation script
Create scripts/teamcity/validate.sh:
#!/usr/bin/env bash
set -euo pipefail
corepack enable
pnpm install --frozen-lockfile
pnpm lint
pnpm test
pnpm test:compose
pnpm build
Make executable:
chmod +x scripts/teamcity/validate.sh
- Step 2: Create immutable image build script
Create scripts/teamcity/build-images.sh:
#!/usr/bin/env bash
set -euo pipefail
: "${REGISTRY:?REGISTRY is required}"
: "${BUILD_NUMBER:?BUILD_NUMBER is required}"
: "${BUILD_VCS_NUMBER:?BUILD_VCS_NUMBER is required}"
IMAGE_TAG="${BUILD_NUMBER}-${BUILD_VCS_NUMBER}"
export IMAGE_TAG
if [[ "$IMAGE_TAG" == "latest" ]]; then
echo "Refusing floating deployment tag" >&2
exit 1
fi
docker build --pull -f docker/edge.Dockerfile -t "${REGISTRY}/travel-edge:${IMAGE_TAG}" .
docker build --pull -f docker/api.Dockerfile -t "${REGISTRY}/travel-api:${IMAGE_TAG}" .
docker build --pull -f docker/worker.Dockerfile -t "${REGISTRY}/travel-worker:${IMAGE_TAG}" .
docker push "${REGISTRY}/travel-edge:${IMAGE_TAG}"
docker push "${REGISTRY}/travel-api:${IMAGE_TAG}"
docker push "${REGISTRY}/travel-worker:${IMAGE_TAG}"
printf '%s\n' "$IMAGE_TAG"
Make it executable. Registry authentication is supplied by the TeamCity Docker/registry connection before this script runs; credentials are not arguments or source-controlled values.
- Step 3: Create smoke script against the single edge port
Create scripts/teamcity/smoke.sh:
#!/usr/bin/env bash
set -euo pipefail
: "${APP_BASE_URL:?APP_BASE_URL is required}"
curl --fail --silent --show-error "${APP_BASE_URL}/health/live"
curl --fail --silent --show-error "${APP_BASE_URL}/health/ready"
curl --fail --silent --show-error "${APP_BASE_URL}/" >/dev/null
Make it executable.
- Step 4: Create deployment script
Create scripts/teamcity/deploy.sh:
#!/usr/bin/env bash
set -euo pipefail
: "${IMAGE_TAG:?IMAGE_TAG is required}"
: "${REGISTRY:?REGISTRY is required}"
: "${APP_BASE_URL:?APP_BASE_URL is required}"
exec 9>/var/lock/travel-planner-deploy.lock
flock -n 9 || { echo "Another deployment is already running" >&2; exit 1; }
if [[ -f .deployed-image-tag ]]; then
cp .deployed-image-tag .previous-image-tag
fi
export IMAGE_TAG REGISTRY
docker compose pull edge api worker postgres redis
docker compose up -d postgres redis --wait --wait-timeout 120
docker compose run --rm --no-deps api node backend/dist/apps/api/migration.js
docker compose up -d --remove-orphans --wait --wait-timeout 120
scripts/teamcity/smoke.sh
printf '%s\n' "$IMAGE_TAG" > .deployed-image-tag
Make it executable. The target working directory contains compose.yml and the protected production .env used by Docker Compose. Do not add docker compose down.
- Step 5: Create rollback script
Create scripts/teamcity/rollback.sh:
#!/usr/bin/env bash
set -euo pipefail
: "${REGISTRY:?REGISTRY is required}"
: "${APP_BASE_URL:?APP_BASE_URL is required}"
if [[ ! -s .previous-image-tag ]]; then
echo "No previous image tag is available for rollback" >&2
exit 1
fi
IMAGE_TAG="$(cat .previous-image-tag)"
export IMAGE_TAG REGISTRY
docker compose pull edge api worker
docker compose up -d --remove-orphans --wait --wait-timeout 120
scripts/teamcity/smoke.sh
printf '%s\n' "$IMAGE_TAG" > .deployed-image-tag
Make it executable. Rollback never performs an automatic database downgrade.
- Step 6: Add the Phase-01 compiled migration entry point
Create backend/apps/api/src/migration.ts:
export async function runMigrations(): Promise<void> {
console.log('No migrations configured in Phase 01');
}
if (require.main === module) {
void runMigrations();
}
Verify that pnpm --filter backend build:api produces backend/dist/apps/api/migration.js. Phase 02 replaces the body with the real versioned migration runner while preserving this container command contract.
- Step 7: Verify scripts syntactically and run validation
chmod +x scripts/teamcity/*.sh
bash -n scripts/teamcity/validate.sh
bash -n scripts/teamcity/build-images.sh
bash -n scripts/teamcity/deploy.sh
bash -n scripts/teamcity/rollback.sh
bash -n scripts/teamcity/smoke.sh
scripts/teamcity/validate.sh
Expected: shell syntax passes; validation passes.
- Step 8: Document TeamCity wiring
Add a table to docs/architecture/deployment.md:
TeamCity stage Repository entry point
Validate scripts/teamcity/validate.sh
Build + Push scripts/teamcity/build-images.sh
Deploy over SSH scripts/teamcity/deploy.sh
Post-deploy smoke scripts/teamcity/smoke.sh
Rollback scripts/teamcity/rollback.sh
Document that the existing TeamCity project may configure these as command-line/SSH steps; deployment logic must remain in version control.
- Step 9: Commit
git add scripts/teamcity backend/package.json README.md docs/architecture/deployment.md
git commit -m "ci: add teamcity build and deployment entry points"
Task 10: Add Build Metadata Endpoint and Complete Phase-01 Verification
Files:
- Create:
backend/apps/api/src/version/version.controller.ts - Create:
backend/apps/api/src/version/version.controller.spec.ts - Create:
backend/apps/api/src/version/version.module.ts - Modify:
backend/apps/api/src/api.module.ts - Modify:
README.md
Interfaces:
-
Consumes: validated build metadata from
AppEnvironment. -
Produces: safe
GET /api/v1/versionresponse withappVersion,teamCityBuildNumber, andsourceRevision; no secrets. -
Step 1: Write the failing version-controller test
Create a test that injects:
{
appVersion: '1.0.0',
teamCityBuildNumber: '123',
sourceRevision: 'abc123'
}
and expects exactly:
{
"appVersion": "1.0.0",
"teamCityBuildNumber": "123",
"sourceRevision": "abc123"
}
Assert that databaseUrl and redisUrl are absent.
- Step 2: Run and verify failure
pnpm --filter backend test -- apps/api/src/version/version.controller.spec.ts
Expected: FAIL because version module/controller do not exist.
- Step 3: Implement the safe version endpoint
Create VersionController at /version under the existing /api/v1 prefix. Return only the three safe metadata fields from the validated environment provider.
- Step 4: Run complete repository verification
pnpm install --frozen-lockfile
pnpm lint
pnpm test
pnpm test:compose
pnpm build
Expected: all PASS.
- Step 5: Verify production Compose rendering
APP_HTTPS_PORT=443 \
IMAGE_TAG=phase01 \
REGISTRY=example.invalid \
POSTGRES_IMAGE_TAG=18.4-alpine \
REDIS_IMAGE_TAG=8.8.1-alpine \
POSTGRES_DB=travel_planner \
POSTGRES_USER=travel_planner \
POSTGRES_PASSWORD=render-only \
TLS_CERT_FILE=/tmp/travel-tls/tls.crt \
TLS_KEY_FILE=/tmp/travel-tls/tls.key \
docker compose -f compose.yml config > /tmp/travel-compose-rendered.yml
grep -n "ports:" /tmp/travel-compose-rendered.yml
Expected: the rendered production configuration has only one ports: section and it belongs to edge.
- Step 6: Run an end-to-end local container smoke test with a test certificate
Generate a disposable certificate outside source control:
mkdir -p /tmp/travel-tls
openssl req -x509 -newkey rsa:2048 -nodes -days 1 \
-keyout /tmp/travel-tls/tls.key \
-out /tmp/travel-tls/tls.crt \
-subj "/CN=localhost"
Start the stack with TLS_CERT_FILE=/tmp/travel-tls/tls.crt, TLS_KEY_FILE=/tmp/travel-tls/tls.key, and APP_HTTPS_PORT=8443, then verify:
curl --insecure --fail https://127.0.0.1:8443/health/live
curl --insecure --fail https://127.0.0.1:8443/health/ready
curl --insecure --fail https://127.0.0.1:8443/
Expected: all three requests succeed through edge; no direct host URL exists for API, PostgreSQL, Redis, or worker in the production Compose file.
- Step 7: Update README with Phase-01 completion commands
Document:
-
developer startup;
-
production environment names;
-
single published-port rule;
-
TeamCity script entry points;
-
health/version URLs;
-
compiled no-op migration entry point is temporary until Phase 02 introduces real migrations.
-
Step 8: Commit
git add backend/apps/api/src/version backend/apps/api/src/api.module.ts README.md
git commit -m "feat: expose safe build metadata and verify foundation"
Phase 01 Acceptance Checklist
Run this checklist before starting Phase 02:
pnpm install --frozen-lockfile
pnpm lint
pnpm test
pnpm test:compose
pnpm build
bash -n scripts/teamcity/*.sh
All commands must pass.
Then verify:
- Angular 22 PWA builds and contains service-worker artifacts.
- NestJS API builds and listens internally on port 3000.
- NestJS worker boots as an application context and has no HTTP listener.
/health/livedoes not depend on PostgreSQL/Redis/Mistral/external APIs./health/readyvalidates PostgreSQL and Redis only./api/v1/versionexposes only safe build metadata.compose.ymlcontains servicesedge,api,worker,postgres,redis.- Only
edgedefinesports:and defines exactly one published TCP port. - Production Compose does not publish port 80.
api,postgres, andredisuse only internal ports/expose semantics.workerhas no inbound port.- No service uses
network_mode: host. - PostgreSQL image is initially pinned to
18.4-alpineand Redis to8.8.1-alpine. - TeamCity scripts build immutable images from one revision.
- Deploy script pulls, runs migration hook, runs
compose up -d --remove-orphans, and smoke-tests through the edge URL. - Deploy script never calls
docker compose down. - Rollback reuses the prior immutable image tag and does not attempt automatic DB downgrade.
- No secret values are committed or built into frontend/images.
Phase 01 Review Boundary
Do not start OIDC, users, trips, Mistral, agent tools, web research, or business migrations during this phase. Phase 02 begins only after the Phase 01 acceptance checklist is reviewed and green.
Addendum: Adjustments Against the Pre-Existing Scaffold (2026-08-17)
The repository already contains a generated Angular project (frontend/, Angular 21.2.0, npm) and a generated NestJS project (backend/, NestJS 11, single-app layout, ts-jest). The master instructions explicitly forbid re-scaffolding either project. This addendum documents the small, non-architectural adjustments applied to this plan as a result — per "kleine technische Details darfst du selbst sinnvoll lösen", no user decision was required:
- Task 1 — pnpm workspace wraps the existing
frontend/andbackend/directories; nothing there is regenerated. - Task 2 —
backend/src/{app.module.ts,app.controller.ts,app.controller.spec.ts,app.service.ts,main.ts}are migrated (moved + adapted) intobackend/apps/api/src/, not regenerated from a schematic.nest-cli.jsonis converted tomonorepo: truewithapi+workerprojects, matching the target file structure. The existingAppController/AppServiceare kept as the initialApiModulecontent instead of an empty module, since deleting working, tested code has no benefit. - Test runner — the existing backend Jest config uses
ts-jest, not@swc/jest, and passes today. Per "bestehende funktionierende Konfiguration bevorzugt weiterverwenden",ts-jestis kept for Phase 01 instead of migrating to SWC; this is a test-runner detail, not an architectural one, and can be revisited later if build performance requires it. - Task 4 — no
ng new/ng add @angular/pwafull re-scaffold. The existing Angular 21.2 project is kept and extended in place:pnpm add -D @angular/pwaequivalent (ng add @angular/pwa) is run against the existing project to layer in the service worker/manifest without touching unrelated generated files. The project already uses the newer Angular file-naming convention (app.ts/app.html/app.scss/app.spec.tsinstead ofapp.component.tsetc.); all plan steps referencingapp.component.*apply to these equivalently-named files instead. - Angular version — kept at 21.2.x (already installed) rather than upgraded to 22.x. Upgrading a working, freshly generated Angular major version is out of scope for "establish the foundation" and is not required by any Phase 01 acceptance criterion; the roadmap's "Angular 22.x" baseline is a target for new projects, not a mandate to upgrade an existing working one mid-foundation. Revisit as an explicit, isolated upgrade task if a later phase needs an Angular-22-only feature.
- package manager pin — both existing projects pin
packageManagerfields (npm@11.12.1). These are superseded by the rootpackageManager: pnpm@...per the master instructions (Abschnitt 52); the per-package fields are removed during the pnpm migration to avoid Corepack conflicts.
No product/business behavior changes result from these adjustments; they only change how the target file structure is reached.