Content Pipeline

Lessons are data. Boxes, topics, worlds, puzzles, interactive parameters, release notes and remote switches are written as YAML in git, validated against a shared schema, compiled, hashed, signed and published to the edge. A new lesson never waits for an app store review.

From YAML to the edge

flowchart LR
  subgraph GIT["Git"]
    Y["content/*.yaml<br/>boxes · topics · worlds<br/>puzzles · release info"]
  end
  subgraph CLI["content-cli"]
    V["validate<br/>schema + references<br/>build-on graph has no cycles"]
    C["compile<br/>catalog · taxonomy · puzzles<br/>per-box bundles"]
    H["hash<br/>SHA-256 file names"]
    S["sign manifest<br/>Ed25519"]
  end
  subgraph EDGE["Worker + R2"]
    U["upload only missing files"]
    M["write manifest<br/>(preview or live)"]
  end
  Y --> V --> C --> H --> S --> U --> M
StepWhat happensFails the build when
ValidateEvery file is parsed against content-schema; ids, topic and world references and "builds on" links are checkedA field is missing, an id is duplicated, a reference dangles, or prerequisites form a cycle
CompileBoxes are grouped into a catalog, a taxonomy (worlds → topics → boxes) and per-box bundlesA box needs an interactive the registry doesn't know
HashEach output is named by its SHA-256, e.g. catalog.<hash>.json—
SignThe manifest lists every file, its hash and size, the engine version and release info, and is signed with the content keyThe signing key is missing
UploadThe CLI sends a checksum of every file; the publish API answers which are missing or stale, and 8 parallel uploaders send only thoseAn upload is refused
SwitchThe manifest is written last, so readers never see a half-published build—

The manifest

classDiagram
  class SignedManifest {
    +ManifestBody body
    +string keyId
    +string signature
  }
  class ManifestBody {
    +int schemaVersion
    +string environment
    +string builtAt
    +string catalog
    +string taxonomy
    +string puzzles
    +File[] files
    +ReleaseInfo release
  }
  class File {
    +string path
    +string sha256
    +int bytes
  }
  class ReleaseInfo {
    +Platform ios
    +Platform android
    +WebInfo web
    +Notes notes
    +Flags flags
  }
  class Platform {
    +int latest
    +int min
  }
  SignedManifest *-- ManifestBody
  ManifestBody *-- File
  ManifestBody *-- ReleaseInfo
  ReleaseInfo *-- Platform
  • The manifest is the only file that changes between publishes. The edge caches it for 60 seconds; every other file is cached as immutable.
  • The client verifies the signature against public keys pinned in the app build, choosing the key by keyId. A new key can be added in one release and used in a later one, so rotation never needs an emergency update.
  • Each file is checked against its SHA-256 before it is used or cached.

decision Sign the manifest, hash everything else. One signature over a list of hashes covers every file, so the client needs only one signature check per start, and files can be cached anywhere without trusting the cache.

Channels, promote and rollback

stateDiagram-v2
  direction LR
  [*] --> Preview: publish ?channel=preview
  Preview --> Live: promote
  Live --> Live: publish
  Live --> Earlier: rollback
  Earlier --> Live: publish again
  note right of Preview: testers opt in by tapping<br/>the version on Me five times
Endpoint (token-protected)Purpose
missingGiven a list of hashes, return the ones not yet stored
fileUpload one content-addressed file
manifestWrite the live manifest, or the preview one with ?channel=preview
promoteCopy the preview manifest to live
rollbackPoint live at an earlier manifest
manifestsList recent manifests for rollback

Every manifest is also kept under its build time, and files are never deleted on publish, so a rollback is just a manifest switch: instant, and safe for clients still holding older files.

Compatibility

Old app builds must keep working when content moves ahead of them.

RuleMechanism
Unknown fields are ignoredReaders never reject extra keys
New capabilities are opt-inEach catalog entry lists what it requires (interactives and layer kinds); a build that can't play it locks that box instead of crashing
Lessons travel with boxesOld content that names a lesson without carrying it falls back to a lesson builder loaded on demand
Very old builds are told to updateSigned release info carries latest and min per platform; below min the app shows a blocking update screen
Broken web copies are refreshedA signed web.min makes copies left open on a bad version reload

success The same rules let content ship daily while phones update weekly or less.

Remote switches

Boolean feature flags live in the signed release info inside the manifest. They take effect on the next content check, without a deploy, and are as tamper-proof as the lessons.

${sch.nav()}