Files
STCourses/docs/superpowers/specs/2026-10-30-stcourses-design.md
T
2026-10-30 02:37:38 +01:00

22 KiB

STCourses: design

Date: 2026-10-30. Status: draft for approval.

STCourses is a self-hosted course platform that one person or one organisation runs for its own learners. It mixes three things: the reading comfort of Packt's book reader, the structured video courses of Pluralsight, and boot.dev's habit of learning by doing small exercises. Course content lives in plain files that are the single source of truth. A browser studio edits those files. AI features are optional and off by default.

Background research on existing open-source platforms is in reports/Open source course platforms.md. None of them fits: they are too heavy (Open edX), too dated (Moodle), put key features in paid tiers (LearnHouse), or keep content in a database. That is why this is a new build.

1. Who it is for, and why

  • Homelabbers. Host your own courses, plus course material you already own, such as a Packt video package or a Pluralsight slide PDF, in one place that tracks your progress.
  • Universities and companies. Run your own catalogue of self-made courses, with your own sign-in, on your own hardware.
  • Course authors. Write in Markdown in a git repo, or in a browser studio. Both edit the same files.

Each install has one owner (a person or an organisation). It is not multi-tenant.

The two reference courses are the quality bar. Everything in this design must work for them without rewriting them:

  • /home/user/functional-programming-course (Pure Snake): 12 modules, three language tracks (JS, Scala 3, F#) that share JSON fixtures, stub files the learner fills in, three hint tiers per exercise, "check your understanding" questions, and a MENTOR.md that tells an AI tutor how to behave.
  • /home/user/systemic-design-course: Rust and TypeScript tracks, scenario-named tests, a workspace template, module gating, the Observe → Predict → Spec → Build → Name it → Expansion Drop pattern, and an Astro site.

Real imported material is in /home/user/example_course_content:

  • a Packt video package: a zip with about 50 MP4s and an old HTML player whose index.html holds the section and clip titles and their order;
  • the companion code repo, with one folder per section, PDF slides, a cheat sheet and a Django sample app;
  • a 59-page Pluralsight slide PDF.

This material is licensed to the user. Nothing from it is committed to this repo.

2. Goals, non-goals, anti-goals

Goals

  1. Comfortable lessons of several types, in one consistent interface:
    • long-form text;
    • text with video;
    • slides attached to the video or section they belong to;
    • text with downloadable code files;
    • small in-browser exercises.
  2. Course content is files on disk, versioned with git. The database never holds course content.
  3. Importing purchased material produces an ordinary course folder. Supported sources: video packages, slide PDFs and code repos or zips.
  4. A browser studio that edits course files and commits them.
  5. Opt-in AI:
    • a course-aware assistant for learners;
    • a studio generator that drafts documents, exercises and knowledge checks.
  6. Easy self-hosting: one Docker image, SQLite by default, and external PostgreSQL as an option.

Non-goals for now

  • Payments, storefronts, marketplaces, prices, ratings and revenue share.
  • Multi-tenancy, meaning several separate organisations on one install.
  • Running full course projects on the server. Learners run projects on their own machine. Only small exercises run in the browser.
  • Mandatory video transcoding and DRM.
  • SCORM, xAPI, cmi5 and LTI. They may come later, for example LTI so universities can embed courses. Nothing in this design should block them.

Anti-goals (failure, even if it technically works)

  • Course content stored only in database tables, or in a proprietary format.
  • Idle memory use above 2 GB.
  • The visual style the user has rejected: glowing gradients, card grids, decorative accents, and interface elements themed to one course's subject (for example a snake-shaped progress bar).
  • An AI mentor that silently hands out solutions when the course asks it not to, or an AI that publishes generated material without an author reviewing it.
  • Unofficial redistribution of licensed imports. Imported courses are private by default.

3. Constraints and decided technology

These were decided with the user:

  • Runtime: Bun, following the conventions in the CLAUDE.md that bun init generates:
    • serving and routing: Bun.serve() with routes and WebSockets, and HTML imports for the frontend;
    • databases: bun:sqlite for SQLite and Bun.sql for Postgres;
    • tooling: bun test, bun install, bunx, Bun.file, and Bun.$ for shell commands;
    • not used: Express, Vite, dotenv, pg, better-sqlite3.
  • Language: TypeScript throughout.
  • Frontend: a React single-page app served by Bun.serve through HTML imports. Next.js was considered and rejected. Its memory use leaves less of the 2 GB budget, it duplicates Bun's own server and bundler, and most pages are interactive and behind sign-in. If public pages need to be visible to search engines later, render just those pages with React's renderToReadableStream inside Bun.serve.
  • Database:
    • SQLite by default;
    • external PostgreSQL through one connection-string setting;
    • user data only (section 7);
    • both databases are supported and tested in CI.
  • Course history: git, through the git command line.
  • Sign-in: OAuth/OIDC, SAML, social logins and username/password. The user suggested Better Auth (or Passport.js).
  • Memory: at most 2 GB at idle for the whole deployment, including the database when Postgres runs in the same compose file.
  • License: AGPL-3.0.
  • Deployment: one Docker image, plus an optional docker-compose file that adds Postgres.
  • Repository: https://gitea.lucaswinther.info/lucasw89/STCourses.git, default branch main.

4. The learner experience

Visual reference: docs/design/mockups/platform-v1.html. It is a click-through mockup that the user approved in direction. Its media files are not committed, so the video and slides appear blank when it is opened from this repo.

4.1 Visual direction (approved)

  • Theme.
    • Dark by default: ground #12141a, side surfaces #181b22, raised #232733, hairlines #262a33/#343947.
    • A light theme is designed separately, not created by inverting the dark one.
  • Accent.
    • One accent colour: amber #f1b44c, with a dim variant #8c7440.
    • It is used only for: the current item, progress, and the primary next action.
    • A course can set its own accent in its manifest. The Docker course in the mockup uses blue #6ea8fe.
  • Type.
    • IBM Plex Sans for all interface text.
    • Literata only for lesson titles and reading text: about 19px with line height 1.75, in a centred column about 660px wide.
    • IBM Plex Mono for code.
    • Fonts are bundled locally, not loaded from Google, so the platform works offline.
  • Structure. Pill-shaped controls, rows separated by thin rules, and thin 4px progress bars. No gradients, glows, card grids, or labels in all caps.
  • Course-neutral interface. A course brings only a cover image (or a generated monogram) and an optional accent colour.
  • Reading / Work toggle. Reading is the roomy serif column. Work is denser sans text in a wider column. Exercise pages always use Work.
  • Floating action bar. A pill at the bottom of the reading column holds "Previous", "Mark done" and "Next lesson".
  • Keyboard shortcuts. Only the expected ones: arrow keys in the slide viewer, Escape to close overlays, and normal Tab focus. No [/] or / shortcuts and no shortcut hints. They may return later as an optional setting.
  • Responsive and accessible. Works down to phone width, with visible focus and respect for reduced-motion settings.

4.2 Library (home)

The current mockup is "good and simple" but lacks the pull of browsing for your next course, the way Kickstarter browsing feels. The library has these parts:

  • Featured course. The owner picks it. It shows a large cover, a one-line pitch and a Start button.
  • Continue row. Lessons you have in progress.
  • Browsable rows. "New in the library", then one row per collection. Each row is a horizontal strip of covers with thin progress bars.
  • Collections. Defined by the owner, for example "Functional programming", "Game design" and "DevOps". A course can belong to several.
  • Filters. By format (text, video, slides, exercises), length, and "in progress".
  • Search across course titles, descriptions and lesson titles.
  • Cover images.
    • Use the course's own artwork when it has some.
    • Otherwise use a frame taken from its first video, or its first slide, at import.
    • Otherwise use a typographic monogram.
  • No storefront signals. No prices, ratings or "bestseller" badges.
  • Scale. It should feel right with 5 courses (a homelab) and with several hundred (a company or university).

4.3 Lesson types

  1. Text lesson.
    • Markdown with headings, code blocks, figures, callouts and folded "think about it first" sections.
    • Hint tiers render as folded sections, opened in order.
    • Multi-track courses show a track switcher (JS / Scala 3 / F#) that swaps the code samples and file names.
  2. Video lesson.
    • A video player (seeking needs HTTP range requests) with a transcript, if one exists, and personal notes.
    • The notes have an "insert timestamp" action that links back to that moment in the video.
  3. Slides belong to something.
    • A slide deck is never a standalone lesson. It is attached to the video, lesson or section it belongs to.
    • On a video lesson, the deck is a companion panel next to or under the video, and you can page through it on its own.
    • On a section, the deck appears as part of that section.
    • The viewer has a 16:9 stage, thumbnails, previous/next, arrow keys, fullscreen, and a page counter.
    • Keeping the slide in step with the video's time is a later nice-to-have.
  4. Notes on slides.
    • Notes attach to a specific slide page.
    • Coming back to the slide shows its note.
    • All notes for a lesson, whether on slides or video timestamps, can be read together as one list.
  5. Code files.
    • A file tree and tabbed code viewer of the course's companion code for that section, with copy buttons.
    • "Download files for this section" gives a zip.
    • A command block shows how to run it locally.
    • An attachments list holds PDFs and cheat sheets.
  6. In-browser exercise.
    • Instructions with a small editor (CodeMirror is acceptable).
    • Tests run in the browser in a sandbox, a Web Worker or sandboxed iframe, never in the page itself.
    • The first version supports JavaScript and TypeScript. Other languages (Python via Pyodide, and later others) come through a pluggable runner interface.
    • Results show pass/fail per test, with expected and actual values. Hint tiers and a Reset button are included.
    • The page shows the local command for running the full project.
    • Exercises in tracks without a browser runner show "run locally" instead of an editor.

4.4 Progress

  • Per-lesson state: not started, in progress or done. For video, the position reached also counts.
  • Progress rolls up to modules and to the course.
  • "Mark done" is manual for text and slides. Video counts as done when watched near the end. An exercise counts as done when all its tests pass.

5. Course format on disk

Phase 0 (section 10) pins down the exact schema. These principles are fixed:

  • A course is a folder, normally its own git repo, under a configured courses directory. It has a manifest file at its root that gives:
    • id, title, pitch, authors, cover, accent and collections;
    • visibility, tracks and source/import info;
    • an ordered list of modules and lessons, or a convention that derives the order from file names.
  • Lessons are Markdown files with a small front matter block (type, title, duration, attachments, linked deck, linked video and track variants). Lesson types are listed in section 4.3.
  • Attachments live with the course: videos, PDFs, images and code. Large binaries may live outside git, in a configured media directory, and be referenced by path and checksum. Phase 0 decides between that and Git LFS.
  • Slide decks are PDF files. The manifest or lesson front matter links each deck to its video, lesson or section. Page images and extracted text are derived caches, not source.
  • Exercises declare the starting code, tests, hint tiers and solution per track. The solution is visible only to authors.
  • Mentor rules: a course may ship AI tutor instructions, such as Pure Snake's MENTOR.md, referenced from the manifest.
  • Compatibility.
    • The two reference courses must load by adding only a manifest and front matter, without moving or rewriting their existing files.
    • Their existing tooling (test scripts, tools/*.mjs) must keep working.
  • Derived data lives in a cache directory that can be rebuilt from the files: search index, slide images and text, transcripts, video posters and AI embeddings.

6. Accounts, roles and visibility

Roles (my proposal):

  • Owner/admin: settings, users, sign-in providers, AI configuration and imports.
  • Author: studio access, either to all courses or per course.
  • Learner.

Visibility per course, set by the author:

  • public: anyone, no sign-in;
  • signed-in: any user;
  • restricted: named users or groups.

Imported courses default to restricted, visible only to the importer.

Sign-in:

  • OIDC/OAuth (including social providers), SAML and username/password.
  • Group membership from SAML/OIDC claims can drive the restricted visibility.

7. Data stored in the database (user data only)

The database holds:

  • users, sessions, sign-in links, roles and groups;
  • per-user progress, lesson state and video position;
  • notes, each attached to a lesson plus an optional video timestamp or slide page;
  • exercise attempts (latest code per track) and results;
  • AI settings: the server provider configuration, and each user's own API key stored encrypted (BYOK);
  • AI conversation history (optional, user-deletable);
  • library curation: featured course and collections. If the owner prefers, these can instead live in a settings file. That is the builder's call.

The same schema and migrations run on SQLite and Postgres. Deleting the database loses user data only, never courses.

8. Import

Import turns material the user owns into an ordinary course folder, writing a manifest and front matter. It never edits the original files; it copies them or references them in place. Sources in the first version:

  • Packt video package (zip). Read the player's index.html for sections and clip titles and their order, and map them to videos/videoN_M.mp4. Each clip becomes a video lesson.
  • Companion code repo (zip or git URL). Folders such as S08 - Containerizing Applications are matched to sections, by number and by name. PDFs inside them become decks or attachments.
  • Slide PDF (for example from Pluralsight). It becomes a deck attached to a chosen lesson or section. Its text is extracted for search and AI.
  • Folder of videos (the "point at a folder" homelab case). It becomes one lesson per file, ordered naturally by file name.

Import runs as a background job with visible progress. It produces cover candidates (a video frame, the first slide). Transcription is an optional, separate job using an external Whisper-style service. The user has faster-whisper-rs and au_whisper_web locally, which may fit. Transcription is never part of the main process's idle footprint.

Imported material is licensed to its owner. The platform makes imports restricted by default and never exposes import sources publicly unless the owner changes visibility.

9. Studio and AI

9.1 Studio

  • Browser editing of the manifest, lessons (Markdown with a live preview in the real lesson renderer), deck links, attachments and exercises.
  • Every save writes files and makes a git commit attributed to the author.
  • Work can happen on a draft branch and be published by merging.
  • External edits (VS Code, git pull) appear in the studio. Files remain the truth.
  • Conflicts between studio edits and external edits must be detected and shown, never silently overwritten.

9.2 Opt-in AI

AI is off by default and has two ways to be configured:

  • Server provider, set by the admin for everyone: Anthropic, any OpenAI-compatible endpoint (including a self-hosted Ollama), or others through a provider interface.
  • Bring your own key (BYOK): a learner or author adds their own key, used only for their own requests.

Learner assistant

  • It is course-aware. It retrieves from the course files, transcripts, slide text, the companion code and the learner's own notes and progress.
  • It gets tools: the platform's own course and notes tools, plus MCP servers configured by the admin, globally or per course.
  • It answers questions, explains and quizzes.
  • Mentor mode.
    • The course's mentor rules (for example "never write the solution") apply by default.
    • The learner may switch them off.
    • When they are off, the interface says so plainly, for example "Mentor mode off: I'll give full answers".

Studio generator

  • It drafts documents, exercises (with tests and hint tiers) and knowledge-check questions from material the author can access: videos via transcripts, text, slides and code.
  • It follows the house style of the reference courses:
    • Pure Snake's module shape: why, objectives, concepts, a puzzle before the explanation, exercises with nudge / stronger hint / near-solution, and check questions;
    • Systemic Design's Observe → Predict → Spec → Build → Name it → Expansion Drop cycle;
    • shared fixtures across tracks.
  • Output is always files on a draft branch, or marked as drafts, for an author to review. Nothing is published automatically.

10. Delivery approach (decided with the user)

  • Phase 0, the contract (one agent, sequential). Produce:
    • the course format schema and validator;
    • the database schema and migrations (SQLite and Postgres);
    • the internal module interfaces;
    • the repo layout, as one Bun workspace with packages;
    • the shared design tokens;
    • manifests for both reference courses;
    • fixture courses for tests.
  • Phase 1, four parallel agents, each in its own git worktree, building against the contract:
    1. learner app: library, reader, video and slide viewers, exercises, notes, progress, sign-in;
    2. importer;
    3. studio;
    4. AI: assistant and studio generator.
  • Phase 2, integration. Merge branch by branch. Run everything against Pure Snake, Systemic Design and the real Docker and Serverless material (kept outside the repo). Fix what breaks, and check the 2 GB idle budget.

11. Left to the builder

These are the builder's call:

  • the exact manifest file format (YAML or JSON) and file layout within the principles in section 5;
  • the client router (React Router or TanStack Router);
  • the editor component;
  • the search engine (SQLite FTS5 against Postgres full-text search, or an in-process index);
  • the PDF rendering library;
  • the Markdown pipeline;
  • how background jobs run, as long as they run in the same process or one small worker;
  • whether Better Auth fits Bun's database clients directly or needs a thin adapter. If it is a poor fit, choose an equivalent and record why.

12. Decisions settled after the builder review (designer's calls)

The decisions below were made by the designer, not by the user, to settle questions a fresh builder raised.

  • Lesson granularity.
    • A manifest lesson entry points at a file, or at a file plus a heading anchor.
    • A Pure Snake module file is one text lesson. Each of its exercises (for example "Exercise 3.1 (move)") is a separate exercise page, addressed by anchor. The hints in the module's ## Hints section are matched to exercises by their labels.
    • Progress is tracked per manifest entry, so per exercise.
  • Metadata location.
    • All course metadata lives in the manifest. Lesson front matter is optional, never required.
    • This keeps Systemic Design's site/scripts/sync-content.mjs and the Astro site working unchanged.
  • Module gating is an optional manifest setting per course: gating: sequential. It is off by default. When on, the reader locks a module until the previous one is done. Systemic Design turns it on.
  • Git model.
    • Learners see the course's published branch (default main), read from a separate worktree that the platform manages. Unpublished drafts never reach learners.
    • The studio works in one worktree per author per draft branch. Publishing merges the draft into the published branch.
    • The platform watches the published worktree and reloads when it changes, after an external commit or a pull.
    • Pushing to and pulling from a course's remote happen only when an author clicks "Sync", and only if a remote is configured. Nothing syncs automatically.
    • Merge conflicts are shown in the studio and resolved by the author. The platform never resolves them automatically.
  • Roles and access.
    • The roles in section 6 stand.
    • Only the owner can make an imported course public or signed-in visible.
    • Restricted visibility uses platform groups, which can be filled from SAML/OIDC group claims.
  • Light theme. Version 1 ships dark only. The design tokens are structured so a light theme can be added later as a second set of values.
  • Exercise completion when there is no in-browser runner. The exercise counts as done when the learner marks it done. A "verify locally" command is shown, and its passing output can be pasted back as optional evidence.

13. Decided by the user after the builder review

  • The reference courses' exercises run locally in v1.
    • Pure Snake and Systemic Design exercises do not run in the browser. Their exercise pages show the instructions, hint tiers, the local test command and "Mark done".
    • The in-browser runner (section 4.3) only targets new, small exercises authored for it.
    • TODO (future feature): a richer browser runner for project-style exercises. It needs a node:test/node:assert shim, a virtual file system for fixtures, multi-file module graphs, and learner code that carries over between modules.
  • Manifests for existing course repos live in STCourses, not in those repos (option B).
    • The platform supports an overlay manifest stored on the platform side, alongside the course, and pointing at an unmodified course repo.
    • The manifests for the reference courses are created this way. Their repos are never written to.
    • A course repo may still contain its own manifest. That is the normal case for courses created in the studio. If both exist, the overlay manifest wins.