Spec 0009: HQ Next.js Application with Auth0
Agent Roles
Section titled “Agent Roles”This specification is the single source of truth for what to build, how to verify it, and who does what. Each agent reads its role below and follows the instructions exactly. Agents do not communicate directly — they communicate through the provenance document.
Builder Agent
Section titled “Builder Agent”Purpose: Read this specification and produce working software with full provenance.
Reads:
- This specification
- ADR-021 (
docs/adr/adr-021-auth0-authentication-hq.md) sites/kevinryan-io/— reference Next.js patternsites/hq-kevinryan-io/— existing static site being replaced.github/workflows/deploy.yml— reference workflow (kevinryan-io)k8s/kevinryan-io/— reference K8s manifestshttps://brand.kevinryan.io— brand guidelines (fonts, colours, design language)
Produces:
- Working Next.js application at
hq.kevinryan.io, gated by Auth0 - A provenance record at
.sdd/provenance/spec-0009-hq-nextjs-auth0.provenance.md
Instructions:
- Save this spec to
.sdd/specification/spec-0009-hq-nextjs-auth0.mdin the repo. This is the canonical reference. Do not modify it after saving. - Read the full specification, all prerequisites, and all files listed under “Current state” before writing any code.
- Build the software as specified. Where the specification is silent on an implementation detail, make a reasonable decision and record it in the provenance.
- Write provenance as you build, not after. Every assumption, interpretation, and deviation is recorded as it happens.
- For every assumption not explicitly stated in this spec, record it under “Assumptions” in the provenance.
- For every ambiguity, record it under “Ambiguities” with your interpretation and the decision you made.
- Do not write tests. Testing is not your role.
- When the build is complete, add a “Build Status” entry to the provenance summarising what was built.
- Commit the spec, implementation, and provenance together.
Testing Agent
Section titled “Testing Agent”Purpose: Read this specification and the builder’s provenance, then generate prose scenarios and executable tests that verify the software against the spec.
Reads:
- This specification
- The provenance document at
.sdd/provenance/spec-0009-hq-nextjs-auth0.provenance.md
Produces:
- Prose scenarios at
.sdd/scenarios/spec-0009-hq-nextjs-auth0.scenarios.md - Executable test code in the
tests/directory - Updates to the provenance document recording findings
- Save this spec to
.sdd/specification/spec-0009-hq-nextjs-auth0.mdin the repo. - Implement all changes described below.
- After completing all work, create a provenance record at
.sdd/provenance/spec-0009-hq-nextjs-auth0.provenance.md.
Prerequisites
Section titled “Prerequisites”- Spec-0008 deployed:
hq.kevinryan.iois live and resolving. - Read ADR-021 (
docs/adr/adr-021-auth0-authentication-hq.md) before starting. This documents the Auth0 decision and records the Auth0 callback URLs that must be registered manually. - Manual prerequisite (human, not agent): The following URLs must be registered in the Auth0 tenant before deployment:
- Allowed Callback URL:
https://hq.kevinryan.io/api/auth/callback - Allowed Logout URL:
https://hq.kevinryan.io - Allowed Web Origins:
https://hq.kevinryan.io
- Allowed Callback URL:
Context
Section titled “Context”hq.kevinryan.io currently serves a static nginx placeholder (spec-0008). This spec replaces it entirely with a Next.js application running in server mode, gated by Auth0/GitHub OAuth.
This is a full replacement of sites/hq-kevinryan-io/. The existing Dockerfile, nginx.conf, and public directory are deleted and replaced with a Next.js application. The K8s deployment manifest is updated to reflect the new port and increased resource requirements. The GitHub Actions workflow is replaced with one that builds and serves a Node.js process rather than a static nginx container.
The critical architectural difference from all other sites in this monorepo: HQ runs in Next.js server mode (next start), not as a static export served by nginx. This is required because Auth0’s Next.js SDK needs API routes (/api/auth/*) which cannot exist in a static export. Server mode also enables the Anthropic API routes required in spec-0010.
The post-login page is a placeholder for the chat interface (spec-0010). It shows: the HQ wordmark, the logged-in user’s GitHub username and avatar, the injected commit SHA, and a logout button. Nothing else.
The visual aesthetic is console/operational — dark background, monospaced accents, lime highlight — consistent with the mockup at brand.kevinryan.io and the existing placeholder page.
Current state (read these files before making changes)
Section titled “Current state (read these files before making changes)”| File / Directory | What it does |
|---|---|
sites/hq-kevinryan-io/ | Existing static site — delete all contents and replace |
sites/kevinryan-io/Dockerfile | Reference Next.js Dockerfile pattern (stage 1 build, stage 2 serve) — adapt for server mode |
sites/kevinryan-io/next.config.ts | Reference Next.js config — HQ removes output: 'export' |
sites/kevinryan-io/package.json | Reference package.json — adapt, add Auth0 SDK |
.github/workflows/deploy.yml | Reference GitHub Actions workflow — copy and adapt |
k8s/hq-kevinryan-io/deployment.yaml | Update image, port, and resource limits |
k8s/kevinryan-io/deployment.yaml | Reference deployment manifest |
docs/adr/adr-021-auth0-authentication-hq.md | Auth0 decision and callback URL reference |
Key facts
Section titled “Key facts”- Site directory:
sites/hq-kevinryan-io/ - Runtime: Next.js server mode (
next start) — NOT static export - Container port:
3000(Next.js default — different from nginx8080used by other sites) - Node.js version:
22.22.0-alpine3.23(match kevinryan-io) - Next.js version:
16.1.4(match kevinryan-io) - Auth0 SDK:
@auth0/nextjs-auth0 - Hostname:
hq.kevinryan.io - Image name (ACR):
kevinryanacr.azurecr.io/hq-kevinryan-io - Image name (GHCR):
ghcr.io/devopskev/hq-kevinryan-io - SHA env var:
NEXT_PUBLIC_COMMIT_SHA(passed as Docker build-arg, same pattern as kevinryan-io) - Fonts: Bebas Neue (display/wordmark), Archivo (body), JetBrains Mono (monospaced accents) — Google Fonts
- Accent colour:
#A8E10C - Background:
#0A0A0A - Text:
#F5F3EF - Auth0 secrets (runtime env vars, injected via K8s secret):
AUTH0_SECRET,AUTH0_BASE_URL,AUTH0_ISSUER_BASE_URL,AUTH0_CLIENT_ID,AUTH0_CLIENT_SECRET - Workflow file:
.github/workflows/deploy-hq.yml(replace existing)
1. Delete existing static site contents
Section titled “1. Delete existing static site contents”Delete the following files from sites/hq-kevinryan-io/:
public/index.htmlnginx.confDockerfile
Do not delete the directory itself — it will be repopulated with the Next.js application.
2. Next.js application scaffold
Section titled “2. Next.js application scaffold”Create a Next.js App Router application under sites/hq-kevinryan-io/ following the structure of sites/kevinryan-io/ with these differences:
- No
output: 'export'innext.config.ts— server mode only - No
trailingSlash— not needed for server mode - Auth0 SDK installed and configured
- Single protected page at
/ - Auth0 API routes at
/api/auth/[auth0]/
package.json
Section titled “package.json”{ "name": "hq-kevinryan-io", "version": "0.1.0", "private": true, "scripts": { "dev": "next dev", "build": "next build", "start": "next start -p 3000", "lint": "eslint" }, "dependencies": { "@auth0/nextjs-auth0": "^4", "next": "16.1.4", "react": "19.2.3", "react-dom": "19.2.3" }, "devDependencies": { "@types/node": "^20", "@types/react": "^19", "@types/react-dom": "^19", "eslint": "^9", "eslint-config-next": "16.1.4", "typescript": "^5" }}next.config.ts
Section titled “next.config.ts”import type { NextConfig } from "next";
const nextConfig: NextConfig = { reactStrictMode: true, env: { NEXT_PUBLIC_COMMIT_SHA: process.env.NEXT_PUBLIC_COMMIT_SHA || 'dev', },};
export default nextConfig;3. Auth0 configuration
Section titled “3. Auth0 configuration”app/api/auth/[auth0]/route.ts
Section titled “app/api/auth/[auth0]/route.ts”import { handleAuth } from '@auth0/nextjs-auth0';export const GET = handleAuth();middleware.ts (at site root, alongside app/)
Section titled “middleware.ts (at site root, alongside app/)”Protect all routes except the Auth0 callback:
import { withMiddlewareAuthRequired } from '@auth0/nextjs-auth0/edge';
export default withMiddlewareAuthRequired();
export const config = { matcher: ['/((?!api/auth).*)'],};Environment variables
Section titled “Environment variables”The following environment variables must be present at runtime. They are not build-time variables — they are delivered to the pod via the platform’s established secret management pattern: Azure Key Vault (kv-kevinryan-io) → External Secrets Operator → Kubernetes Secret named hq-auth0-secrets.
| Variable | Key Vault secret name | Description |
|---|---|---|
AUTH0_SECRET | hq-auth0-secret | Long random string — session encryption key |
AUTH0_BASE_URL | hq-auth0-base-url | https://hq.kevinryan.io |
AUTH0_ISSUER_BASE_URL | hq-auth0-issuer-base-url | Auth0 tenant URL e.g. https://your-tenant.auth0.com |
AUTH0_CLIENT_ID | hq-auth0-client-id | Auth0 application client ID |
AUTH0_CLIENT_SECRET | hq-auth0-client-secret | Auth0 application client secret |
The secret values are written to Key Vault manually (see Manual steps). The ExternalSecret manifest (section 7) pulls them into a native K8s secret automatically.
4. Home page
Section titled “4. Home page”Create app/page.tsx — the post-login landing page. This is a placeholder for the chat interface (spec-0010).
The page must display:
- HQ wordmark —
HQin Bebas Neue, large, lime accent on the period or as highlight - GitHub username — from the Auth0 session (
user.nicknameoruser.name) - GitHub avatar — from the Auth0 session (
user.picture) displayed as a small circle - Commit SHA —
build: {NEXT_PUBLIC_COMMIT_SHA}in JetBrains Mono, small, lime accent, bottom of page - Logout button — minimal, monospaced, top right corner. Calls
/api/auth/logout. - Chat placeholder — a centred empty state message in the main content area:
chat interface coming soonin JetBrains Mono, muted colour. This marks where spec-0010 will place the chat input.
Visual style: dark background #0A0A0A, warm white text #F5F3EF, lime accent #A8E10C, console/operational aesthetic consistent with the existing placeholder page. No navigation, no sidebar, no cards. Minimal.
app/layout.tsx
Section titled “app/layout.tsx”Standard Next.js root layout. Load Google Fonts: Bebas Neue, Archivo, JetBrains Mono. Dark background. No additional structure.
5. Dockerfile
Section titled “5. Dockerfile”Replace sites/hq-kevinryan-io/Dockerfile. This is a server-mode Node.js container, not nginx:
FROM node:22.22.0-alpine3.23 AS build
ARG COMMIT_SHA=devENV NEXT_PUBLIC_COMMIT_SHA=${COMMIT_SHA}
RUN corepack enable && corepack prepare pnpm@latest --activate
WORKDIR /app
COPY pnpm-lock.yaml pnpm-workspace.yaml ./COPY sites/hq-kevinryan-io/package.json ./sites/hq-kevinryan-io/
WORKDIR /app/sites/hq-kevinryan-ioRUN pnpm install --frozen-lockfile
WORKDIR /appCOPY sites/hq-kevinryan-io/ ./sites/hq-kevinryan-io/
WORKDIR /app/sites/hq-kevinryan-ioRUN pnpm build
FROM node:22.22.0-alpine3.23
LABEL org.opencontainers.image.source="https://github.com/DevOpsKev/kevin-ryan-platform"
RUN corepack enable && corepack prepare pnpm@latest --activate
WORKDIR /app
COPY --from=build /app/sites/hq-kevinryan-io/.next ./sites/hq-kevinryan-io/.nextCOPY --from=build /app/sites/hq-kevinryan-io/public ./sites/hq-kevinryan-io/publicCOPY --from=build /app/sites/hq-kevinryan-io/package.json ./sites/hq-kevinryan-io/package.jsonCOPY --from=build /app/sites/hq-kevinryan-io/node_modules ./sites/hq-kevinryan-io/node_modulesCOPY pnpm-workspace.yaml ./
WORKDIR /app/sites/hq-kevinryan-io
EXPOSE 3000
CMD ["pnpm", "start"]Design notes:
- No nginx in this Dockerfile — Next.js serves its own HTTP responses
NEXT_PUBLIC_COMMIT_SHAis injected at build time as before- Auth0 environment variables are NOT in the Dockerfile — they are injected at runtime via K8s secret
- The
/healthzendpoint does not exist in this container by default — see K8s manifests section
6. Health check endpoint
Section titled “6. Health check endpoint”Next.js does not serve /healthz natively. Create app/api/healthz/route.ts:
export async function GET() { return new Response('ok', { status: 200 });}This must NOT be behind auth middleware. Update middleware.ts matcher to exclude it:
export const config = { matcher: ['/((?!api/auth|api/healthz).*)'],};7. Kubernetes manifests
Section titled “7. Kubernetes manifests”Update k8s/hq-kevinryan-io/deployment.yaml and k8s/hq-kevinryan-io/service.yaml. Add k8s/hq-kevinryan-io/externalsecret.yaml. The namespace, ingress, and Flux sync files do not change.
Update the deployment to:
- Change container port from
8080to3000 - Increase resource limits (Node.js requires more memory than nginx)
- Add
envFromto inject the Auth0 secrets from thehq-auth0-secretsK8s secret
apiVersion: apps/v1kind: Deploymentmetadata: name: hq-kevinryan-io namespace: hq-kevinryan-iospec: replicas: 1 selector: matchLabels: app: hq-kevinryan-io template: metadata: labels: app: hq-kevinryan-io spec: containers: - name: hq-kevinryan-io image: kevinryanacr.azurecr.io/hq-kevinryan-io:latest ports: - containerPort: 3000 envFrom: - secretRef: name: hq-auth0-secrets livenessProbe: httpGet: path: /api/healthz port: 3000 initialDelaySeconds: 10 periodSeconds: 10 readinessProbe: httpGet: path: /api/healthz port: 3000 initialDelaySeconds: 5 periodSeconds: 5 resources: requests: cpu: 50m memory: 128Mi limits: cpu: 200m memory: 256MiUpdate k8s/hq-kevinryan-io/service.yaml to target port 3000:
apiVersion: v1kind: Servicemetadata: name: hq-kevinryan-io namespace: hq-kevinryan-iospec: selector: app: hq-kevinryan-io ports: - port: 80 targetPort: 3000externalsecret.yaml
Section titled “externalsecret.yaml”Create k8s/hq-kevinryan-io/externalsecret.yaml. This pulls the Auth0 secrets from Key Vault into a native K8s secret following the same pattern as k8s/umami/externalsecret.yaml and k8s/observability/externalsecret.yaml:
apiVersion: external-secrets.io/v1kind: ExternalSecretmetadata: name: hq-auth0-secrets namespace: hq-kevinryan-iospec: refreshInterval: 1h secretStoreRef: kind: ClusterSecretStore name: azure-keyvault target: name: hq-auth0-secrets creationPolicy: Owner data: - secretKey: AUTH0_SECRET remoteRef: key: hq-auth0-secret - secretKey: AUTH0_BASE_URL remoteRef: key: hq-auth0-base-url - secretKey: AUTH0_ISSUER_BASE_URL remoteRef: key: hq-auth0-issuer-base-url - secretKey: AUTH0_CLIENT_ID remoteRef: key: hq-auth0-client-id - secretKey: AUTH0_CLIENT_SECRET remoteRef: key: hq-auth0-client-secretDesign notes:
refreshInterval: 1h— ESO syncs Key Vault values into the K8s secret every hour. If secrets are rotated in Key Vault the pod picks them up on the next sync without a restart.creationPolicy: Owner— ESO owns the lifecycle of the K8s secret. If the ExternalSecret is deleted, the K8s secret is deleted too.- No template block needed — the Key Vault secret values map directly to environment variable names without transformation.
8. Terraform — Auth0 secrets
Section titled “8. Terraform — Auth0 secrets”Add the following to infra/variables.tf. Follow the exact pattern of cloudflare_api_token — type = string, sensitive = true:
variable "auth0_secret" { description = "Auth0 session encryption secret for HQ" type = string sensitive = true}
variable "auth0_issuer_base_url" { description = "Auth0 tenant URL for HQ (e.g. https://your-tenant.auth0.com)" type = string sensitive = true}
variable "auth0_client_id" { description = "Auth0 client ID for HQ" type = string sensitive = true}
variable "auth0_client_secret" { description = "Auth0 client secret for HQ" type = string sensitive = true}Add the following to infra/main.tf, alongside the existing azurerm_key_vault_secret resources:
resource "azurerm_key_vault_secret" "hq_auth0_secret" { name = "hq-auth0-secret" value = var.auth0_secret key_vault_id = module.keyvault.key_vault_id}
resource "azurerm_key_vault_secret" "hq_auth0_base_url" { name = "hq-auth0-base-url" value = "https://hq.kevinryan.io" key_vault_id = module.keyvault.key_vault_id}
resource "azurerm_key_vault_secret" "hq_auth0_issuer_base_url" { name = "hq-auth0-issuer-base-url" value = var.auth0_issuer_base_url key_vault_id = module.keyvault.key_vault_id}
resource "azurerm_key_vault_secret" "hq_auth0_client_id" { name = "hq-auth0-client-id" value = var.auth0_client_id key_vault_id = module.keyvault.key_vault_id}
resource "azurerm_key_vault_secret" "hq_auth0_client_secret" { name = "hq-auth0-client-secret" value = var.auth0_client_secret key_vault_id = module.keyvault.key_vault_id}Update .github/workflows/terraform.yml to pass the new variables in both the plan and apply jobs, alongside the existing TF_VAR_cloudflare_api_token entry:
TF_VAR_auth0_secret: ${{ secrets.TF_VAR_AUTH0_SECRET }}TF_VAR_auth0_issuer_base_url: ${{ secrets.TF_VAR_AUTH0_ISSUER_BASE_URL }}TF_VAR_auth0_client_id: ${{ secrets.TF_VAR_AUTH0_CLIENT_ID }}TF_VAR_auth0_client_secret: ${{ secrets.TF_VAR_AUTH0_CLIENT_SECRET }}Design notes:
hq-auth0-base-urlis hardcoded tohttps://hq.kevinryan.io— it is not a variable because it is determined by the infrastructure, not an external system.AUTH0_SECRETis a session encryption key generated once. Store it in GitHub secrets and do not rotate it unless there is a security incident — rotation invalidates all active sessions.- Follow the
cloudflare_api_tokenpattern exactly:sensitive = trueon all variables,TF_VAR_prefix on GitHub secret names.
9. GitHub Actions workflow
Section titled “9. GitHub Actions workflow”Replace .github/workflows/deploy-hq.yml. Follow deploy.yml (kevinryan-io) exactly, substituting kevinryan-io → hq-kevinryan-io throughout. Path trigger: sites/hq-kevinryan-io/**.
The workflow does not change — build, push to ACR/GHCR, update manifest, commit. The Dockerfile handles the Node.js build internally.
10. pnpm workspace
Section titled “10. pnpm workspace”Add sites/hq-kevinryan-io to pnpm-workspace.yaml if it is not already present. Check the file before modifying.
Constraints and Assumptions
Section titled “Constraints and Assumptions”- Constraint: HQ runs in Next.js server mode.
output: 'export'must not be present innext.config.ts. - Constraint: Auth0 environment variables are runtime secrets, not build-time args. They must never appear in the Dockerfile or GitHub Actions workflow.
- Constraint: The Auth0 callback URLs must be registered in the Auth0 tenant before login will work. This is a manual step documented in Prerequisites.
- Constraint: Auth0 secret values reach Key Vault via Terraform, following the same pattern as
cloudflare_api_token. The values are stored as GitHub Actions secrets (TF_VAR_*) and written to Key Vault byinfra/main.tfduringterraform apply. The agent adds the Terraform resources; the human adds the GitHub secrets. - Assumption: The
ClusterSecretStorenamedazure-keyvaultis already running and healthy in the cluster (established by spec-0003/ADR-018). - Assumption:
pnpm-lock.yamlat the repo root covers all workspace packages. The agent runspnpm installfrom the workspace root context in the Dockerfile. - Assumption: Auth0 tenant is already provisioned with a GitHub social connection configured as the identity provider.
Out of Scope
Section titled “Out of Scope”- Chat interface — that is spec-0010
- Anthropic API integration — that is spec-0010
- GitHub MCP integration — that is spec-0010
- Demo mode toggle — that is spec-0010
- Adding GitHub Actions secrets to the repository — manual step performed by the human before running Terraform
- Registering Auth0 callback URLs — manual step, must be done before deployment
Manual steps (not performed by the agent)
Section titled “Manual steps (not performed by the agent)”Before merging the PR:
- Register callback URLs in Auth0 dashboard:
- Allowed Callback URL:
https://hq.kevinryan.io/api/auth/callback - Allowed Logout URL:
https://hq.kevinryan.io - Allowed Web Origins:
https://hq.kevinryan.io
- Allowed Callback URL:
Before merging the PR:
- Add the following GitHub Actions secrets to the
DevOpsKev/kevin-ryan-platformrepository (Settings → Secrets and variables → Actions):
| Secret name | Value |
|---|---|
TF_VAR_AUTH0_SECRET | Generate with openssl rand -hex 32 |
TF_VAR_AUTH0_ISSUER_BASE_URL | https://<your-tenant>.auth0.com |
TF_VAR_AUTH0_CLIENT_ID | Auth0 application client ID |
TF_VAR_AUTH0_CLIENT_SECRET | Auth0 application client secret |
Note: AUTH0_BASE_URL is always https://hq.kevinryan.io — hardcoded in Terraform, not a GitHub secret.
- Run
terraform applyto write the secrets to Key Vault — this happens automatically when the PR is merged andinfra/**changes trigger the Terraform workflow.
After merging the PR:
- Verify ESO has synced the secret:
kubectl get secret hq-auth0-secrets -n hq-kevinryan-io - Confirm the pod is running:
kubectl get pods -n hq-kevinryan-io - Visit
https://hq.kevinryan.io— should redirect to GitHub OAuth login - Complete login — should land on the HQ page showing your GitHub username and avatar
- Click logout — should return to the login redirect
Provenance Record
Section titled “Provenance Record”After completing the work, create .sdd/provenance/spec-0009-hq-nextjs-auth0.provenance.md using the provenance template at .sdd/provenance/template.md.
Validation steps
Section titled “Validation steps”- This spec has been saved to
.sdd/specification/spec-0009-hq-nextjs-auth0.md sites/hq-kevinryan-io/public/index.html,nginx.confand the staticDockerfileno longer existsites/hq-kevinryan-io/package.jsonexists and contains@auth0/nextjs-auth0as a dependencysites/hq-kevinryan-io/next.config.tsexists and does NOT containoutput: 'export'sites/hq-kevinryan-io/app/api/auth/[auth0]/route.tsexistssites/hq-kevinryan-io/app/api/healthz/route.tsexistssites/hq-kevinryan-io/middleware.tsexists and excludes bothapi/authandapi/healthzfrom auth requirementsites/hq-kevinryan-io/app/page.tsxexists and referencesNEXT_PUBLIC_COMMIT_SHAsites/hq-kevinryan-io/Dockerfileexists, containsnode:22.22.0-alpine3.23, does NOT containnginx, and exposes port3000k8s/hq-kevinryan-io/deployment.yamlreferences port3000andenvFromreferencinghq-auth0-secretsk8s/hq-kevinryan-io/service.yamltargets port3000k8s/hq-kevinryan-io/externalsecret.yamlexists, referencesClusterSecretStoreazure-keyvault, and maps all five Auth0 variablesinfra/variables.tfcontainsauth0_secret,auth0_issuer_base_url,auth0_client_id,auth0_client_secretvariables all withsensitive = trueinfra/main.tfcontains fiveazurerm_key_vault_secretresources forhq-auth0-*.github/workflows/terraform.ymlpassesTF_VAR_auth0_*in both plan and apply jobsterraform fmt -check -recursive infra/passes.github/workflows/deploy-hq.ymlpath trigger referencessites/hq-kevinryan-io/**pnpm lintpassespnpm buildpasses insidesites/hq-kevinryan-io/(without Auth0 env vars — build must succeed without them, only runtime requires them)- The provenance record exists at
.sdd/provenance/spec-0009-hq-nextjs-auth0.provenance.md - All files are committed together in a single commit