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.
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
| Step | What happens | Fails the build when |
|---|---|---|
| Validate | Every file is parsed against content-schema; ids, topic and world references and "builds on" links are checked | A field is missing, an id is duplicated, a reference dangles, or prerequisites form a cycle |
| Compile | Boxes are grouped into a catalog, a taxonomy (worlds → topics → boxes) and per-box bundles | A box needs an interactive the registry doesn't know |
| Hash | Each output is named by its SHA-256, e.g. catalog.<hash>.json | — |
| Sign | The manifest lists every file, its hash and size, the engine version and release info, and is signed with the content key | The signing key is missing |
| Upload | The CLI sends a checksum of every file; the publish API answers which are missing or stale, and 8 parallel uploaders send only those | An upload is refused |
| Switch | The manifest is written last, so readers never see a half-published build | — |
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
keyId. A new key can be added in one release and used in a later one, so rotation never needs an emergency update.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.
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 |
|---|---|
missing | Given a list of hashes, return the ones not yet stored |
file | Upload one content-addressed file |
manifest | Write the live manifest, or the preview one with ?channel=preview |
promote | Copy the preview manifest to live |
rollback | Point live at an earlier manifest |
manifests | List 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.
Old app builds must keep working when content moves ahead of them.
| Rule | Mechanism |
|---|---|
| Unknown fields are ignored | Readers never reject extra keys |
| New capabilities are opt-in | Each 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 boxes | Old content that names a lesson without carrying it falls back to a lesson builder loaded on demand |
| Very old builds are told to update | Signed release info carries latest and min per platform; below min the app shows a blocking update screen |
| Broken web copies are refreshed | A 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.
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()}