Single self-updating Cloud Run service. Node 24, ESM. Everything runs inside one service. No second service, no scheduler, no API keys. Vertex AI auth = IAM (attached service account / ADC).
- app-engineer owns:
src/server.mjs,src/lib/*.mjs(exceptschema.mjswhich is locked),Dockerfile,.dockerignore,.gcloudignore,.env.example. - design-lead owns:
src/views/*.mjs,src/content/seed-curriculum.json,README.md. - cloud-infra owns: GCP resources (service accounts, IAM, GCS bucket),
deploy/*,autoconfig.sh,autoconfig.bat,CHANGELOG.md. Runs the actual deploy.
src/lib/schema.mjs and package.json are already written, locked, do not change shapes.
See src/lib/schema.mjs (Zod). Curriculum = { version, generatedAt, tracks[], concepts[], projects[], changelog[] }.
Concept has trackId + week (1-12) + prereqs[] + resources[]. Project maps to a Shoolini syllabus tag.
Export these pure functions (return strings, no I/O):
export function page({ title, description, path, bodyHtml }) // -> full <!doctype html> string (layout + inlined CSS)
export function homeView(curriculum) // -> bodyHtml string
export function trackView(curriculum, track)// track = one TrackSchema obj; -> bodyHtml
export function graphView(curriculum) // knowledge-graph page; -> bodyHtml
export function projectsView(curriculum) // -> bodyHtml
export function challengeView(curriculum) // The 30-Day Challenge; -> bodyHtml
export function notFoundView() // -> bodyHtml
export function statusView(state, curriculum) // shows last self-update result; -> bodyHtmlsrc/views/pitch.mjs:
export function pitchPage() // -> full self-contained 10-slide HTML deck (Guy Kawasaki format)Rules for views: all CSS inlined in page() via a <style> block. Zero external/CDN assets.
Skeuomorphic, distinctive, tactile design. WCAG 2.2 AA: semantic HTML, ARIA, keyboard nav,
focus order, alt text, no color-only meaning, prefers-reduced-motion respected. Escape all
dynamic strings (provide an esc() helper inside the views module).
GET / home · GET /track/:id · GET /graph · GET /projects · GET /challenge
GET /status (self-update status) · GET /pitch · GET /api/curriculum (JSON)
GET /health (shallow 200) · GET /health/ready (deep: GCS reachable)
404 -> notFoundView. Errors never leak stack traces.
- On a page request (NOT /health*), check in-memory
freshTodayflag. If set, skip. - Else read
meta/state.jsonfrom GCS. IflastUpdateDate === todayIST-> set flag, skip. - Else acquire atomic lock: create
meta/lock-<todayIST>with GCS preconditionifGenerationMatch: 0. 412 -> another instance owns today -> skip. Success -> we own it. - Run update synchronously within the request (AbortController, 50s cap):
- Vertex Gemini 2.5 Flash + Google Search grounding -> research latest free CS/AI learning resources & the 2026 AI-native stack (text findings).
- Vertex Gemini 2.5 Flash (JSON out) -> merge findings into current curriculum.
- Validate with
CurriculumSchema. Invalid -> keep old, resultfailed. - Changed -> write
content/curriculum.json, bumpversion, prepend changelog. Unchanged -> resultunchanged. Either way writemeta/state.jsonwith today's date.
- Hard cap: at most ONE update per IST day, enforced by the date check + atomic lock.
- All wrapped in try/catch, a failed update must NEVER break page serving.
- First run: if
content/curriculum.jsonmissing in GCS, seed it fromsrc/content/seed-curriculum.json(resultseeded).
content/curriculum.json, the live curriculummeta/state.json, StateSchemameta/lock-YYYY-MM-DD, daily lock object
PORT(default 8080) ·CONTENT_BUCKET(required) ·GCP_PROJECT(required)VERTEX_LOCATION(defaultasia-south1) ·GEMINI_MODEL(defaultgemini-2.5-flash)NODE_ENVNo secrets/API keys. Vertex uses ADC from the attached service account.
Region asia-south1. --min-instances=0 (scale to zero, $0 idle),
--max-instances=4, --cpu=1 --memory=512Mi, --concurrency=80, --timeout=120,
--allow-unauthenticated, default CPU throttling (billed only during requests),
--service-account=parallelcs-run@dmjone.iam.gserviceaccount.com,
env vars set via --set-env-vars. Build via gcloud run deploy --source . (no local Docker).