Versioning
How PanelWave manifests declare their format version, what the version numbers mean, and how to upgrade older manifests with the CLI.
Every manifest declares which format version it was written for in the required top-level panelwave header. Consumers use it to decide how to interpret the document and whether an upgrade is needed.
The panelwave header (PanelwaveHeader)
| Property | Type | Required | Constraints | Description |
|---|---|---|---|---|
version | string | Yes | Pattern ^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z-.]+)?$ | Semantic format version the manifest targets, e.g. "1.1.0". Pre-release suffixes (1.2.0-beta.1) are allowed |
schema | string (Uri) | Yes | Absolute URI | URL of the schema the manifest validates against |
generator | string | — | — | Free-form tool identifier that produced the manifest, e.g. "panelwave-cms/2.3" |
{
"panelwave": {
"version": "1.7.0",
"schema": "https://panelwave.org/schema/1.0/panelwave.schema.json",
"generator": "panelwave-cms/1.0"
}
}
Version semantics
The format follows semantic versioning:
| Segment | Example | Meaning |
|---|---|---|
Patch (1.0.x) | 1.0.1 | Clarifications and non-functional changes — no structural impact |
Minor (1.x.0) | 1.1.0 | Backward-compatible additions — new optional fields and enum values |
Major (x.0.0) | 2.0.0 | Breaking changes — requires migration |
One schema file per major version
The schema file lives in a directory named after the major version. Minor versions are additive and ship in place within that directory:
- Current format version: 1.7.0
- Schema location:
1.0/panelwave.schema.jsonin the panelwave/schema repository - Schema
$id:https://panelwave.org/schema/1.0/panelwave.schema.json(unchanged by minor releases; an identifier — the URL is not publicly served yet)
This is why a 1.7.0 manifest still points its schema URI at the 1.0/ path — the 1.1/1.2/1.3/1.4/1.5/1.6/1.7 additions were merged into the same schema file, and every valid 1.0 manifest remains valid against it.
Current releases
The format and the open-source packages that read and write it, as of 2026-10-04:
| Release | Current version | Format | Notes |
|---|---|---|---|
| Format / schema (panelwave/schema) | 1.7.0 | — | 1.0/panelwave.schema.json, CC BY 4.0 |
@panelwave/cli | 1.2.0 | bundles 1.7.0 | Validates offline and upgrades to 1.7.0. CLI 1.1.0 bundled 1.6.0, CLI 1.0.0 1.5.0. See CLI |
@panelwave/types | 1.2.0 | 1.7.0 | TypeScript interfaces for the manifest, including the 1.7.0 fields |
@panelwave/player | 1.2.0 | up to 1.6, plus Edge.label | Chapters without edges, x-locked panels, Hotspot.display and textColor arrive with the release after 1.2.0 (already live on read.panelwave.org and in the CMS preview). See Player |
@panelwave/mcp | 0.1.0 | — | Local bridge to the PanelWave MCP server for desktop AI assistants. See Local bridge |
All packages are MIT-licensed and published from github.com/panelwave with npm provenance. The CMS exports and publishes 1.7.0 and imports formats 1.0–1.7.
Compatibility rules as encoded in the schema
Backward compatibility (old manifests, new schema). Minor releases only add optional properties and new enum values. A manifest written for 1.0.0 validates unchanged against the current schema. Example: schema 1.1 added playMode/startMode to VideoLayer, but the legacy 1.0 fields autoplay and loop remain valid and are not deprecated — consumers map them at read time (loop: true → playMode: "loop" and autoplay → startMode only when the new field is absent; a present new field always wins). See Video for the full mapping.
Forward compatibility (new manifests, old consumers). The schema is strict about unknown properties (additionalProperties: false / unevaluatedProperties: false on most objects), so a 1.0-era validator will reject fields introduced later. Custom data that must survive round-trips belongs in x- prefixed extension fields instead.
Deprecation policy. Deprecated fields are marked with "deprecated": true in the schema and remain supported for at least one major version.
What changed in 1.7.0
Relaxing and backward-compatible with 1.6.0 — existing manifests remain valid unchanged.
Graph.edgesmay be empty. TheminItems: 1constraint is gone; the property itself is still required ("edges": []). A chapter without edges — typically a single-panel chapter — follows its reading order. Graph.x-extension fields on panels and extras blocks.PanelandExtraBlock(and with itExtraCharacterSheet) accept^x-properties with any value, in addition to the manifest root. Motivating use: server-side paywall views mark stripped panels"x-locked": true. Extensions.
Four additive fields were folded into 1.7.0 on 2026-10-04, before its tooling release — all optional, so existing manifests stay valid:
Edge.label(LocalizedString): the reader-facing choice text of a path, shown on the branch chooser's buttons. Graph.Hotspot.display(auto|button|area, defaultauto): whether readers see the hotspot's label as a button or an invisible click area over the artwork.autoshows a button when the panel'sgoTohotspots lead to two or more panels (a choice). Hotspots.BalloonConfig.textColor(hex color, default#000000; also onBalloonConfigOverride): the lettering color, e.g. amber on-screen text on a dark fill. Speech Bubbles.- Variable ids may use underscores (
trust_jonas): the pattern is now^[a-zA-Z0-9][a-zA-Z0-9_]*(?:\.[a-zA-Z0-9_-]+)*$. Variables.
@panelwave/cli 1.2.0 and @panelwave/types 1.2.0 (released 2026-10-04) bundle and type 1.7.0. A validator with an older schema copy (CLI 1.1.0 or earlier) rejects empty edges, x- fields on panels and the four fields above — update with npm install -g @panelwave/cli@latest.
What changed in 1.6.0
Additive; existing manifests remain valid unchanged. A purchase paywall rule can list every product that unlocks it, and the manifest can describe those products for readers:
PaywallRule.requiredProductIds(Identifier[], unique, at least one item): owning any one of the listed products satisfies the rule, and players offer one Buy option per product. Only purchase rules use it.- Without it, nothing changes:
requireEntitlementstays the only product hint, and a reader that cannot resolve it accepts any purchase. Exporters that also target older players keeprequireEntitlementset to the first listed product. paywall.products(PaywallProduct[]: requiredid, optionalname/descriptionasLocalizedString,price{ amount, currency },typepurchase|subscription): display info for the products and tiers the rules reference by id. Players label each Buy / Subscribe option with the entry's localized name, description and price, falling back to the rule's name / price, then the id. Informational only — the rules decide what unlocks what.
Details: Paywall → Product lists, Paywall → Products.
Three more additions were merged into 1.6.0 on 2026-09-30 — the version number did not change, and every manifest that was valid before is still valid:
- Layer keyframe animations.
PanelAnimationsgainskeyframes(AnimationKeyframe[]: requiredlayerId,property,timeMs,value; optionalid,easing),loopandname. Nine animatable properties; offsets are fractions of the panel size. The viewport-move fields are unchanged. Animations. - Several alternate covers.
extras.alt_coveraccepts oneExtraBlock(as before) or a non-empty array. Extras. - Ensemble character sheets.
ExtraCharacterSheetacceptscharacterIdsnext tocharacterId; at least one of the two is required. Extras.
A validator that bundles an earlier copy of the 1.6.0 schema rejects manifests using these fields — use @panelwave/cli 1.2.0 or later.
What changed in 1.5.0
All additive; existing manifests remain valid unchanged. 1.5 adds authoring metadata blocks so a work can round-trip between authoring systems (they power the CMS's work archive transfer) — rendering consumers may ignore all of them:
assets.folders(AssetFolder[]): the asset library's folder tree —id,name, optionalparentId(nesting) andorder. Assets reference folders through the newfolderIdsarray on the asset entry (n:m — one asset can be filed in several folders).- Top-level
localization(Localization): translation workflow state.locales[]lists the work's locales withisDefault/isActive;entries[]carries one row per translatable string —key,defaultText, optionalcategory/context, anisStaleflag, and per-localevalues(textplus amachineflag marking machine translations).
Rendered text in the manifest stays in LocalizedString objects as before — the localization block does not replace them; it preserves the workflow state (stale markers, machine-translation flags) that authoring tools need and players do not.
Details: Assets, Localization concepts.
What changed in 1.4.0
All additive; existing manifests remain valid unchanged. Introduces the infinite canvas — a chapter's panels on one continuous world-space plane, read by a camera that follows the story graph:
Chapter.canvas(CanvasLayout): world-unitplacements(map of panel id →CanvasPlacementwithx/y/w/h,z,r,origin, panel-relativeenterFraming,revealMode), planebackground,camerapolicy (fitMode,overview,freeRoam,bounds), and presentationaldecorations. World units: 1 unit = 1 CSS px at zoom 1.Edge.cameraMove(CameraMove): per-edge camera travel —path(direct|arc|waypoints+WorldPoint[]),zoomProfile(hold|pull-back|dive),durationMs,easing,reducedMotionFallback. Inherits liketransition: edge →settings.outputPresets[format].defaultCameraMove→ built-in direct move.FormatPreset.canvasView(defaultfalse) andFormatPreset.defaultCameraMove: canvas view renders only when the active format enables it and the chapter has a canvas — otherwise players fall back to panel view. Print formats ignorecanvas.- Semantic validation rules (placement keys must be chapter panels, etc.) are enforced by the CLI and CMS preflight — JSON Schema cannot express them structurally.
Details: Infinite Canvas, Graph, Settings.
What changed in 1.3.0
All additive; existing manifests remain valid unchanged:
- Reusable style presets:
settings.typography.textStyles(name →TextStyle) andsettings.typography.balloonPresets(name →BalloonConfigOverride), referenced by the newstyleRefproperty onTextLayerandSpeechBubble. Text resolution: work defaults → preset → inlinestyle; balloon cascade: workballoon_config→ character → preset → inlineballoonConfig. UnknownstyleRefs are ignored. TextLayer.styleis now the shared$defs/TextStyledefinition (same shape as before; also used by the preset map).- The speech toggle is implicit: every speech bubble is inherently subject to the reader's speech toggle (initial state:
settings.ui.speechDefault), ANDed on top ofvisibleIf. Per-bubblevisibleIf: {"var": "prefs.speech"}boilerplate is obsolete;visibleIfis reserved for story logic.
Details: Settings, Speech Bubbles, Layers.
What changed in 1.2.0
Relaxing, backward-compatible — lets exporters write leaner manifests:
mimebecame optional onImageVariant,AudioVariant,VideoVariant,SubtitleVariant, andVectorVariant; consumers derive it from thesrcfile extension. Still required when the extension is missing or misleading.Edge.transitioninheritance clarified: an edge without atransitioninherits the active format'ssettings.outputPresets[format].defaultTransition, falling back to a plain cut.- Documented export discipline: omit values equal to schema defaults (e.g.
shareable: true, placementz: 0/r: 0).
Details: Assets, Graph, Settings.
What changed in 1.1.0
All additive; existing manifests remain valid unchanged:
VideoLayer:playMode(once|loop|pingpong|loop-from),loopFromMs,startMode(on-view|on-hover|on-click),controlsVideoVariant:direction(forward|reverse) for pre-rendered reverse encodes (smooth ping-pong)AssetCatalogItemVideo: optionalposter(VideoPoster)settings.ui: work-level defaultsvideoPlayModeDefault,videoStartModeDefault,videoMutedDefaulttracking.eventWhitelist: new eventsvideoPlay,videoPause,videoEnded,videoLoop
Details: Video, Assets, Settings, Tracking.
Upgrading manifests with the CLI
@panelwave/cli ships an upgrade command that normalizes a manifest to the latest supported format:
# Preview the changes without writing anything
panelwave upgrade ./my-comic/panelwave.json --dry-run
# Upgrade in place
panelwave upgrade ./my-comic/panelwave.json
# Write to a different file
panelwave upgrade ./my-comic/panelwave.json --output ./my-comic/panelwave.upgraded.json
The upgrade currently performs these normalizations:
- Adds a missing
panelwaveheader - Updates
panelwave.versionandpanelwave.schemato the CLI's target version - Derives a missing
meta.localesarray frommeta.default_locale - Renames the pre-1.0 draft layer field
typetokind - Adds a missing
graph.edgesarray
The CLI's upgrade target is always the version declared by the schema bundled in that CLI release — 1.7.0 in CLI 1.2.0 (1.6.0 in CLI 1.1.0, 1.5.0 in CLI 1.0.0); the SDK build fails if the two diverge (see CLI → Bundled schema). Update with npm install -g @panelwave/cli@latest when a new format version ships. Since 1.1–1.7 are purely additive or relaxing, older manifests need no migration to be read by current consumers — upgrading normalizes the header and legacy fields. Run --dry-run first and review the reported steps before overwriting files.
After upgrading, always re-validate:
panelwave validate ./my-comic/panelwave.json
See Validation for validation options and CLI for the full command reference.