From 0196de59a411904f26b7b69bd4b19caeead47891 Mon Sep 17 00:00:00 2001 From: Danijel Martinek Date: Mon, 6 Apr 2026 14:38:33 +0200 Subject: [PATCH] Add Plan 3: Payload CMS Integration implementation plan 10 tasks covering: cms-core dependencies, Users/Articles/Media collections, slug auto-generation hook, SiteSettings global, Payload config, dual-mode cms-client (local + HTTP), thin Next.js admin shell, build verification. --- .../plans/2026-04-06-plan-3-payload-cms.md | 977 ++++++++++++++++++ 1 file changed, 977 insertions(+) create mode 100644 docs/superpowers/plans/2026-04-06-plan-3-payload-cms.md diff --git a/docs/superpowers/plans/2026-04-06-plan-3-payload-cms.md b/docs/superpowers/plans/2026-04-06-plan-3-payload-cms.md new file mode 100644 index 0000000..aacf4f5 --- /dev/null +++ b/docs/superpowers/plans/2026-04-06-plan-3-payload-cms.md @@ -0,0 +1,977 @@ +# Plan 3: Payload CMS Integration — 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:** Implement Payload CMS integration: `@repo/cms-core` (config, collections, hooks), `@repo/cms-client` (dual-mode local+HTTP client with generated types), and `apps/cms` (thin Next.js shell serving the Payload admin panel). + +**Architecture:** Payload config and collections live in `@repo/cms-core` — standalone, testable, framework-agnostic. `@repo/cms-client` provides a dual-mode client (Local API primary, HTTP fallback) that receives a Payload instance via injection. `apps/cms` is a thin Next.js 15 shell that imports config from cms-core and serves the admin panel. Type generation runs `payload generate:types` against cms-core's config. + +**Tech Stack:** Payload CMS 3.x, @payloadcms/db-postgres, @payloadcms/next, @payloadcms/richtext-lexical, Next.js 15, PostgreSQL 16, sharp + +--- + +## File Map + +### packages/cms-core +| File | Responsibility | +|---|---| +| `packages/cms-core/package.json` | Dependencies: payload, @payloadcms/db-postgres, @payloadcms/richtext-lexical | +| `packages/cms-core/src/payload.config.ts` | Root Payload config (db, editor, collections, globals) | +| `packages/cms-core/src/collections/users/index.ts` | Users collection with auth enabled | +| `packages/cms-core/src/collections/articles/index.ts` | Articles collection config | +| `packages/cms-core/src/collections/articles/fields.ts` | Article field definitions | +| `packages/cms-core/src/collections/articles/hooks/before-change.ts` | Slug auto-generation hook | +| `packages/cms-core/src/collections/media/index.ts` | Media collection with uploads | +| `packages/cms-core/src/globals/site-settings.ts` | Site settings global | +| `packages/cms-core/src/index.ts` | Exports config + all collections | + +### packages/cms-client +| File | Responsibility | +|---|---| +| `packages/cms-client/package.json` | Dependencies: payload (types only) | +| `packages/cms-client/src/client.ts` | createPayloadClient() factory | +| `packages/cms-client/src/local-client.ts` | LocalPayloadClient — wraps Payload instance | +| `packages/cms-client/src/http-client.ts` | HTTPPayloadClient — REST API fallback | +| `packages/cms-client/src/types.ts` | Shared client types (PayloadClient interface) | +| `packages/cms-client/src/index.ts` | Exports | + +### apps/cms +| File | Responsibility | +|---|---| +| `apps/cms/package.json` | Dependencies: next, payload, @payloadcms/next, @repo/cms-core | +| `apps/cms/next.config.mjs` | Next.js config wrapped with withPayload | +| `apps/cms/src/app/(payload)/admin/[[...segments]]/page.tsx` | Admin panel catch-all route | +| `apps/cms/src/app/(payload)/admin/[[...segments]]/not-found.tsx` | Admin 404 page | +| `apps/cms/src/app/(payload)/layout.tsx` | Payload layout with RootLayout | +| `apps/cms/src/app/(payload)/custom.scss` | Empty custom styles | +| `apps/cms/src/payload-types.ts` | Generated types (via payload generate:types) | + +--- + +### Task 1: Install cms-core dependencies + +**Files:** +- Modify: `packages/cms-core/package.json` +- Modify: `packages/cms-core/tsconfig.json` + +- [ ] **Step 1: Update packages/cms-core/package.json** + +```json +{ + "name": "@repo/cms-core", + "private": true, + "version": "0.0.0", + "type": "module", + "main": "./src/index.ts", + "types": "./src/index.ts", + "scripts": { + "build": "tsc --noEmit", + "lint": "eslint .", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "payload": "^3.14.0", + "@payloadcms/db-postgres": "^3.14.0", + "@payloadcms/richtext-lexical": "^3.14.0" + }, + "devDependencies": { + "@repo/eslint-config": "workspace:*", + "@repo/typescript-config": "workspace:*", + "@types/node": "^22.0.0" + } +} +``` + +- [ ] **Step 2: Update tsconfig.json** + +```json +{ + "extends": "@repo/typescript-config/base.json", + "compilerOptions": { + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "jsx": "react-jsx", + "baseUrl": ".", + "paths": { + "@/*": ["./src/*"] + } + }, + "include": ["src/**/*.ts", "src/**/*.tsx"], + "exclude": ["node_modules", "dist"] +} +``` + +- [ ] **Step 3: Run pnpm install** + +Run: `pnpm install` +Expected: Payload and its dependencies install successfully. + +- [ ] **Step 4: Commit** + +```bash +git add packages/cms-core/package.json packages/cms-core/tsconfig.json pnpm-lock.yaml +git commit -m "feat(cms-core): add Payload CMS dependencies" +``` + +--- + +### Task 2: Users collection + +**Files:** +- Create: `packages/cms-core/src/collections/users/index.ts` + +- [ ] **Step 1: Create Users collection** + +```typescript +import type { CollectionConfig } from "payload"; + +export const Users: CollectionConfig = { + slug: "users", + auth: true, + admin: { + useAsTitle: "email", + }, + fields: [ + { + name: "displayName", + type: "text", + }, + { + name: "role", + type: "select", + options: [ + { label: "Admin", value: "admin" }, + { label: "Editor", value: "editor" }, + { label: "Author", value: "author" }, + ], + defaultValue: "author", + required: true, + }, + ], +}; +``` + +- [ ] **Step 2: Commit** + +```bash +git add packages/cms-core/src/collections/users/ +git commit -m "feat(cms-core): add Users collection with auth" +``` + +--- + +### Task 3: Articles collection with fields and hooks + +**Files:** +- Create: `packages/cms-core/src/collections/articles/fields.ts` +- Create: `packages/cms-core/src/collections/articles/hooks/before-change.ts` +- Create: `packages/cms-core/src/collections/articles/index.ts` + +- [ ] **Step 1: Create fields.ts** + +```typescript +import type { Field } from "payload"; + +export const articleFields: Field[] = [ + { + name: "title", + type: "text", + required: true, + maxLength: 255, + }, + { + name: "slug", + type: "text", + unique: true, + admin: { + position: "sidebar", + description: "Auto-generated from title if left empty", + }, + }, + { + name: "content", + type: "richText", + }, + { + name: "status", + type: "select", + options: [ + { label: "Draft", value: "draft" }, + { label: "Published", value: "published" }, + ], + defaultValue: "draft", + required: true, + admin: { + position: "sidebar", + }, + }, + { + name: "author", + type: "relationship", + relationTo: "users", + required: true, + admin: { + position: "sidebar", + }, + }, + { + name: "featuredImage", + type: "upload", + relationTo: "media", + }, + { + name: "publishedAt", + type: "date", + admin: { + position: "sidebar", + date: { + pickerAppearance: "dayAndTime", + }, + }, + }, +]; +``` + +- [ ] **Step 2: Create hooks/before-change.ts** + +This is a CMS-operational hook (slug auto-generation) — stays in cms-core per the design spec. + +```typescript +import type { CollectionBeforeChangeHook } from "payload"; + +function generateSlug(title: string): string { + return title + .toLowerCase() + .replace(/[^a-z0-9]+/g, "-") + .replace(/^-|-$/g, ""); +} + +export const autoGenerateSlug: CollectionBeforeChangeHook = ({ + data, + operation, +}) => { + if (operation === "create" || operation === "update") { + if (data && data.title && !data.slug) { + data.slug = generateSlug(data.title); + } + } + return data; +}; +``` + +- [ ] **Step 3: Create articles/index.ts** + +```typescript +import type { CollectionConfig } from "payload"; + +import { articleFields } from "./fields.js"; +import { autoGenerateSlug } from "./hooks/before-change.js"; + +export const Articles: CollectionConfig = { + slug: "articles", + admin: { + useAsTitle: "title", + defaultColumns: ["title", "status", "author", "updatedAt"], + }, + hooks: { + beforeChange: [autoGenerateSlug], + }, + versions: { + drafts: true, + }, + fields: articleFields, +}; +``` + +- [ ] **Step 4: Commit** + +```bash +git add packages/cms-core/src/collections/articles/ +git commit -m "feat(cms-core): add Articles collection with slug auto-generation hook" +``` + +--- + +### Task 4: Media collection + +**Files:** +- Create: `packages/cms-core/src/collections/media/index.ts` + +- [ ] **Step 1: Create Media collection** + +```typescript +import type { CollectionConfig } from "payload"; + +export const Media: CollectionConfig = { + slug: "media", + upload: { + mimeTypes: ["image/*", "application/pdf"], + }, + admin: { + useAsTitle: "filename", + }, + fields: [ + { + name: "alt", + type: "text", + required: true, + }, + ], +}; +``` + +- [ ] **Step 2: Commit** + +```bash +git add packages/cms-core/src/collections/media/ +git commit -m "feat(cms-core): add Media collection with uploads" +``` + +--- + +### Task 5: Site settings global + +**Files:** +- Create: `packages/cms-core/src/globals/site-settings.ts` + +- [ ] **Step 1: Create site-settings.ts** + +```typescript +import type { GlobalConfig } from "payload"; + +export const SiteSettings: GlobalConfig = { + slug: "site-settings", + admin: { + group: "Settings", + }, + fields: [ + { + name: "siteName", + type: "text", + required: true, + defaultValue: "My App", + }, + { + name: "siteDescription", + type: "textarea", + }, + ], +}; +``` + +- [ ] **Step 2: Commit** + +```bash +git add packages/cms-core/src/globals/ +git commit -m "feat(cms-core): add SiteSettings global" +``` + +--- + +### Task 6: Payload config + exports + +**Files:** +- Create: `packages/cms-core/src/payload.config.ts` +- Modify: `packages/cms-core/src/index.ts` + +- [ ] **Step 1: Create payload.config.ts** + +```typescript +import { buildConfig } from "payload"; +import { postgresAdapter } from "@payloadcms/db-postgres"; +import { lexicalEditor } from "@payloadcms/richtext-lexical"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +import { Users } from "./collections/users/index.js"; +import { Articles } from "./collections/articles/index.js"; +import { Media } from "./collections/media/index.js"; +import { SiteSettings } from "./globals/site-settings.js"; + +const filename = fileURLToPath(import.meta.url); +const dirname = path.dirname(filename); + +export default buildConfig({ + editor: lexicalEditor(), + collections: [Users, Articles, Media], + globals: [SiteSettings], + secret: process.env.PAYLOAD_SECRET || "default-secret-change-me", + db: postgresAdapter({ + pool: { + connectionString: + process.env.DATABASE_URL || + "postgresql://postgres:postgres@localhost:5432/template", + }, + }), + typescript: { + outputFile: path.resolve(dirname, "payload-types.ts"), + }, +}); +``` + +- [ ] **Step 2: Update src/index.ts** + +```typescript +export { Users } from "./collections/users/index.js"; +export { Articles } from "./collections/articles/index.js"; +export { Media } from "./collections/media/index.js"; +export { SiteSettings } from "./globals/site-settings.js"; +export { default as config } from "./payload.config.js"; +``` + +- [ ] **Step 3: Commit** + +```bash +git add packages/cms-core/src/payload.config.ts packages/cms-core/src/index.ts +git commit -m "feat(cms-core): add Payload config with postgres adapter and lexical editor" +``` + +--- + +### Task 7: cms-client — dual-mode Payload client + +**Files:** +- Modify: `packages/cms-client/package.json` +- Create: `packages/cms-client/src/types.ts` +- Create: `packages/cms-client/src/local-client.ts` +- Create: `packages/cms-client/src/http-client.ts` +- Create: `packages/cms-client/src/client.ts` +- Modify: `packages/cms-client/src/index.ts` + +- [ ] **Step 1: Update packages/cms-client/package.json** + +```json +{ + "name": "@repo/cms-client", + "private": true, + "version": "0.0.0", + "type": "module", + "main": "./src/index.ts", + "types": "./src/index.ts", + "scripts": { + "build": "tsc --noEmit", + "lint": "eslint .", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "payload": "^3.14.0" + }, + "devDependencies": { + "@repo/eslint-config": "workspace:*", + "@repo/typescript-config": "workspace:*", + "@types/node": "^22.0.0" + } +} +``` + +- [ ] **Step 2: Create src/types.ts** + +```typescript +export interface FindOptions { + where?: Record; + sort?: string; + limit?: number; + page?: number; + depth?: number; + locale?: string; +} + +export interface PayloadClientResult { + docs: T[]; + totalDocs: number; + limit: number; + totalPages: number; + page: number; + pagingCounter: number; + hasPrevPage: boolean; + hasNextPage: boolean; + prevPage: number | null; + nextPage: number | null; +} + +export interface PayloadClient { + find>( + collection: string, + options?: FindOptions + ): Promise>; + + findByID>( + collection: string, + id: string, + options?: { depth?: number } + ): Promise; + + create>( + collection: string, + data: Record, + options?: { depth?: number } + ): Promise; + + update>( + collection: string, + id: string, + data: Record, + options?: { depth?: number } + ): Promise; + + delete>( + collection: string, + id: string + ): Promise; +} +``` + +- [ ] **Step 3: Create src/local-client.ts** + +```typescript +import type { Payload } from "payload"; +import type { FindOptions, PayloadClient, PayloadClientResult } from "./types.js"; + +export class LocalPayloadClient implements PayloadClient { + constructor(private payload: Payload) {} + + async find>( + collection: string, + options?: FindOptions + ): Promise> { + const result = await this.payload.find({ + collection: collection as any, + where: options?.where as any, + sort: options?.sort, + limit: options?.limit, + page: options?.page, + depth: options?.depth, + locale: options?.locale as any, + }); + return result as unknown as PayloadClientResult; + } + + async findByID>( + collection: string, + id: string, + options?: { depth?: number } + ): Promise { + const result = await this.payload.findByID({ + collection: collection as any, + id, + depth: options?.depth, + }); + return result as unknown as T; + } + + async create>( + collection: string, + data: Record, + options?: { depth?: number } + ): Promise { + const result = await this.payload.create({ + collection: collection as any, + data: data as any, + depth: options?.depth, + }); + return result as unknown as T; + } + + async update>( + collection: string, + id: string, + data: Record, + options?: { depth?: number } + ): Promise { + const result = await this.payload.update({ + collection: collection as any, + id, + data: data as any, + depth: options?.depth, + }); + return result as unknown as T; + } + + async delete>( + collection: string, + id: string + ): Promise { + const result = await this.payload.delete({ + collection: collection as any, + id, + }); + return result as unknown as T; + } +} +``` + +- [ ] **Step 4: Create src/http-client.ts** + +```typescript +import type { FindOptions, PayloadClient, PayloadClientResult } from "./types.js"; + +export class HTTPPayloadClient implements PayloadClient { + constructor(private baseURL: string) {} + + private async request(path: string, options?: RequestInit): Promise { + const response = await fetch(`${this.baseURL}${path}`, { + headers: { "Content-Type": "application/json" }, + ...options, + }); + if (!response.ok) { + throw new Error(`Payload API error: ${response.status} ${response.statusText}`); + } + return response.json() as Promise; + } + + async find>( + collection: string, + options?: FindOptions + ): Promise> { + const params = new URLSearchParams(); + if (options?.limit) params.set("limit", String(options.limit)); + if (options?.page) params.set("page", String(options.page)); + if (options?.sort) params.set("sort", options.sort); + if (options?.depth) params.set("depth", String(options.depth)); + if (options?.where) params.set("where", JSON.stringify(options.where)); + const query = params.toString(); + return this.request>( + `/api/${collection}${query ? `?${query}` : ""}` + ); + } + + async findByID>( + collection: string, + id: string, + options?: { depth?: number } + ): Promise { + const params = new URLSearchParams(); + if (options?.depth) params.set("depth", String(options.depth)); + const query = params.toString(); + return this.request( + `/api/${collection}/${id}${query ? `?${query}` : ""}` + ); + } + + async create>( + collection: string, + data: Record, + options?: { depth?: number } + ): Promise { + const params = new URLSearchParams(); + if (options?.depth) params.set("depth", String(options.depth)); + const query = params.toString(); + return this.request( + `/api/${collection}${query ? `?${query}` : ""}`, + { method: "POST", body: JSON.stringify(data) } + ); + } + + async update>( + collection: string, + id: string, + data: Record, + options?: { depth?: number } + ): Promise { + const params = new URLSearchParams(); + if (options?.depth) params.set("depth", String(options.depth)); + const query = params.toString(); + return this.request( + `/api/${collection}/${id}${query ? `?${query}` : ""}`, + { method: "PATCH", body: JSON.stringify(data) } + ); + } + + async delete>( + collection: string, + id: string + ): Promise { + return this.request(`/api/${collection}/${id}`, { method: "DELETE" }); + } +} +``` + +- [ ] **Step 5: Create src/client.ts** + +```typescript +import type { Payload } from "payload"; +import type { PayloadClient } from "./types.js"; +import { LocalPayloadClient } from "./local-client.js"; +import { HTTPPayloadClient } from "./http-client.js"; + +type PayloadClientOptions = + | { mode: "local"; payload: Payload } + | { mode: "http"; baseURL: string }; + +export function createPayloadClient(options: PayloadClientOptions): PayloadClient { + if (options.mode === "local") { + return new LocalPayloadClient(options.payload); + } + return new HTTPPayloadClient(options.baseURL); +} +``` + +- [ ] **Step 6: Update src/index.ts** + +```typescript +export { createPayloadClient } from "./client.js"; +export { LocalPayloadClient } from "./local-client.js"; +export { HTTPPayloadClient } from "./http-client.js"; +export type { + PayloadClient, + PayloadClientResult, + FindOptions, +} from "./types.js"; +``` + +- [ ] **Step 7: Run pnpm install and commit** + +Run: `pnpm install` + +```bash +git add packages/cms-client/ pnpm-lock.yaml +git commit -m "feat(cms-client): add dual-mode Payload client (local + HTTP)" +``` + +--- + +### Task 8: apps/cms — thin Next.js shell + +**Files:** +- Modify: `apps/cms/package.json` +- Create: `apps/cms/next.config.mjs` +- Create: `apps/cms/src/app/(payload)/admin/[[...segments]]/page.tsx` +- Create: `apps/cms/src/app/(payload)/admin/[[...segments]]/not-found.tsx` +- Create: `apps/cms/src/app/(payload)/layout.tsx` +- Create: `apps/cms/src/app/(payload)/custom.scss` +- Modify: `apps/cms/tsconfig.json` + +- [ ] **Step 1: Update apps/cms/package.json** + +```json +{ + "name": "@repo/cms", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "build": "next build", + "dev": "next dev --port 3001", + "lint": "eslint .", + "typecheck": "tsc --noEmit", + "generate:types": "payload generate:types" + }, + "dependencies": { + "@payloadcms/next": "^3.14.0", + "@payloadcms/ui": "^3.14.0", + "@repo/cms-core": "workspace:*", + "next": "^15.3.0", + "payload": "^3.14.0", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "sharp": "^0.33.0" + }, + "devDependencies": { + "@repo/eslint-config": "workspace:*", + "@repo/typescript-config": "workspace:*", + "@types/node": "^22.0.0", + "@types/react": "^19.0.0", + "@types/react-dom": "^19.0.0" + } +} +``` + +- [ ] **Step 2: Update apps/cms/tsconfig.json** + +```json +{ + "extends": "@repo/typescript-config/nextjs.json", + "compilerOptions": { + "baseUrl": ".", + "paths": { + "@/*": ["./src/*"], + "@payload-config": ["../../packages/cms-core/src/payload.config.ts"] + } + }, + "include": ["src/**/*.ts", "src/**/*.tsx", "next-env.d.ts"], + "exclude": ["node_modules"] +} +``` + +- [ ] **Step 3: Create next.config.mjs** + +```javascript +import { withPayload } from "@payloadcms/next/withPayload"; + +/** @type {import('next').NextConfig} */ +const nextConfig = {}; + +export default withPayload(nextConfig); +``` + +- [ ] **Step 4: Create admin catch-all page** + +```tsx +// apps/cms/src/app/(payload)/admin/[[...segments]]/page.tsx +/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */ +/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */ +import type { Metadata } from "next"; + +import config from "@payload-config"; +import { RootPage, generatePageMetadata } from "@payloadcms/next/views"; +import { importMap } from "../importMap.js"; + +type Args = { + params: Promise<{ segments: string[] }>; + searchParams: Promise>; +}; + +export const generateMetadata = ({ + params, + searchParams, +}: Args): Promise => + generatePageMetadata({ config, params, searchParams }); + +const Page = ({ params, searchParams }: Args) => + RootPage({ config, importMap, params, searchParams }); + +export default Page; +``` + +- [ ] **Step 5: Create admin not-found page** + +```tsx +// apps/cms/src/app/(payload)/admin/[[...segments]]/not-found.tsx +/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */ +/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */ +import type { Metadata } from "next"; + +import config from "@payload-config"; +import { NotFoundPage, generatePageMetadata } from "@payloadcms/next/views"; +import { importMap } from "../importMap.js"; + +type Args = { + params: Promise<{ segments: string[] }>; + searchParams: Promise>; +}; + +export const generateMetadata = ({ + params, + searchParams, +}: Args): Promise => + generatePageMetadata({ config, params, searchParams }); + +const NotFound = ({ params, searchParams }: Args) => + NotFoundPage({ config, importMap, params, searchParams }); + +export default NotFound; +``` + +- [ ] **Step 6: Create payload layout** + +```tsx +// apps/cms/src/app/(payload)/layout.tsx +/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */ +/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */ +import type { ServerFunctionClient } from "payload"; + +import config from "@payload-config"; +import { RootLayout } from "@payloadcms/next/layouts"; +import React from "react"; + +import { importMap } from "./importMap.js"; +import "./custom.scss"; + +type Args = { + children: React.ReactNode; +}; + +const serverFunction: ServerFunctionClient = async function (args) { + "use server"; + const { default: payloadModule } = await import("payload"); + return payloadModule.handleServerFunctions({ ...args, config, importMap }); +}; + +const Layout = ({ children }: Args) => ( + + {children} + +); + +export default Layout; +``` + +- [ ] **Step 7: Create empty importMap and custom.scss** + +```typescript +// apps/cms/src/app/(payload)/importMap.js +export const importMap = {}; +``` + +```scss +// apps/cms/src/app/(payload)/custom.scss +// Custom admin panel styles +``` + +- [ ] **Step 8: Run pnpm install and commit** + +Run: `pnpm install` + +```bash +git add apps/cms/ pnpm-lock.yaml +git commit -m "feat(cms): add thin Next.js shell for Payload admin panel" +``` + +--- + +### Task 9: Update docker-compose with CMS service + +**Files:** +- Modify: `docker-compose.yml` + +- [ ] **Step 1: Update docker-compose.yml** + +```yaml +services: + postgres: + image: postgres:16-alpine + restart: unless-stopped + ports: + - "5432:5432" + environment: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + POSTGRES_DB: template + volumes: + - postgres_data:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U postgres"] + interval: 5s + timeout: 5s + retries: 5 + +volumes: + postgres_data: +``` + +Note: CMS app service will be added when Dockerfiles are created in a later plan. For now, local dev uses `pnpm dev --filter @repo/cms`. + +- [ ] **Step 2: Commit (no change needed if docker-compose is already correct)** + +Only commit if docker-compose was modified. + +--- + +### Task 10: Verify build + +- [ ] **Step 1: Run pnpm install from root** + +Run: `pnpm install` +Expected: All dependencies resolve. + +- [ ] **Step 2: Run turbo build** + +Run: `pnpm build` +Expected: All workspaces build. The cms-core and cms-client packages should pass `tsc --noEmit`. The cms app should run `next build` (may need database for full build — if it fails due to no DB, that's expected and acceptable at this stage). + +- [ ] **Step 3: Commit any remaining changes** + +```bash +git add -A && git status +# Only commit if there are changes +git commit -m "chore: update lockfile after CMS integration" +```