Abdulmuizz HamzatandRotimi Best 73f39da54a feat(workflows): add Railway preview environment and teardown workflows (#766)
* feat(workflows): add Railway preview environment and teardown workflows

* fix(workflows): enhance security by binding branch names to env variables in Railway workflows

* fix(workflows): ensure preview URLs are live by blocking on service healthchecks during deployment

* temp: auto-run railway-preview.yml on push for demo recording

workflow_dispatch requires the file on the default branch before "Run
workflow" is available; add a scoped push trigger + input fallbacks so
this branch can produce a real run to record. Revert before merge.

* fix(workflows): pass --workspace to railway link for non-interactive CI

Railway CLI now requires --workspace when running `railway link`
non-interactively; without it the preview workflow fails at the link
step. Defaults to the "cio projects" workspace the staging project
lives in, overridable via a RAILWAY_WORKSPACE repo variable.

* fix(workflows): read RAILWAY_PROJECT_ID from secrets, not vars

It's configured as a repository secret (has been for weeks), but the
workflow read it via the vars context, which is a separate namespace
and always resolved empty -> railway link failed with Project "" not
found. Read it via secrets to match how it's actually stored.

* fix(workflows): pass --workspace to environment new/delete, stop masking failures

railway environment new needed --workspace in non-interactive mode too
(same as railway link), but its failure was unconditionally swallowed
by `|| echo`, so the workflow proceeded to "Link" as if the preview
environment existed when it never got created. Add --workspace to
both environment new and environment delete, and only swallow the
"already exists" case -- any other failure now fails the run where it
actually happened instead of surfacing as a confusing error two steps
later.

* fix(workflows): stop passing --project/--workspace to railway environment new/delete

Verified against the actual CLI (railway environment new --help / delete
--help): those subcommands take NO --project/--workspace/--environment
flags at all, and were silently rejecting them, which is why environment
creation kept failing and getting masked by the fallback echo. Only
`railway link` accepts --project/--workspace/--environment, so link to
the base project first, then create/delete environments by name, then
re-link to switch into the new one for deploy.

* fix(workflows): force path-style addressing for mc against Railway's MinIO domain

mc alias set defaults to --path auto, which can pick virtual-host-style
bucket addressing (bucket.host/key). Railway's edge only routes the
exact configured domain, not wildcard bucket subdomains, so mb then
fails with a misleading "specified bucket does not exist" even though
the alias/auth check (a GET) succeeds. Force --path on, matching the
app's own OBJECT_STORAGE_FORCE_PATH_STYLE=true for this same endpoint.

* chore: retrigger preview workflow after fixing cio-minio start command

staging's cio-minio now has the correct Custom Start Command, and the
stale pr-chore-staging-setup preview env (duplicated before that fix)
has been deleted. Empty commit to trigger a fresh deploy run.

* fix(workflows): generate cio-minio's public domain in the preview env

Confirmed staging's cio-minio has its own domain, but environment
duplication doesn't carry it to the new preview environment, so
RAILWAY_PUBLIC_DOMAIN came back empty there and produced an invalid
"https://" alias URL. Generate one in CI, same as the existing
cio-dashboard domain step, pinned to port 9000 since the minio/minio
image exposes both the S3 API (9000) and console (9001).

* chore(workflows): revert temp push-trigger testing scaffold, trim comments

Remove the push trigger and || fallback expressions added solely to
get a run before this file exists on main (workflow_dispatch requires
that for the "Run workflow" UI). Back to workflow_dispatch-only, with
comments trimmed from a debugging narrative down to the durable facts
future readers need. All real fixes found during testing (link
--workspace, secrets vs vars, environment new/delete flag handling,
mc --path on, cio-minio domain generation) are unchanged.

* fix(workflows): fix teardown link/secrets bugs, harden domain reads

railway-preview-teardown.yml had the same class of bugs already found
and fixed in railway-preview.yml during testing:
- RAILWAY_PROJECT_ID read from vars.* while the deploy workflow (and
  the actual GitHub config) uses secrets.* — always resolved empty.
- environment delete was called without linking first, and passed an
  unsupported --project flag; the resulting CLI error was swallowed
  by `|| echo`, so teardown silently no-opped on every run and every
  preview environment leaked indefinitely.

Also hardens railway-preview.yml against a repeat of the cio-minio
domain bug: two other "generate domain, then read it back" spots
(cio-minio bucket seeding, cio-dashboard's final URL report) had the
same unguarded `|| true` swallow with no check that the resulting
host is non-empty. Both now fail loudly instead of silently
proceeding — the dashboard one matters most since it's the final
step and would otherwise report success with a broken link.

Resolves the now-verified SPIKE comment on environment duplication,
and trims a few comments that restated code instead of explaining
non-obvious behavior.

* fix(workflows): address CodeRabbit findings on preview lifecycle

- Unify concurrency groups between deploy and teardown (was
  railway-preview-<branch> vs railway-preview-teardown-<branch>, so
  they never coordinated). A PR-close teardown and a manual deploy
  for the same branch could otherwise race, letting a deploy recreate
  an environment right after teardown deleted it.
- Pin the Railway CLI to 5.30.1 (the version verified against all
  session) in both workflows, instead of an unpinned `npm install -g`
  that could silently pick up different undocumented CLI behavior
  later, same class of bug this session hit repeatedly.
- Append a short hash of the original branch string to the derived
  environment name in both workflows. The slug alone is lossy (/, _,
  and . all collapse to the same separator), so e.g. "foo/bar" and
  "foo_bar" previously collided onto the same environment name.

* fix(workflows): scope teardown job to same-repo PRs only

The header comment already documented that this only applies to
same-repo branches (secrets are withheld from fork PR runs), but
nothing enforced it -- the job ran on every closed PR including
forks, where the empty RAILWAY_API_TOKEN/RAILWAY_PROJECT_ID made
railway link fail with an auth error and show a red run for a PR
that never had a preview environment to clean up.

* docs: migrate RAILWAY_PREVIEW.md to PREVIEW_ENV.md

Replace docker/docs/RAILWAY_PREVIEW.md with PREVIEW_ENV.md at the repo
root, restructured per PR review feedback around the two things a
reader actually needs: how to deploy to staging, and how to deploy a
PR to a preview environment. Drops the one-time Railway project setup
section (not needed on this page, per review).

---------

Co-authored-by: Rotimi Best <best@classroomio.com>
2026-08-01 21:07:31 +01:00
2026-04-27 11:40:38 +01:00
2025-09-10 17:24:35 +01:00
2024-01-28 23:55:55 +01:00
2026-04-27 11:40:38 +01:00
2025-11-01 15:49:29 +01:00
2026-04-27 11:40:38 +01:00
2023-12-08 21:34:18 +03:00

ClassroomIO is a no-code tool that allows you build and scale your online bootcamp with ease.

ClassroomIO.com

The Open Source Learning Management System for Companies
Website | Join Discord community

✨ About ClassroomIO.com

ClassroomIO Courses page

ClassroomIO is an open source LMS for companies. Run compliance/employee training, customer education, and partner certification cohorts — all from one platform. Self-host it on your own infrastructure or use the cloud version.

Who It's For

Use Case What You Get
Compliance Training Deadline tracking, renewals & retake intervals, grace periods, waivers, certificates with custom IDs
Customer Education Branded academy portal, cohorts (cohorts with goals), AI lesson tutor, multilingual content
Partner Training Partner workspaces, branded certificates, custom domains, embeddable widgets, multilingual support

Key Features

Course & Content

  • Course management — unlimited courses, lessons, exercises, grading, and certificates
  • Cohorts — group courses into cohorts with goals, team management, and progress tracking
  • AI Course Builder — generate outlines, lesson content, and assignments (Gemini, GPT-4o, Claude)
  • AI Lesson Tutor — in-lesson AI assistant that helps learners as they study

Compliance & Certification

  • Compliance tracking — status, deadlines, grace periods, renewals, and waivers
  • Certificates — issue branded certificates with custom IDs

Learner & Org Experience

  • Multi-org & multi-teacher — invite teachers, assign courses, manage multiple organizations
  • Student dashboard — learners access all courses, assignments, and progress in one place
  • Multilingual — deliver content in 10+ languages

Integrations & Developer Tools

  • REST API + Webhooks — enroll users, trigger automations, receive events (certificate.issued, enrollment.completed, and more)
  • MCP server — @classroomio/mcp on npm for AI-native integrations
  • Embeddable widget — embed your course catalog on any website

Platform

  • Fully open source — self-host the entire stack on your own servers

For what's coming next, see the public roadmap.

Built With

Get a Demo

You can book a quick 15 min demo to see if ClassroomIO is a good fit for you

Book a Call with ClassroomIO.com

Getting Started

To get a local copy up and running, please follow these simple steps.

Prerequisites

Here is what you need to be able to run ClassroomIO.com

  • Node.js (Version: >=20.19.3) — required
  • pnpm (v10) — required; the package scripts call pnpm directly, so npm/yarn are not substitutes
  • Docker — required; runs Postgres + Redis (no local install of those needed)

Project Structure

This repo is a monorepo that consists of these primary apps:

  1. website: The landing page of ClassroomIO hosted here
  2. api: The api service that handles PDF, video processing, Emailing and Notifications.
  3. dashboard: The web application that runs the learning management system hosted here.
  4. docs: Official documentation of ClassroomIO hosted here

The repository also contains shared packages under packages/ (for example packages/db, packages/utils, and packages/ui).

Development

Local Setup

Before you start: make sure pnpm and Docker are installed (see Prerequisites). New to the project? Read DEV_SETUP_NOTES.md first — it's a full step-by-step walkthrough with a troubleshooting reference for the errors you're likely to hit.

  1. Fork the repo, then clone it:

    git clone https://github.com/classroomio/classroomio.git
    
  2. Go to project folder:

    cd classroomio
    
  3. Set up Node (using nvm):

    nvm use
    

    You first might need to install the specific version and then use it:

    nvm install && nvm use
    

    You can install nvm from here.

  4. Install dependencies:

    pnpm i
    
  5. Set up your .env files:

    Each app reads its own .env. In apps/api, apps/jobs and apps/dashboard, duplicate .env.example to .env and fill the values below (host ports). The secret values are placeholders — generate each one with openssl rand -hex 32 rather than copying the example text. The two PRIVATE_SERVER_KEY values must match — they authenticate the dashboard's server-to-server calls to the API, so generate it once and paste the same value into both files.

    • apps/api/.env:

      DATABASE_URL="postgresql://postgres:postgres@localhost:5432/classroomio"
      REDIS_URL="redis://localhost:6379"
      PUBLIC_SERVER_URL="http://localhost:3002"
      TRUSTED_ORIGINS="http://localhost:5173"        # the dashboard dev origin
      BETTER_AUTH_SECRET="<generate-with-openssl-rand-hex-32>"
      PRIVATE_SERVER_KEY="<generate-with-openssl-rand-hex-32>"   # generate once; use the SAME value in the dashboard
      DASHBOARD_ORIGIN="http://localhost:5173"        # optional; used for invite/email links back to the dashboard
      
    • apps/jobs/.env: The jobs service handles background tasks (emails, media processing, analytics rollups, etc.).

      Recommended setup (simplest):

      cd apps/jobs
      ln -s ../../apps/api/.env .env  
      
    • apps/dashboard/.env:

      PUBLIC_SERVER_URL="http://localhost:3002"
      PRIVATE_SERVER_URL="http://localhost:3002"      # used by the SSR auth proxy — required, or login returns "API upstream not configured"
      PRIVATE_SERVER_KEY="<generate-with-openssl-rand-hex-32>"   # must match the API (paste the value generated above)
      PUBLIC_IS_SELFHOSTED=true
      
    • Running the background workers? Also create apps/jobs/.env (e.g. cp apps/api/.env apps/jobs/.env); it needs REDIS_URL. The packages/db/.env used by the DB scripts is set up in step 6.

    • Optional for self-hosted Enterprise-only features (SSO, token-auth, no-tracking): set LICENSE_KEY in apps/api/.env.

    • Optional upload size limits: set matching UPLOAD_MAX_*_MB values in both apps/api/.env and apps/dashboard/.env (see apps/api/.env.example). Defaults are 5 MB documents, 2 MB assignment files, 800 MB videos.

  6. Start local infrastructure for API (Postgres + Redis) and seed the DB:

    docker compose -f docker-compose.yaml up -d postgres redis
    cp packages/db/.env.example packages/db/.env   # one-time (Windows: Copy-Item)
    pnpm --filter @cio/db db:setup:seed
    
    • Connect with DATABASE_URL=postgresql://postgres:postgres@localhost:5432/classroomio
    • Connect with REDIS_URL=redis://localhost:6379
    • db:setup:seed creates the schema and seeds demo data once Postgres is up. The db scripts run with their own working directory, so they read DATABASE_URL from packages/db/.env (not apps/api/.env).
  7. (Optional) Start MinIO locally for object storage (media/documents):

    docker compose -f docker-compose.yaml --profile minio up -d minio minio-init
    
    • Console: http://localhost:9001 (user/pass default minioadmin / minioadmin)
    • S3 endpoint: http://localhost:9000
    • Buckets created by minio-init: videos, documents, media
    • Add to apps/api/.env when using MinIO locally:
      • OBJECT_STORAGE_ENDPOINT=http://localhost:9000
      • OBJECT_STORAGE_PUBLIC_ENDPOINT=http://localhost:9000
      • OBJECT_STORAGE_ACCESS_KEY_ID=minioadmin
      • OBJECT_STORAGE_SECRET_ACCESS_KEY=minioadmin
      • OBJECT_STORAGE_FORCE_PATH_STYLE=true
      • OBJECT_STORAGE_MEDIA_PUBLIC_BASE_URL=http://localhost:9000/media
  8. Run the local app services in separate terminals:

    pnpm api:dev
    
    pnpm dashboard:dev
    
    • If you see Failed to resolve entry for package "@cio/...", the shared workspace packages haven't been built yet (their dist/ is missing). Build them once with pnpm build, then re-run the dev commands.
  9. Default local URLs:

  10. Optional: run other apps:

  • website: pnpm website:dev
  • docs: pnpm dev --filter=@cio/docs
  1. Login into dashboard:

    To learn more about how to login with a dummy account, go here.

Enabling the AI Course Assistant

The in-course AI chat (course authoring, plan generation, lesson edits) is disabled by default. Enable it by setting at least one provider API key in apps/api/.env (or the root .env for the Docker stack):

# Pick one or more — the dashboard model picker exposes Gemini 2.5 Flash and GPT-4o.
OPENAI_API_KEY=sk-...        # enables GPT-4o
GOOGLE_API_KEY=AIza...       # enables Gemini 2.5 Flash (default model in the picker)
ANTHROPIC_API_KEY=sk-ant-... # supported in code; not currently in the picker UI

Notes:

  • The GET /agent/status endpoint flips to enabled: true as soon as any of those keys is set, which is what the dashboard checks before showing the AI button on a course.
  • Each chat request sends the user-selected model (persisted in localStorage as classroomio-ai-chat-model). The API resolves the provider for that model (packages/utils/src/agent-models) and returns 503 AI_NOT_CONFIGURED if that provider's key is missing.
  • Optional Tinybird observability: set TINYBIRD_TOKEN (and optionally TINYBIRD_BASE_URL) in apps/api/.env. Events are silently skipped when the token is absent.
  • More detail on architecture, tools, and routes lives in prd/ai-course-assistant [DONE]/README.md.

Docker Compose (Full Stack)

cp .env.example .env   # copy env template, edit for your domain
./run-docker-full-stack.sh

The script reads root .env via docker compose --env-file .env and auto-generates a secure PRIVATE_SERVER_KEY (and BETTER_AUTH_SECRET) when missing.

See .env.example for the full list of environment variables with required/optional grouping, and docker/docs/SELF_HOST.md for the complete Docker self-hosting guide. To raise upload caps (documents, videos, assignment files), set the UPLOAD_MAX_*_MB vars in .env — see the docs.

Publishing

When cutting releases for hosted assets or the npm MCP package, run:

  • Storybook — pnpm --filter @cio/storybook storybook:publish
  • Course widget embed — pnpm --filter @cio/embeds embeds:publish
  • Question type picker — pnpm --filter @cio/embeds embeds:publish (same script builds and uploads both embeds)
  • MCP (@classroomio/mcp) — bump the version in packages/mcp/package.json, then pnpm mcp:build and pnpm --filter @classroomio/mcp publish
S
Description
No description provided
Readme AGPL-3.0
141 MiB
Languages
TypeScript 44.8%
Svelte 41.2%
HTML 8.8%
MDX 2.5%
CSS 1.5%
Other 1.2%