codingstairs
NotesEDULifeContact
⌕Search⌘K
koen

Navigation

  • Intro
  • Blog
  • Life

Get in touch

Send without signing in. Add your email if you'd like a reply.

  • Leave a message anonymously →
  • ✉ warragon112@gmail.com
  • KakaoTalk Open Chat ↗

© 2026 codingstairs

  • Notes
  • EDU
  • Search
  • Life
  • Contact
  • Legal
  • RSS
  • GitHub
EDU›Central admin platform — many domains behind one hub›Step 8

Step 8

E2E manifest + deploy

0 views

E2E manifest + deploy

As page count grows, the common mistake becomes "added a page, forgot the spec". Derive route manifests from the filesystem and remove the cost.

1. Page manifest

import fg from 'fast-glob';
import { writeFileSync } from 'node:fs';

const files = await fg(['src/app/**/page.tsx', '!src/app/api/**']);
const routes = files.map((f) =>
  '/' + f
    .replace(/^src\/app\//, '')
    .replace(/\/page\.tsx$/, '')
    .replace(/\/\([^)]+\)/g, '')
    .replace(/\/\[(\.{3})?([^\]]+)\]/g, '/:$2')
);
writeFileSync('e2e/pages/manifest.json',
  JSON.stringify({ routes: routes.sort() }, null, 2));

2. API manifest

const files = await fg('src/app/api/**/route.ts');
const endpoints = [];
for (const file of files) {
  const src = readFileSync(file, 'utf-8');
  const methods = ['GET','POST','PUT','PATCH','DELETE']
    .filter((m) => new RegExp(`export\\s+async\\s+function\\s+${m}\\b`).test(src));
  if (!methods.length) continue;
  endpoints.push({ path: '/' + file.replace(/^src\/app\//, '').replace(/\/route\.ts$/, ''), methods });
}

3. Single spec iterating

for (const route of manifest.routes) {
  test(`page ${route} smoke`, async ({ page }) => {
    const url = route
      .replace(/:id/g, '00000000-0000-0000-0000-000000000000')
      .replace(/:slug/g, 'test-slug');
    const resp = await page.goto(url);
    expect(resp?.status() ?? 0).toBeLessThan(500);
  });
}

4. Protect writes

export function skipWriteOnProd() {
  if (process.env.E2E_ENV === 'PROD') test.skip();
}

Apply plus grepInvert: /@write/ in the PROD Playwright config.

5. CI drift detection

- name: regenerate manifests
  run: |
    pnpm tsx e2e/pages/generate-manifest.ts
    pnpm tsx e2e/equivalence/generate-manifest.ts
    git diff --exit-code e2e/**/manifest.json

6. Deploy — Docker standalone

export default { output: 'standalone' } satisfies NextConfig;
FROM node:20-alpine AS deps
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN corepack enable && pnpm install --frozen-lockfile

FROM node:20-alpine AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN corepack enable && pnpm build

FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
RUN apk add --no-cache postgresql-client
COPY --from=builder /app/public ./public
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
EXPOSE 3000
CMD ["node", "server.js"]

postgresql-client is needed for pg_dump.

7. docker-compose (dev)

services:
  admin:
    build: .
    env_file: .env.dev
    ports: ["127.0.0.1:3000:3000"]
    volumes:
      - ./backups:/app/backups
    depends_on:
      - postgres-blog
      - postgres-market

  postgres-blog:
    image: postgres:15-alpine
    environment:
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: postgres
      POSTGRES_DB: blog
    ports: ["127.0.0.1:5435:5432"]
    volumes: [blog-data:/var/lib/postgresql/data]

127.0.0.1: loopback binding keeps PG off the public internet.

8. Caddy reverse proxy (prod)

admin.example.com {
  reverse_proxy admin:3000
}

VPN / IP allow-list is better, but HTTPS via Caddy is the minimum.

9. Checklist

  • CI regenerates manifests and diffs clean
  • Every write test tagged @write and uses skipWriteOnProd
  • DISABLE_CRON=1 for local dev
  • PG containers bind 127.0.0.1:
  • Quarterly restore rehearsal

Closing

Once a single hub serves several domains, "why is this page slow?" and "where did the audit miss?" are answered in one place. The five foundations (separate pools, shared table, audit log, backups, manifest-driven E2E) keep compounding.

Next

  • architecture-patterns (successor course)

← Step 7

Backup automation — pg_dump + cron

🎉 You finished Central admin platform — many domains behind one hub

What's next? Pick another course below.

Next: Local LLM · pgvector · building a RAG chatbot →Browse all courses