# 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 ```bash 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 you@example.com "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 ```bash node bootstrap-owner.js [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: ```bash docker run --rm -v :/data \ -e SQLITE_PATH=/data/progress.db -e PLATFORM_DB_URL=postgres://postgres:postgres@:5433/rootnode_platform \ 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.