Courses & content authoring
Courses on Mastria are markdown-first. Your content is plain text you can version, review, and take with you — never locked in a proprietary editor.
Structure
An academy contains courses; a course contains sections; a section contains lessons. Lessons are the unit learners read and complete.
SQL Fundamentals ← course
├── Getting started ← section
│ ├── Welcome ← lesson
│ └── Why DuckDB
└── Your first queries
├── Selecting data
├── Aggregations
└── Joins
Courses and lessons have stable, readable URLs
(/courses/sql-fundamentals/lessons/joins). Lesson URLs survive moving a
lesson between sections, so links you share stay valid as a course
evolves.
Writing lessons
Lessons are GitHub-flavored markdown:
- Headings, lists, bold/italic, links, images
- Fenced code blocks with language tags (
sql,python, …) - Tables and blockquotes
- Callouts, GitHub alert syntax:
> [!NOTE]on the first line of a blockquote (alsoTIP,IMPORTANT,WARNING,CAUTION)
If your team can write a README, they can write a course.
Draft vs. published
Work-in-progress is never visible to learners:
- Every lesson has a working copy and a published copy — the text and the title. Learners only ever see the published copy; you can edit and rename freely behind the scenes, and a rename alone marks the lesson Changed like any edit.
- The boundary is stated where you work: lesson text and titles wait for publish; the order of lessons, section names, and course settings apply to the published course as you change them (the outline says so on a published course, and so does Review & publish).
- Every lesson wears one of three states, everywhere in the admin: Draft (never published), Published, or Changed (published, with newer edits waiting). "Changed" is the one to watch — it means learners are seeing an older version than the one you're editing.
- Courses are also draft or published as a whole. A draft course is invisible even if some lessons are published.
- Publishing is an explicit act, never a side effect of saving.
- Deleting is safe, for lessons and courses alike: brand-new drafts disappear for real, while anything that has ever been published moves to a Deleted drawer — learners lose access immediately, but history and learner records stay intact and you can restore it at any time, exactly as it was.
Lab blocks
Lessons can embed hands-on labs: a ladder of exercises with automatic checks, instant feedback written by the author (never a bare "wrong"), progressive hints, and an "Apply the solution" checkpoint so nobody stays blocked. Learner code runs in the browser — SQL on a real DuckDB engine, Python on a real CPython runtime — and checks verify the actual result of the run, not just the code's shape. Labs can preload tables, data, and packages. During the design-partner phase, we author lab blocks with you — same-day, like all content.
Full reference — every field, all six check types, hints, and how to test a lab before publishing: Lab authoring.
The course workspace
Staff members get an Admin area on your academy domain (/admin).
Opening a course lands you in a single workspace: the course
outline stays pinned on the left — every lesson led by its state dot
(solid grey for Published, hollow for Draft, amber for Changed), plus
a health strip (“7 published”, “2 changed”, “1 error”, each count a
click-to-filter and a legend for the dots) — and the editor fills the
rest. Switching lessons is one click; your
unsaved keystrokes are flushed automatically before every switch (and
the switch waits for that save — if it can't complete, you stay on the
lesson with the reason shown in the outline), so
nothing is ever lost to navigation.
Editing is markdown with power tools, not a rich-text editor:
- A formatting toolbar (bold, headings, lists, quotes, links, inline code — plus ⌘B/⌘I) that writes markdown into your document. Deliberately no font pickers, sizes, or colors: your academy's design system renders every lesson on-brand, in light and dark mode, and your content stays exportable plain text forever.
- An Insert menu for the rarer blocks — code, tables, images, videos, callouts — each stamped in as valid markdown, and a dedicated + Add lab menu: pick the language learners will write (SQL on DuckDB, Python, or code-only) and a complete, valid lab skeleton lands with an id that's unique in the lesson. Once a lesson has a lab, + Exercise / + Hint / + Check grow the lab at your cursor without touching anything else you wrote.
- Sections you can shape: click a section name in the outline to rename it, collapse sections you're not working in (remembered per course), and move a lesson to another section from the Section picker beside its title. The outline keeps Review & publish, View published, Course settings, and History pinned at its foot however long the course grows, and folds behind an "Outline" bar on small screens.
- ⌘K jumps to any lesson by fuzzy title — or creates one from whatever you typed. "+ New lesson" in the outline is one click to a blinking cursor.
- Live validation as you type: errors (like a broken lab block)
mark the exact line and block publishing; quality nudges (a lab
missing its solution, a stray
#heading competing with the lesson title) warn without blocking. - Split preview shows the rendered draft beside the editor — the real learner pipeline, refreshed each time autosave settles (pause it while you run a lab there). Preview draft as learner opens the full page, and View published course (in the outline rail) opens what learners see right now, without your unpublished changes. In a preview, labs run but "Check my work" belongs to learners on the published lesson — Test lab in the editor is what verifies your checks.
- Nothing is lost to a replacement. If the draft changed in another tab, the editor pauses and lets you copy your text, download it, compare it with the latest, and then load the latest — your version is kept in the browser and offered back under the editor. Restoring a historical version from History keeps your current draft the same way.
- Repo-synced lessons open read-only with an Open source in GitHub button: the repo is the source of truth, and merging is how they change. "Experiment locally" lets you try edits as a scratch draft — previewable, never published, replaced by the next sync — with copy and download buttons to carry the result to the repo.
Videos come from where they already live. Paste a YouTube, Vimeo,
Loom, Wistia, or Mux (player.mux.com/<playback-id> — your Mux Data
analytics keep working) link — or a direct .mp4/.webm file or
.m3u8 stream URL (Cloudflare Stream, your own CDN) — on its own
line, and learners get a player. Nothing loads from
the provider until they press play, so lesson pages stay fast and
cookie-quiet. The same URL inside a sentence stays an ordinary link,
and so does anything that can't embed — the editor tells you why while
you type. There's no video upload, on purpose: your channel keeps its
views and analytics, and your academy never meters streaming.
Duplicate anything: a lesson (the copy opens instantly, as a draft with fresh history), a section with its lessons, or a whole course — a private course's copy stays private. Accidental copy? Fresh drafts can be deleted for real with an inline confirm; anything with publish history moves to the Deleted drawer instead, restorable at any time.
Version history is automatic: every publish snapshots the lesson. A tool release broke your course? Open the history drawer, restore any earlier version to your draft, review, and republish — the bad version stays in history, nothing is ever rewritten.
Review & publish
Publishing scales from one lesson to one deliberate release, and Review & publish (the first row at the foot of the outline) is the launch path:
- In the editor, Publish ships the lesson you're on — no ceremony — and tells you whether learners can see it yet (a lesson published inside a draft course waits for the course to open).
- Review & publish shows three things before you act. Availability:
whether the course is open to learners, who can open it (listed,
unlisted, or hidden; everyone or selected accounts), and how many
lessons are published — with Unpublish course here, naming what
learners lose. What will publish: every draft or changed lesson as
a row with its change size (+12 −3 lines, "new lesson", or "title
only"), a link to the full diff, and a checkbox. What's unfinished:
rows are grouped ready / unfinished / empty / blocked — only
ready lessons start selected, so an empty lesson from a pasted
outline or one still carrying a
TODOnever ships by accident (you can still tick it deliberately); lessons that fail validation can't be selected at all, with the reasons on the row. Labs read Test failed (the solution doesn't pass its own checks) apart from not tested since the last edit — a reminder to press Test lab, never a blocker. - A draft course opens in the same act: tick Also open the course to learners in this release and the button reads "Publish N lessons and open the course". A course can't be opened with nothing published in it.
- Add an optional publish note ("Updated for product v2.1"), press publish, and the outcome stays on the screen: which lessons went live, which were refused (and why), and whether learners can see them now — with links to the published course and to history.
- Course history shows every publish grouped by release — when, by whom, with what note, and which lessons — so "what changed in this course last month" is one page, with a diff per lesson.
For repo-synced courses the same screen is a sync report: merging to your branch is the release act, and the commit message becomes the release note in course history.
The Courses page
Admin → Courses is a table built for an academy of a hundred courses, not ten: 25 per page, sortable by title or last updated (the newest of the course itself and any of its lessons — shown as "3 h ago" with the exact time on hover, created date underneath), and a search box that matches titles, URLs, and tags (partial, case-insensitive; the row shows its URL and tags, so a hit is never a mystery). Filter tabs answer the management questions: Status (published / draft), Needs publishing (courses with draft lessons or unpublished edits), Audience (everyone / restricted / unlisted / hidden / closed to new), and Source (admin / repo, shown once a synced course exists). Each row shows:
- Course — the title with its attributes (Featured, Coming soon, Synced from repo), the course URL with a copy button, its tags, and a View link only when a public page exists (a live course or a coming-soon teaser — drafts have no public page yet).
- Status — Published or Draft, as quiet grey text, and an amber "N to publish" badge when lessons are waiting (it opens Review & publish). Amber is kept for what needs you, so the courses waiting on a release stand out down the column.
- Lessons — how many are published, then what's pending (drafts, changed) and what's empty.
- Access — what learners can actually reach: everyone, N accounts, unlisted, hidden, existing learners only, or not open. "Published" alone never means learners can open a course.
New course is a row at the top; click it to open the Blank / From template / Paste an outline panel (an empty academy opens it for you). Enter in the title creates; what you type survives a search or a reload in this tab until the course exists, and a failed create keeps the form with a Retry.
The Actions menu on a row leads where each entry says: the editor, a preview of the first lesson, the live page (when there is one), Review & publish (with the pending count — the way to release changes), the course and access settings, duplicate (copies get distinct names: "(copy)", "(copy 2)"…), and delete. A draft course with published lessons also offers Open to learners — that opens the course now with what's already published and leaves pending edits unpublished; it is not a way to release them. Unpublish and delete confirm first and name the consequence: delete is permanent for a never-published course with no history, and a move to Deleted for anything that ever published. The Deleted drawer at the bottom lists each course with its URL, when it was deleted, and the state it holds; restoring a course that was open to learners reopens it immediately, and the drawer says so before you confirm. Leaving the list from the editor (⌘K → All courses) or after a delete returns you to the search, filters, and page you had.
Lesson Library
Some lessons belong to every course — "Install the CLI", "Get an API key". The Lesson Library keeps one source of truth for those and shares it with consent, never behind your back:
- The library lives under Admin → Library: its lessons open in the same editor as any course lesson (same labs, same validation), inside the admin pages rather than a course workspace, and it is never visible to learners. New shared lesson creates the library itself the first time. The list is searchable, sortable (name, recently updated), and filterable by what maintenance needs — not used yet, update waiting, with local changes — 25 lessons a page.
- Each shared lesson shows its impact under its name — "used in 3 courses · 2 copies waiting for this version · 1 with local changes" — and the same summary heads the lesson page, with the copies on demand: course › section, whether the copy is in sync, waiting for the current version, or carrying local changes, and whether its learners have the draft ("published", "published · draft awaits publish", "not published"). "In sync" is a statement about drafts: a course's learners see nothing new until that course publishes. A copy in a template is labeled as such — it seeds new courses, learners never see it.
- Courses add a shared lesson as a linked copy — a real lesson in the course, with its own URL, publish state, and history. Two doors: Add to course… on the library lesson (pick the course, then the section; a course without sections gets one created) and + From library in a course's outline (search, an inline look at the content, a preview in a new tab, and "already in this course" when a copy is there — you can open it or add another). Add to library does the reverse: it promotes an existing lesson into the library and leaves the linked copy in its place, nothing breaks or moves.
- When the library version changes, every linked copy shows update available — in the course outline, on the lesson, and on Review & publish. Pulling shows you the comparison first — current course draft → incoming library version, the direction the update writes — then replaces the copy's draft only; learners see nothing until that course publishes, through its own release act. The draft it replaces is kept in your browser and offered back in the editor. If the draft or the library lesson changed while the comparison was open, nothing is replaced and you are asked to reload it — another author's edits are never applied or overwritten unseen. When a change is too large to show line by line, both versions are readable in full and the update still applies.
- Titles stay independent. A copy's title is the course's own: renaming the library lesson never renames copies. When you pull an update, a checkbox can adopt the library title if you want it.
- Editing a copy directly marks it diverged — the link pauses and updates stop being offered, because your edits win. You can compare with the library and re-adopt its version at any time. If you edit a copy until it matches the library again, it reads in sync; Mark as synchronized re-links it so the next library change shows up as an update rather than a divergence.
- Make independent keeps a copy — URL, content, history, learner records — and stops it following the library. It is also how you clear the way to delete a shared lesson: a library lesson can't be deleted while courses link to it.
- Shared lessons never publish, so they have no publish history. Save a version keeps a snapshot you can diff against or restore (the lesson's Versions drawer; template lessons have the same button, for the same reason); a shared lesson with saved versions deletes to the library's Deleted drawer, restorable — while it sits there its copies read as ordinary lessons and follow it again once restored — while one without deletes for good. Duplicate copies a shared lesson into the library with no links.
Nothing is ever synced silently: publishing the library never touches a course, and pulling an update is always your explicit choice, made with the comparison in front of you.
Course templates
Most academies' courses share a starting point — an intro section, a welcome lesson, a recap at the end. Build it once as a template and every new course can start from it. Templates live under Templates in the admin sidebar; they are edited like any course but are never visible to learners and can't be published — they exist to be copied.
A template is copied, never linked: a course created from one is independent from the first second. Editing the template changes what future courses start with; courses already created from it keep what they have. The same holds the other way — Save as template on a course makes an independent copy under Templates, and later edits to the course don't reach it.
What a copy carries: every section and lesson with its current draft text (not a published version), the About page, the short description and course card (image, level, duration, tags), and the search & social preview. What it deliberately leaves behind: visibility and account access, certificate settings, the recommended next course, catalogs and featured, publish state and history — the new course starts as a draft with the defaults every new course gets.
The Templates page. Each row shows the name, "N sections · M lessons · K empty", when it was last edited, the description, and an Outline disclosure with the sections and lessons — enough to choose without opening the editor. Search by name or description; sort by Name (the same order the From-template picker uses) or Recently updated; 25 per page. Use template on a row opens the New course panel with that template picked — you name the course, see its URL, and create. The row's Actions menu opens the editor or Template settings, renames, duplicates (the copy stays a template, in this list), and deletes.
Creating a template. New template at the top offers Blank (an empty template, opened on "Create your first lesson") and From an existing course (pick the course, keep or change the suggested name — "Onboarding (template)", then "(template 2)" — and create). Save as template… in any course's settings is the same act from the other side.
Inside a template. The editor is the course workspace with template wording: the outline strip counts sections, lessons, empty lessons, and validation errors (never "published" or "draft" — templates don't release anything); Use this template sits at the bottom of the outline, with Template settings (the defaults new courses start with — the About page, card, and search preview copy into every course made from it) and ← All templates under it. The breadcrumb reads Admin › Templates. There is no Access, no Completion, no history, no analytics for a template.
Deleting a template. A template that has held lessons moves to the Deleted drawer at the bottom of the Templates page and can be restored exactly as it was; only a template that never held a lesson is removed for good. Courses created from a deleted template are unaffected. If a New course panel remembers a template that has since been deleted, it says so by name and asks you to pick another — it never quietly swaps in a different template.
Creating a course offers three doors, and each shows what it will do before it does it — the permanent URL the title mints (renaming later never changes it), and the structure about to be created:
- Blank — an empty course. One click on Create your first lesson creates the first section for you and opens the editor with the title selected.
- From template — pick a template (or arrive from Use template); the door lists what the new course starts with, what is copied and what isn't, and that the copy is independent.
- Paste an outline — paste a markdown list and watch it parse as you type: headings become sections and list items become draft lessons; with no headings, indent lessons under their section instead. Outline-created lessons start empty, and Review & publish keeps them unselected until you write them.
Where the rest lives
Every other authoring surface has its own page:
- Course settings — details, course card, visibility and access, completion.
- Lab authoring — the lab block, every check type, and Test lab.
- Home & catalogs — the academy home, composing it yourself, catalogs, learning paths.
- Content freshness — video transcripts, claims, and docs reviews.
- Integrations & exports — courses as code (GitHub sync and publish on merge), webhooks, metric ingest, CSV.
On the roadmap
- Full content export: your entire academy — markdown, assets, structure — in one click. Portability is a promise, not a feature flag.
Design partners: we'll set up your staff seats — from there the academy is fully self-serve, and we stay a message away.