Evolves scenario_attempts (Phase 2B: one row per validation click) into a real attempt lifecycle — one row per continuous working session, with live counters (validation attempts/passes/fails, hint reveals, duration) updated in place, not re-inserted. Backward compatible: every new column is nullable/defaulted, existing rows backfilled under the new semantics rather than dropped. hint_count (Phase 2B, always NULL — 'not measurable') finally gets a real DEFAULT 0 and increments, since this is the phase that builds real hint tracking. curriculum-rules.js computes rule-driven stage lock/complete/percent/next-scenario state from scenarios/rootnode-path.json's new per-stage completion_rule (all_required/threshold/ prerequisites) against a student's progress. The capstone stage is no longer a hardcoded locked placeholder — it carries an unlock_rule and a pointer at the assessment definition that actually defines its scenario draw (see the assessment-engine commit).
RootNode Academy — Platform API (Phase 3A)
Lightweight service: no k3s, no --privileged, no Lab terminal runtime. See
docs/PHASE2B_ADMIN_AND_PLATFORM_DATA.md for the original split/migration design and
docs/PHASE3A_IDENTITY_AND_INSTRUCTOR_ACCESS.md for persistent sessions, per-instructor
accounts, RBAC, and audit logging — 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), and platform/test/e2e-phase3a.js (persistent sessions
including a real restart, instructor auth, RBAC, cohort scoping, CSRF, audit log). All three
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 (survive a restart) but there's no distributed revocation channel to the
Gateway's short-lived whoami cache, exam mode's live bookkeeping stays Lab-local, and OIDC/SSO
is a documented adapter interface, not a working integration yet.