CursivaDocs
Learners

Progress and certificates

Progress records how far each learner has advanced through the steps of a content and across the entire tree of modules that make up an experience. A single enrollment at the root covers that whole tree, and full completion is what unlocks the certificate.

How progress is recorded

The progress of an enrollment is a map that associates each content with the steps completed inside it — in code, something like Record<string, string[]>: the key is the content id, and the value is the list of section ids already completed. The root is just one more entry in this map, with no special handling: a single map, keyed by content id, covers the entire course.

Each section is addressed by its stable id (the title's blockId, with position as a fallback). Because progress stores stable ids rather than positions, reordering the content in the editor never corrupts the advancement already recorded: the "completed" section stays the same, whether it sits first or last.

Enrollment fieldWhat it stores
progressMap content id → completed section ids, including the root
stepCompletedAtMap content id → section id → completion moment (in ms)
completedAtThe moment the entire tree was completed for the first time, or empty
statusactive grants access; a refunded or delinquent enrollment keeps the history but does not grant access

One enrollment covers the entire tree

The root content is the sellable unit. The enrollment is recorded on it, and that single enrollment covers the entire recursive tree of sections and references beneath it — at any nesting depth. References (modules) are navigated continuously under the same root enrollment: there is no separate enrollment per subcontent.

Access and progress read exactly the same published tree (the organization's completion manifest), so they never disagree about the shape of the course. A content only accumulates progress if it belongs to the enrolled tree — attempting to record advancement in a content outside the tree is rejected.

  • Reachability is verified by a traversal of the reference graph, safe against cycles: a set of already-visited nodes prevents infinite loops.
  • A content that is referenced but never published does not count as completed (there are no completion facts for it), so it never silently closes the tree.
  • The manifest is assembled once per organization and reused for all rows — no document is loaded per learner to compute progress.

Path steps

The path of a content is the ordered sequence, in document order, of its sections (titles) and the references to subcontents. Each type of step is completed in its own way:

TypeHow it is completed
SectionMarked as completed within the content itself
Reference (module)Completed when the entire referenced subtree is completed — pure arithmetic over the manifest, without loading the content

Contents with no titles still form a single step. An experience composed only of references uses the modules' completion, without creating an extra empty step.

Each step exposed to the learner carries these signals:

Step fieldMeaning
doneThe step is completed
availableThe release rule has been satisfied — the learner can act on it
currentFirst released step not yet completed (the "continue here" highlight)
lockedRelease rule not yet met — content hidden, cannot be completed
lockReasonWhy it is locked: drip or prerequisite
unlockAtDrip only: the moment (in ms) when the step releases, for the countdown

Prerequisites and release

A single evaluator decides the availability of every step, with different semantics per type:

  • Sections release by prerequisite. With no explicit rule, a section follows the immediately preceding step (sequential path). An empty list means "always open." A list of ids requires that all listed steps be completed — which allows nonlinear paths.
  • References (modules) release by their release rule: drip (time) plus explicit prerequisites. A module is never blocked by mere document order.
Lock reason (lockReason)When it appears
prerequisiteA required step has not yet been completed
dripThe time-based release has not yet arrived; unlockAt carries the moment

Time rules and prerequisite rules coexist: the learner receives only the steps available at that moment.

Drip anchor

Time-based release is measured from an anchor that the read and write paths share (a mismatch would show the step as open and still refuse completion):

  • Evergreen courses release relative to the learner's enrollment date.
  • Time-based courses release relative to the learner's cohort start — the entire cohort opens together. With no cohort, it falls back to the enrollment date.

A step whose drip is anchored to the previous step only starts counting once the predecessor is completed; until then it stays locked, with the countdown not yet started.

Completing a step

Marking a section as completed passes through guards in the core (not just in the interface):

  1. The section must be available — you cannot jump to a locked step. Unknown ids are refused.
  2. If the section carries a quiz, it is only completed after the learner has submitted the questionnaire (any graded attempt that touches the questions from that pool). Without a submission, completion is refused.

On completion, the moment is stamped in stepCompletedAt (which feeds the drip anchored to the previous step) and cleared on undo. Completion is recomputed over the entire tree. When that recomputation marks the tree as completed for the first time, completedAt is written — this is the trigger for both the completion email and the content.completed webhook.

Acceleration and adaptive review

Quiz performance feeds two adaptive features that lean on the same progress model:

  • Review hints point to the root's direct modules whose last quiz fell below 70% — "you struggled here, worth revisiting."
  • Test-out lets the learner skip a module they have proven through the quiz: with a score of >= 80% on the last attempt, all sections of that module are marked as completed under the root enrollment, and the path advances. Sections are completed in ascending order, respecting sequential release.

Certificates

When the experience is completed, the certificate is tied to the completion and has its own page, at the content's URL within the organization (/{org}/{content}/certificate). Access requires being authenticated.

The certificate's gate is completion of the entire tree (the live rootCompleted signal), not just the root's own sections — anyone who has not yet finished is redirected back to the content. The page displays:

  • the certificate of completion label;
  • the learner's name and the content title;
  • the issuing organization and the completion date (derived from the stored completedAt);
  • actions to print, return to the content, and go to "My learning."

Notification emails

Two emails accompany the learner's journey. Both are best-effort: they are side effects that never block the main action — failures are logged, and nothing is sent when the email provider is not configured.

EmailWhen it firesWhat it carries
EnrollmentRight after the enrollment is createdConfirmation and a link to get started; the enrollment row is the source of truth, the email never blocks it
CompletionThe first time the entire tree is completed (justCompleted)Congratulations and a link to the certificate

Restart and monitor

The team with edit rights can restart a learner's course: this erases the enrollment's entire progress map — the root and all referenced modules — clears the completion timestamps, and reopens completion. The quiz history remains, since it feeds adaptive mastery and insights.

In team monitoring, each learner appears with step-by-step progress (completed steps over the total, in the same format the learner sees) and the last quiz result, and each content carries the count of enrolled and completed learners.

Next steps