Files
rootnode-academy/platform/api
ced4568 611e1ac4ba feat(enrollment): enrollment lifecycle, entitlement gating, self-paced student API
- enrollment.js: server-only status transitions (pending->active->completed, cancel),
  deriveEnrollmentAccess (pure, derived-at-read-time like classroom.js's derivePresence —
  'expired' is never stored, computed from access_ends_at), maybeCompleteEnrollment
  (idempotent, server-derived from the existing capstone achievement).
- environment-view.js: publicEnvView/launchEnvironmentForStudent extracted out of server.js's
  original /api/environment/launch route body so the new entitlement-gated self-paced launch
  reuses the identical disposable-Lab sequence instead of a second copy — behavior for the
  existing cohort/pilot route is completely unchanged.
- program-status.js: combines an enrollment with real curriculum/progress state into one view
  (access, percent-complete, current stage, deterministic next-activity/resume target) — shared
  by the student and admin surfaces so both show identical information.
- selfpaced-routes.js: GET /api/student/programs(/:id), POST .../launch — entitlement enforced
  server-side before ever touching the Provisioner, never just hidden in the UI.
- rbac.js: requireEnrollmentAccess — a plain instructor may manage an enrollment they created
  or that belongs to a student sharing one of their cohorts; a self-paced enrollment can have
  no cohort at all, so the existing cohort-only checks can't cover it.
- server.js: found and fixed a real bug — POST /api/progress/record's handler predated the
  wrap() convention, so an unexpected internal error (a malformed environment_id) crashed the
  entire process instead of just 500ing that request. Now wrapped + input-validated.
2026-08-25 12:08:12 -05:00
..

RootNode Academy — Platform API (Phase 3B)

Lightweight service: no k3s, no --privileged, no Lab terminal runtime. See docs/PHASE2B_ADMIN_AND_PLATFORM_DATA.md for the original split/migration design, docs/PHASE3A_IDENTITY_AND_INSTRUCTOR_ACCESS.md for persistent sessions, per-instructor accounts, RBAC, and audit logging, and docs/PHASE3B_LEARNING_AND_ASSESSMENT.md for attempt telemetry, progressive hints, curriculum rules, and the exam/capstone engine — this file is the operational reference.

Configuration reference

Variable Required Meaning
PLATFORM_DB_URL no (defaults to local dev Postgres) Postgres connection string
PROVISIONER_URL no Provisioner base URL
PROVISIONER_SHARED_SECRET yes Internal Provisioner auth — process refuses to start without it
PLATFORM_API_PORT no Listen port, default 4300
FRONTEND_DIST_DIR no Where the built frontend lives — set by the Dockerfile, override for local non-Docker dev
SCENARIOS_DIR / BUNDLES_DIR / CURRICULUM_PATH_FILE no Override the default scenarios/ locations, mainly for local dev outside Docker
STUDENT_SESSION_TTL_HOURS / STUDENT_SESSION_IDLE_HOURS no Default 8h / 2h
INSTRUCTOR_SESSION_TTL_HOURS / INSTRUCTOR_SESSION_IDLE_HOURS no Default 12h / 2h
COOKIE_SECURE no Set to 1 once served over real HTTPS — adds Secure to every session/CSRF cookie
TRUST_PROXY no Set to 1 once fronted by a trusted reverse proxy — honors X-Forwarded-For for req.ip (audit source_ip, login rate limiting)

There is no ADMIN_PASSWORD anymore — instructor accounts live in Postgres (instructors table), created via bootstrap-owner.js for the first one and the admin portal after that.

Local dev

cd platform
echo "PROVISIONER_SHARED_SECRET=$(openssl rand -hex 32)" > .env
docker compose -f docker-compose.dev.yml build
docker compose -f docker-compose.dev.yml up -d

cd api
PLATFORM_DB_URL=postgres://postgres:postgres@localhost:5433/rootnode_platform \
  node bootstrap-owner.js [email protected] "Your Name" "a-real-password-12-chars-plus"

Everything is reachable through the Gateway at http://localhost:4200 — students at /, instructors at /admin (sign in with the bootstrapped owner). Platform API itself isn't meant to be hit directly by a browser (same boundary as the Provisioner relative to Platform API), though nothing stops you from curling it directly at :4300 for local debugging.

Owner bootstrap

node bootstrap-owner.js <email> <display_name> [password]

Prefer BOOTSTRAP_OWNER_PASSWORD over the positional argument outside a throwaway local test — CLI args can leak into shell history and ps. Refuses to run if an active owner already exists unless the target email belongs to an existing instructor (the intended recovery path if a platform is ever locked out with zero owners: point this at a known instructor account to promote them).

SQLite migration

One-time cutover, run against the real data volume:

docker run --rm -v <identity-data-volume>:/data \
  -e SQLITE_PATH=/data/progress.db -e PLATFORM_DB_URL=postgres://postgres:postgres@<host>:5433/rootnode_platform \
  <platform-api-image> node migrate-sqlite.js

Always backs up the SQLite file first, is safe to re-run, and writes a JSON report — see docs/PHASE2B_ADMIN_AND_PLATFORM_DATA.md §2/§3 for the full guarantees and rollback procedure. This step is about academic data (students/cohorts/progress) — it has nothing to do with instructor accounts, which are Phase 3A-native and start empty until you bootstrap one.

Tests

platform/test/e2e-phase2a.js (student-flow regression), platform/test/e2e-phase2b.js (the classroom acceptance workflow), platform/test/e2e-phase3a.js (persistent sessions including a real restart, instructor auth, RBAC, cohort scoping, CSRF, audit log), and platform/test/e2e-phase3b.js (attempt telemetry, progressive hints, curriculum/Capstone unlock rules, the exam engine including a real restart, two-student assessment isolation, instructor analytics). All four need the full stack up and OWNER_EMAIL / OWNER_PASSWORD set to a bootstrapped account — see platform/README.md.

Known limitations

See docs/PHASE3A_IDENTITY_AND_INSTRUCTOR_ACCESS.md "Known limitations" (sessions are Postgres-backed and survive a restart, but there's no distributed revocation channel to the Gateway's short-lived whoami cache; OIDC/SSO is a documented adapter interface, not a working integration yet) and docs/PHASE3B_LEARNING_AND_ASSESSMENT.md §20 (no automatic abandoned-attempt detection, needs-attention thresholds are fixed constants, pool_spec authoring is raw JSON in the admin UI).