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
- The client posts credentials to
/api/auth/login. - The BFF verifies the Firebase ID token (or accepted dev credentials in local smoke tests:
admin/password,alice/correct-password). - On success, the BFF mints an HTTP-only
sessioncookie (15-minute rolling expiry,SameSite=Strict,Securein production) and issues a CSRF double-submit token (csrfcookie +X-CSRF-Tokenheader). - Subsequent route handlers forward the verified identity to the API via
X-User-IdandX-User-Roleheaders while also propagating tracing headers (X-Request-Id,traceparent, optionaltracestate). /api/auth/logoutclears both cookies and rotates the tracing context so the next request starts fresh.
Roles and permissions
| Action | Anonymous | Owner | Admin |
|---|---|---|---|
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).
ensureRequestContextmiddleware guarantees that every request has a stablerequestIdand 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
401after 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
401immediately after deploy, delete cookies and retry; deployments rotate signing secrets whenSESSION_SECRETchanges. - 429 loops — Writes are rate-limited to 10/minute/user. Use exponential backoff (200ms → 400ms → 800ms) and respect
Retry-Afterheaders before retrying. Thefrontend-next/src/lib/http/backoff.tshelper exposeswith429Backoffto wrap fetch calls, emit retry telemetry, and avoid hammering the API. - CORS 401 vs 403 —
401indicates an unauthenticated request (missing/expired token or cookie).403means 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
POSTendpoint supports the pattern it will document the required header (e.g.,Idempotency-Key) and retention window. The current/api/postsPOSTroute 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/PATCHfor updates so retries can safely re-send the request without creating duplicates. Only retryPOSTwhen the endpoint explicitly documents an idempotency key. -
Use
with429Backofffor safe retries — Wrap idempotent requests inwith429Backoffto honourRetry-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-Aftervalue 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_URLis required on the server (e.g., Cloud Run service env var) for the initial server-rendered request to/poststo fetch posts from the backend. Without it, SSR may not include post items on first paint. In local dev, SSR will fall back toNEXT_PUBLIC_API_URLifAPI_BASE_URLis not set, but production should always setAPI_BASE_URL.
CI/CD Overview
Stage 1: Tests (Quality Gate)
- Runs monorepo tests via
pnpm -r testat the root. - Individual packages also have their own
test:ciscripts (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=1to 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-nextHTML 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:
- Security Review: docs/week-10/SECURITY-REVIEW.md — Comprehensive audit: OWASP Top 10 compliance, threat modeling, vulnerability assessment, sign-off
- Performance Audit: docs/week-10/performance.md — Measured baselines for
/status(p95: 95ms),/postsSSR (p95: 1.2s), optimization analysis, load capacity - Evidence Summary: docs/week-10/evidence-summary.md — Quality gate results, acceptance criteria, implementation summary, artifact links
Test Artifacts:
- Coverage Report: docs/week-10/coverage/index.html — Jest coverage for
frontend-next/src/**/*.{ts,tsx}with threshold targets - Accessibility Audit: docs/week-10/a11y/index.html — axe-core scan results for SSR pages and design system components
- Playwright Report: docs/week-10/playwright/index.html — E2E test evidence: SSR proof (JS disabled), accessibility, error states
- Status Probe Metrics: docs/week-10/status-probe.json — p95 latency tracking for
/statusendpoint (rolling 10-minute samples)
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:
- Node Verification (T028/DEV-702) — CI verifies Node 20.x via
npm run verify:node - 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)
- Packet Building (T080/DEV-705) — Consolidates release artifacts: contracts, a11y, benchmarks, security audit via
npm run gate:packet - Checklist Validation (T106/DEV-707) — Maps checked PR items to tasks and packet artifacts via
npm run gate:checklists - 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.tsexportsretryWithIdempotencyBackoff, 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.tsfor contract coverage ensuring retries honour these semantics.
Repository Structure
api/: Express API (TypeScript), deployed to Cloud Runfrontend-next/: Next.js app, deployed to Cloud Runcloudbuild.yaml: Monolithic build + deploy for both services.github/workflows/main.yml: Test + deploy workflow
Notes
- Frontend image builds with
NEXT_PUBLIC_API_URLpassed as a build-arg from Cloud Build. - Consider setting a Cloud Run service account for least privilege deployments.
/healthdependency probes can optionally call the Firebase Admin SDK whenHEALTHCHECK_FIREBASE_ADMIN_PING=trueand honourHEALTHCHECK_TIMEOUT_MS(or per-dependency overrides) to prevent slow checks from delaying responses.
Production configuration (required)
- Ensure the runtime has
NODE_ENV=productionso development fallbacks in route handlers are disabled. - Set
API_BASE_URLon the frontend service (Cloud Run) to the API HTTPS URL. - If protecting the API with IAP or requiring ID tokens, set
IAP_AUDIENCE(orID_TOKEN_AUDIENCE) so the app can mint ID tokens for upstream calls.
Training
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:8080fromapiworkspace)
Local steps:
-
Start the API service:
cd api pnpm install pnpm dev # API listens on http://localhost:8080 -
Create the frontend env file:
# file: frontend-next/.env.local NEXT_PUBLIC_API_URL=http://localhost:8080 -
Install dependencies and start the frontend:
cd frontend-next pnpm install pnpm dev # App on http://localhost:3000 -
Verify it works:
- Visit http://localhost:3000/posts
- You should see a list of posts. If not, ensure the API is running on http://localhost:8080 and that
NEXT_PUBLIC_API_URLis set accordingly in.env.local.
Environment variables:
| Variable | Scope | Local example | Production example | Purpose |
|---|---|---|---|---|
API_BASE_URL | Server-only (Cloud Run) | http://localhost:8080 | https://maximus-training-api-wyb2jsgqyq-bq.a.run.app | Upstream API base URL used by server SSR and Route Handlers. Set on Cloud Run service env vars. |
NEXT_PUBLIC_API_URL | Client and SSR fallback | http://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_URL | CI/E2E usage | http://localhost:3000 | https://maximus-training-frontend-673209018655.africa-south1.run.app | Public URL of the app for Playwright and link checks in CI. |
DATABASE_URL | Server-only | postgresql://localhost:5432/dev | postgresql://...neon.tech/prod | PostgreSQL connection string (Neon or local). Required for server start and DB tests. |
SESSION_SECRET | Server-only | dev-secret-32chars | (32+ char random) | Strong random string for session encryption. Required in production. |
GCP_PROJECT_ID | Server-only | proj-rms-dev | proj-rms-prod | Google Cloud project ID for GCP services. |
GCP_REGION | Server-only | africa-south1 | africa-south1 | Google Cloud region for deployments. |
VERTEX_LOCATION | Server-only | us-central1 | us-central1 | Vertex AI location for model access. |
VERTEX_MODEL | Server-only | gemini-2.5-flash | gemini-2.5-flash | Vertex AI model name. |
ASSISTANT_ENABLED | Server-only | false | true | Enable assistant API routes at /api/assistant/*. |
ASSISTANT_MACROS_ONLY | Server-only | false | false | Restrict assistant to macros-only mode. |
VITE_ASSISTANT_ENABLED | Client build-time | false | true | Enable assistant UI in client build. |
ASSISTANT_CORS_ORIGINS | Server-only | http://localhost:3000 | https://maximus-training-frontend-673209018655.africa-south1.run.app | CSV of allowed CORS origins for assistant endpoints. |
ASSISTANT_FORWARDING_SECRET | Server-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):
- Demo URL: https://maximus-training-frontend-673209018655.africa-south1.run.app
- Once deployed from the default branch, this link will be referenced in the Review Packet manifest and PR summaries.
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:
🤖 Manual Claude Code Reviews (Recommended - Free!)
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.mdsection "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.