Monorepo CI/CD: API + Next.js (Cloud Run)

November 14, 2025 · View on GitHub

This repository hosts a monorepo with a Node.js API (api/) and a Next.js app (frontend-next/). A unified CI/CD pipeline runs tests first (quality gate) and then deploys both services sequentially via a single Google Cloud Build job.

Live Deployments

Note: Deployment URLs are environment-specific and managed via Google Cloud Run. Retrieve URLs via:

gcloud run services describe maximus-training-frontend --region africa-south1 --format='value(status.url)'
gcloud run services describe maximus-training-api --region africa-south1 --format='value(status.url)'

Or check the Google Cloud Console for live service details.

Authentication

The identity flow follows the spec’s BFF-first model: browsers never contact the API directly for mutations. Instead, the Next.js App Router handlers verify Firebase credentials, mint session cookies, and forward normalized headers to the Express API so logs, rate limits, and audit records always align with a single requestId/traceId pair.

Sign-in flow

  1. The client posts credentials to /api/auth/login.
  2. The BFF verifies the Firebase ID token (or accepted dev credentials in local smoke tests: admin/password, alice/correct-password).
  3. On success, the BFF mints an HTTP-only session cookie (15-minute rolling expiry, SameSite=Strict, Secure in production) and issues a CSRF double-submit token (csrf cookie + X-CSRF-Token header).
  4. Subsequent route handlers forward the verified identity to the API via X-User-Id and X-User-Role headers while also propagating tracing headers (X-Request-Id, traceparent, optional tracestate).
  5. /api/auth/logout clears both cookies and rotates the tracing context so the next request starts fresh.

Roles and permissions

ActionAnonymousOwnerAdmin
View posts (GET /posts, /posts/{id})
Create post✅ (attributed to userId)
Edit/delete own post
Edit/delete others’ posts❌ (403 Forbidden)

Roles are enforced server-side. Client payloads that attempt to spoof authorId, userId, or role are stripped before business logic runs.

Session, CSRF, and tracing propagation

  • Session cookies are rotated on login, logout, and role change; back-end handlers never trust stale cookies.
  • Double-submit CSRF enforcement rejects writes without matching cookie/header pairs or when tokens expire (>2h or ±5m clock skew).
  • ensureRequestContext middleware guarantees that every request has a stable requestId and W3C trace headers so API logs, audit records, and CI benches correlate across tiers.

SameSite tradeoffs

SameSite=Strict protects against cross-site forgery and remains the default. Pivot to Lax only when integrations demand third-party redirects or embedded frames. Document the justification in release notes, enable explicit origin allowlists, and tighten CSRF checks before changing this flag.

Troubleshooting

  • Clock skew — Firebase tokens tolerate ±5 minutes. Ensure local devices use NTP/chrony if logins fail with 401 after adjusting clocks.
  • Revoked token — Admin revocations force re-authentication. Clear cookies and sign in again; check audit logs (type:"audit") for denial reasons.
  • Stale session — If the BFF refuses a request with 401 immediately after deploy, delete cookies and retry; deployments rotate signing secrets when SESSION_SECRET changes.
  • 429 loops — Writes are rate-limited to 10/minute/user. Use exponential backoff (200ms → 400ms → 800ms) and respect Retry-After headers before retrying. The frontend-next/src/lib/http/backoff.ts helper exposes with429Backoff to wrap fetch calls, emit retry telemetry, and avoid hammering the API.
  • CORS 401 vs 403401 indicates an unauthenticated request (missing/expired token or cookie). 403 means the identity is valid but lacks access (e.g., owner editing another user's post, CSRF mismatch, or read-only maintenance mode).

Idempotency & retry guidance

  • Idempotency keys explained — An idempotency key is a unique, client-generated token (for example, a UUID stored alongside the pending request) that the server can use to de-duplicate retries. When a POST endpoint supports the pattern it will document the required header (e.g., Idempotency-Key) and retention window. The current /api/posts POST route does not yet accept an idempotency key, so treat that mutation as non-idempotent until the API changelog announces support.

  • Idempotent verbs first — Prefer PUT/PATCH for updates so retries can safely re-send the request without creating duplicates. Only retry POST when the endpoint explicitly documents an idempotency key.

  • Use with429Backoff for safe retries — Wrap idempotent requests in with429Backoff to honour Retry-After, cap attempts, and emit callbacks with remaining quota information:

    import { with429Backoff } from '@/lib/http/backoff';
    
    const response = await with429Backoff(
      () => fetch('/api/posts', { method: 'PUT', body: JSON.stringify(payload) }),
      {
        maxAttempts: 3,
        onRetry: ({ attempt, delayMs, remaining }) => {
          console.info(
            `Retry #${attempt} in ${delayMs}ms (remaining quota: ${remaining ?? 'unknown'})`,
          );
        },
      },
    );
    
    if (response.status === 429) {
      // Surface an actionable message: "We are throttling writes for your account. Please retry later."
    }
    
  • Surface guidance to users — When retries are exhausted, display the Retry-After value and clarify that the action was not completed to prevent accidental duplicates.

  • Handle duplicates explicitly — If a retry results in a 409 Conflict, show the user which record already exists (e.g., "A post with this slug already exists") and avoid mutating local optimistic caches. Server handlers should log the conflicting idempotency key or unique field to help diagnose repeated submissions.

Local Development

Prerequisites

  • Node.js 20.x (LTS) — enforced via engines field in package.json and CI verification scripts
  • Docker (optional, for container workflows)

Run API locally

cd api
pnpm install
pnpm dev
# API listens on http://localhost:8080

Run Next.js locally

Create .env.local in frontend-next/:

NEXT_PUBLIC_API_URL=http://localhost:8080

Then start the dev server:

cd frontend-next
pnpm install
pnpm dev
# App on http://localhost:3000

Note on server-side configuration:

API_BASE_URL is required on the server (e.g., Cloud Run service env var) for the initial server-rendered request to /posts to fetch posts from the backend. Without it, SSR may not include post items on first paint. In local dev, SSR will fall back to NEXT_PUBLIC_API_URL if API_BASE_URL is not set, but production should always set API_BASE_URL.

CI/CD Overview

Stage 1: Tests (Quality Gate)

  • Runs monorepo tests via pnpm -r test at the root.
  • Individual packages also have their own test:ci scripts (e.g., api/, frontend-next/).

Stage 2: Deploy (Single Cloud Build)

  • A single Cloud Build job builds and deploys:
    • frontend-next/ to Cloud Run (port 3000)
    • api/ to Cloud Run (port 8080)
  • Both services are deployed with --min-instances=1 to avoid cold starts.

Cloud Build file: cloudbuild.yaml

Evidence and Reports

  • The Quality Gate job summary includes a section titled "frontend-next Coverage (with thresholds)" with the current coverage table.
  • The Review Packet artifacts include the coverage-frontend-next HTML coverage report and the Playwright HTML report. See the Review Packet guide for details and local rebuild steps: docs/ReviewPacket/README.md
  • For guidance on finding and interpreting CI/CD evidence artifacts, see docs/release-evidence.md

Week 10 Artifacts (Frontend Foundations – SSR & Hardening)

Evidence artifacts for the Week 10 milestone (Frontend Foundations with SSR, observability, design system v1, and quality gates):

Evidence Reports:

Test Artifacts:

All artifacts are generated during the CI/Quality Gate phase and linked in release PRs per DEVELOPMENT_RULES.md.

Evidence Collection Flow (T028–T106)

The release process follows a 5-step evidence collection flow:

  1. Node Verification (T028/DEV-702) — CI verifies Node 20.x via npm run verify:node
  2. Quality Gate (T030/DEV-704) — Aggregates coverage (API ≥80% lines, ≥70% branches; frontend-next ≥70% lines), a11y (0 critical), security (0 critical/high), and Spectral OpenAPI validation (0 errors)
  3. Packet Building (T080/DEV-705) — Consolidates release artifacts: contracts, a11y, benchmarks, security audit via npm run gate:packet
  4. Checklist Validation (T106/DEV-707) — Maps checked PR items to tasks and packet artifacts via npm run gate:checklists
  5. Release Creation — Tags version and publishes GitHub release with artifacts via npm run release:create -- --version v8.0.0

Security: SameSite=Strict Cookies (T082/DEV-706)

Session cookies are protected against CSRF attacks via SameSite=Strict. The regression test suite (frontend-next/tests/cookie.samesite.spec.ts) validates:

  • Same-site XHR with credentials — Session cookie is sent; authenticated users have edit/delete permissions
  • Cross-site requests — Browser withholds session cookie (SameSite enforcement); unauthenticated users see read-only permissions
  • Defense in depth — Combines SameSite=Strict, HttpOnly, Secure flags, and origin validation

Run the test locally:

cd frontend-next && pnpm test cookie.samesite.spec.ts

Local gate commands (parity with CI):

# Generate security and governance artifacts
pnpm run security:audit
pnpm run governance:report

# Aggregate Quality Gate and emit Coverage Totals block
pnpm run gate:aggregate

# Verify Node.js version (T028/DEV-702)
pnpm run verify:node

# Build evidence packet (T080/DEV-705)
pnpm run gate:packet

# Validate checklist-to-evidence mappings (T106/DEV-707)
pnpm run gate:checklists -- --pr-body '<markdown-checklist>'

CI Overview

This repository uses Google Cloud Build for deploys (cloudbuild.yaml). Tests and quality gate run prior to deploys in the upstream CI environment.

Useful Commands

# Run all tests (root)
pnpm -r test

# API tests
cd api && pnpm run test:ci

# Frontend tests
cd frontend-next && pnpm run test:ci

Idempotency and retries

  • frontend-next/src/lib/http/backoff.ts exports retryWithIdempotencyBackoff, a shared helper used by Route Handlers and future background jobs to enforce retry safety.
  • POST requests are never retried. The helper throws with guidance to use PUT/PATCH plus an idempotency key when retries are required.
  • PUT/PATCH retries must reuse the same idempotency key on every attempt; the helper surfaces the key to each retry via the callback context.
  • See frontend-next/tests/idempotency.e2e.spec.ts for contract coverage ensuring retries honour these semantics.

Repository Structure

  • api/: Express API (TypeScript), deployed to Cloud Run
  • frontend-next/: Next.js app, deployed to Cloud Run
  • cloudbuild.yaml: Monolithic build + deploy for both services
  • .github/workflows/main.yml: Test + deploy workflow

Notes

  • Frontend image builds with NEXT_PUBLIC_API_URL passed as a build-arg from Cloud Build.
  • Consider setting a Cloud Run service account for least privilege deployments.
  • /health dependency probes can optionally call the Firebase Admin SDK when HEALTHCHECK_FIREBASE_ADMIN_PING=true and honour HEALTHCHECK_TIMEOUT_MS (or per-dependency overrides) to prevent slow checks from delaying responses.

Production configuration (required)

  • Ensure the runtime has NODE_ENV=production so development fallbacks in route handlers are disabled.
  • Set API_BASE_URL on the frontend service (Cloud Run) to the API HTTPS URL.
  • If protecting the API with IAP or requiring ID tokens, set IAP_AUDIENCE (or ID_TOKEN_AUDIENCE) so the app can mint ID tokens for upstream calls.

Training

Quality Gate workflow

Monorepo containing multiple small apps and exercises (quote/, expense/, stopwatch/, todo/, frontend/, and more).

Run & Try (frontend-next)

Prerequisites:

  • Node.js 18+
  • A running Posts API (default: http://localhost:8080 from api workspace)

Local steps:

  1. Start the API service:

    cd api
    pnpm install
    pnpm dev
    # API listens on http://localhost:8080
    
  2. Create the frontend env file:

    # file: frontend-next/.env.local
    NEXT_PUBLIC_API_URL=http://localhost:8080
    
  3. Install dependencies and start the frontend:

    cd frontend-next
    pnpm install
    pnpm dev
    # App on http://localhost:3000
    
  4. Verify it works:

Environment variables:

VariableScopeLocal exampleProduction examplePurpose
API_BASE_URLServer-only (Cloud Run)http://localhost:8080https://maximus-training-api-wyb2jsgqyq-bq.a.run.appUpstream API base URL used by server SSR and Route Handlers. Set on Cloud Run service env vars.
NEXT_PUBLIC_API_URLClient and SSR fallbackhttp://localhost:8080(not required)Base URL for API in local dev; SSR falls back to this if API_BASE_URL is unset. Prefer server proxy (/api) in production.
NEXT_PUBLIC_APP_URLCI/E2E usagehttp://localhost:3000https://maximus-training-frontend-673209018655.africa-south1.run.appPublic URL of the app for Playwright and link checks in CI.
DATABASE_URLServer-onlypostgresql://localhost:5432/devpostgresql://...neon.tech/prodPostgreSQL connection string (Neon or local). Required for server start and DB tests.
SESSION_SECRETServer-onlydev-secret-32chars(32+ char random)Strong random string for session encryption. Required in production.
GCP_PROJECT_IDServer-onlyproj-rms-devproj-rms-prodGoogle Cloud project ID for GCP services.
GCP_REGIONServer-onlyafrica-south1africa-south1Google Cloud region for deployments.
VERTEX_LOCATIONServer-onlyus-central1us-central1Vertex AI location for model access.
VERTEX_MODELServer-onlygemini-2.5-flashgemini-2.5-flashVertex AI model name.
ASSISTANT_ENABLEDServer-onlyfalsetrueEnable assistant API routes at /api/assistant/*.
ASSISTANT_MACROS_ONLYServer-onlyfalsefalseRestrict assistant to macros-only mode.
VITE_ASSISTANT_ENABLEDClient build-timefalsetrueEnable assistant UI in client build.
ASSISTANT_CORS_ORIGINSServer-onlyhttp://localhost:3000https://maximus-training-frontend-673209018655.africa-south1.run.appCSV of allowed CORS origins for assistant endpoints.
ASSISTANT_FORWARDING_SECRETServer-only(random)(random)HMAC secret for assistant ingress authentication.

Running with Docker

Build the container:

docker build -t nextjs-app:latest ./frontend-next

Run the container:

docker run -p 3000:3000 nextjs-app:latest

Note: For optimal Google Cloud Run deployment, use output: "standalone" in frontend-next/next.config.ts.

Live Demo (frontend-next):

Quickstart

Prerequisites:

  • Node.js 18+
  • Git

From the repo root:

# install root tools and run the monorepo test runner
pnpm install
pnpm -r test

AI-Powered Code Reviews

This repository supports AI-assisted code reviews through multiple methods:

Use Claude Code interactively for comprehensive PR reviews:

How to use: Simply ask in your Claude Code session:

"Review my Phase 2 PR"
"Check this code for security issues"
"Review the Button component for accessibility"

Advantages:

  • Free - No API key required
  • Interactive - Ask follow-up questions
  • Context-aware - Understands your codebase
  • Same quality - Uses Claude Sonnet 4.5

🔮 Gemini Reviews (Automated - Already Configured)

Trigger Gemini code reviews on any PR:

@gemini-cli /review

Features:

  • Integrated via MCP tools
  • Direct inline suggestions
  • Automatic on PR open (if enabled)
  • Uses your existing GCP setup

📚 More Options

For additional free AI review tools and detailed comparison, see: docs/AI-REVIEW-ALTERNATIVES.md

Available alternatives:

  • Manual Claude Code reviews (⭐ Recommended)
  • Gemini reviews (already set up)
  • CodeRabbit (free for open source)
  • Qodo (free tier: 20 PRs/month)
  • GitHub Copilot (if you have access)

Note: The automated Claude review workflow is disabled by default (requires ANTHROPIC_API_KEY). Use manual Claude Code reviews instead for the same quality without API costs!

Spec Kit (Specify) integration

Slash commands are available via your AI assistant (Cursor/Copilot) using the prompts in .github/prompts.

Non-interactive CLI setup (Windows/PowerShell):

# one-time: ensure uv is available
python -m pip install --user uv

# run Specify CLI (examples)
python -m uv tool run --from git+https://github.com/github/spec-kit.git specify init --here --ai cursor --script ps --ignore-agent-tools --no-git

# create or update a spec in Cursor/Copilot chat
/specify Build a todo filtering feature by status and text search

# generate an implementation plan
/plan Use React state + URL params; add tests and docs

# generate tasks from artifacts
/tasks Generate dependency-ordered tasks for the above plan

Relevant files and scripts:

  • .github/prompts/specify.prompt.md, plan.prompt.md, tasks.prompt.md
  • .specify/templates/*.md (templates used by prompts)
  • .specify/scripts/powershell/*.ps1 (helper scripts invoked by prompts)

Governance Waivers and Quality Gate

If a temporary exception is required (e.g., security audit has high findings or audit unavailable), follow the policy in SECURITY_EXCEPTIONS.md and mirror approved waivers in the governance report consumed by the gate.

  • Guide: see CONTRIBUTING.md section "Governance basics" (example JSON provided)
  • Schema: scripts/quality-gate/schemas/governance.schema.json
  • Generate a baseline governance report:
pnpm run governance:report

This writes governance/report.json. Add approvedExceptions entries as needed with mentor approval and expiry dates.

Status checks & docs-only exemption

This repo enforces protected checks per DEVELOPMENT_RULES.md:

  • Required checks: lint, ypecheck, unit, coverage, 11y, contract, uild, deploy-preview
  • PR body must include evidence fields (Linear key, Gate run, Artifacts, Demo URL(s), Screenshots if UI, Linked Plan).

Docs-only PRs: If your change only updates documentation and touches no runtime code:

  • Add label docs-only or put DEV-EXEMPT under Linear Key(s) in the PR body.
  • Gate Artifacts may be N/A for docs-only PRs.
  • The PR template checker (scripts/check-pr-template.js) recognizes these exemptions.

See DEVELOPMENT_RULES.md for full details.