Under the hood

You don't need this page to author a course. It's here so you know what's possible when a project pushes past the defaults, and what your assistant is working with when it says yes. Every topic below is specified in full in the technical guide your assistant reads.

Course configuration

Each course has one configuration file, course.config.js, covering title and identity, branding (logo, primary colour, font), navigation mode (free or sequential), the completion rule, the passing score, the export standard, and accessibility strictness. Everything has a sensible default; a bare title is a valid config. Your assistant edits it from sentences like "make navigation sequential and raise the pass mark to 80".

The hooks API

Six hooks are the stable contract between custom components and the runtime: questions, quiz control, navigation, progress, completion, and saved state. The point of the design: a custom widget that registers through the question hook is scored, persisted, and reported to the LMS identically to a built-in question type. Custom work is never tracking-blind.

The saved-state hook is worth calling out: a custom interaction can persist a learner's in-progress work, and it survives a reload or a return the next day on every export target, browser storage on the web, the LMS's own saved state under SCORM and cmi5. A half-finished interaction resumes exactly where the learner left off, with no work on your part beyond storing the value.

Custom layouts and quiz shells

Two drop-in files change a course's chrome without forking anything: layout.svelte replaces the default sidebar-and-topbar shell, and quiz.svelte replaces the quiz presentation while the underlying scoring machinery stays put.

Custom access rules

Beyond free and sequential navigation, a course can define its own access logic: prerequisites, unlocking a module only after a specific quiz score, time-based gating. It's a small function with full access to progress and course structure.

xAPI destinations

Custom xAPI statements can flow to the LMS's own record store (for cmi5/xAPI launches), to an external LRS, or fan out to several destinations at once, each with its own queueing and retry. Statements carry learner identity automatically, on SCORM exports too, where the runtime derives the actor from the LMS session. One hard rule: the course configuration ships inside the package where anyone can read it, so LRS credentials must come from a token endpoint, never be pasted into config.

Assets and styling

Media lives in an assets/ folder per course and is referenced through the $assets/ alias, which is checked at build time: a missing file fails the build instead of shipping a broken image. Styling layers from coarse to fine: branding config for colour and font, a set of --tessera-* CSS custom properties for theming, and a styles/ folder that overrides anything.

Build customisation

The build itself is owned by Tessera (there's no bundler config to maintain), with two escape hatches: tessera.config.js merges partial build settings on top, and web exports emit a baseline Content-Security-Policy that can be extended when a course legitimately loads external fonts or media.

The guardrails

A few constraints keep courses portable and updatable, and your assistant observes them: page content is static (no runtime data fetching inside pages), only the public API is used (never internal paths that change between versions), and framework behaviour is extended through the extension points above, never by patching installed files.

The full reference

The canonical specification of all of this, hooks signatures, config fields, xAPI behaviour per standard, recipes, ships inside every workspace and updates with the framework: AGENTS.md. It's written for your assistant, but it's readable, and if you want to know exactly how something works, it's the source of truth.

Copied