Schema reference

This is the manifest and the wire protocol in the form you look things up in. It follows the specification (version 1.0, Draft 2), which stays the authority where the two differ.

The same rules exist as JSON Schema (2020-12), published here:

Put this as the first field of a manifest and an editor that understands JSON Schema will complete and check it as you type:

JSON
{ "$schema": "https://remora.anovelproject.com/schema/manifest.v1.schema.json" }

The manifest

A manifest is one JSON object. It describes the state an application publishes, the components that show it, and the scenarios that arrange them. This is a complete one:

JSON
{
  "specVersion": "1.0",
  "id": "com.example.cpu",
  "name": "CPU",
  "version": "1.0.0",
  "components": { "cpu": { "type": "dial", "label": "CPU" } },
  "scenarios": { "main": { "landscape": { "root": "cpu" } } },
  "defaultScenario": "main"
}

Top-level fields

FieldTypeRequiredDescription
specVersionstringYesThe version of the specification the manifest follows: "1.0".
idstringYesThe application ID. Reverse-DNS style, lowercase, stable across versions: com.example.cpu. See names and identifiers.
namestringYesA short name, shown in viewer listings.
versionstringYesThe author's own version of the template. Informational.
descriptionstringNoLonger text, shown in viewer detail views.
iconasset pathNoA square PNG or JPEG, at least 256 px.
fontsobjectNoFont name to asset path. The names are then usable as font in a text style.
stateobjectNoThe initial state document. Its contents are free-form.
componentsobjectYesComponent ID to component definition. See components.
scenariosobjectYesScenario name to scenario. At least one. See scenarios and layouts.
defaultScenarionameYesThe scenario shown when none is selected.
scenariobindingNoA binding whose value names the active scenario. See choosing the scenario.
$schemastringNoA pointer to the manifest JSON Schema, for editors. Servers and viewers ignore it.

A manifest may contain only the fields defined for the specVersion it declares. The schema is strict: a misspelt field name is an error at registration, not something silently ignored. The contents of state are the one exception.

Conventions

Names and identifiers

IdentifierChosen byForm
Name: a component ID, scenario name, action name, area name or font nameThe template author^[A-Za-z][A-Za-z0-9_-]{0,63}$
Application IDThe application authorReverse-DNS style, lowercase: ^[a-z0-9]+([.-][a-z0-9]+)+$, at most 128 characters.
State pathThe application authorSegments joined by ., each matching ^[A-Za-z0-9_-]+$. A segment made only of digits indexes an array: player.hp, team.0.name.
Package hashDerived64 lowercase hexadecimal characters: the SHA-256 of the archive.
Instance ID, Device ID, Server IDThe serverOpaque strings of at most 64 characters from [A-Za-z0-9_-].

Names are case-sensitive and scoped to their manifest. Nothing in a template needs to be globally unique.

All field names are camelCase. A receiver ignores fields it does not recognise.

Colours

A colour is a string in CSS hexadecimal notation: #RRGGBB or #RRGGBBAA. Alpha is FF when omitted, so #00000000 is fully transparent and #0000004D is black at 30%.

Lengths and layout units

Every length in a template (font sizes, gaps, padding, radii, border widths) is a number in layout units. One layout unit is 1% of the shorter side of the area the viewer gives the template. Nothing is in pixels, so a template scales as a whole across phones, tablets and watches.

A text size of 4 on a phone held sideways, whose shorter side is 400 px, is 16 px tall.

Asset paths

An asset is referred to by its path inside the package, relative to the archive root, with forward slashes: assets/portraits/warden.svg. A path must not be absolute, must not contain a .. or an empty segment, and must not contain a backslash.

An image reference is either a package asset path or a runtime asset reference of the form runtime:<path>, naming an image the application uploaded while running. See the asset message.

Durations are in milliseconds.

State

Each registered application has one state document: a JSON object owned by the application and held by the server. The manifest's state is its starting value, and doubles as documentation of the keys the template expects.

JSON
{ "state": {
    "player": { "name": "", "hp": 0, "hpMax": 100, "potions": 0 },
    "pet": { "away": false },
    "party": []
} }

The application changes the document by sending state patches. A template never names a wire message; it refers only to paths in this document. An application may publish keys the template does not use, and a template may refer to keys the application has not published yet.

Bindings

A binding connects a component property to the state document. Any property marked bindable in this reference accepts either a literal value or a binding object.

Value bindings

JSON
{ "bind": "player.hp" }

The property takes the value found at the state path. An optional default is used while the path is missing:

JSON
{ "bind": "player.name", "default": "—" }

When the path is missing and there is no default, the property takes its documented default.

Formatted text

A format string produces text. {} stands for the bound value, and {path} for any other state path:

JSON
{ "bind": "player.hp", "format": "{} HP" }
JSON
{ "format": "{player.hp} / {player.hpMax}" }

A placeholder may carry a number specifier after a colon:

PlaceholderValueResult
{}1234.51234.5
{:.0}1234.51235
{:.2}1234.51234.50
{:,}1234.51,234.5
{:,.0}1234.51,235

The precision is 0 to 6. Literal braces are written {{ and }}. A placeholder whose path is missing renders as an empty string. Nothing else inside a format string is interpreted.

Conditions

A condition is a binding that yields true or false. It is what visible, enabled, a toggle's value and a variant's when take. With no operator it tests whether the value is truthy:

JSON
{ "bind": "pet.away" }

One comparison operator may be added. The operand is a literal:

JSON
{ "bind": "player.potions", "gt": 0 }
OperatorTrue when the bound value is
eq, neequal, or not equal, to the operand: the same JSON type and value. The operand is a string, number or boolean.
gt, gte, lt, ltea number that compares accordingly with a numeric operand.

"not": true inverts the result:

JSON
{ "bind": "media.playing", "not": true }

The values false, 0, "", an empty array, an empty object and a missing path are falsy; everything else is truthy. A comparison against a missing path, or between mismatched types, is false.

Implicit binding

Every component type except button has one primary property. When a component definition leaves it out, that property is bound to the top-level state key with the same name as the component ID.

JSON
{ "components": { "cpu": { "type": "dial", "label": "CPU" } } }

This dial has the ID cpu and no value, so it shows the state key cpu.

ComponentPrimary property
texttext
imagesrc
dial, bar, toggle, slidervalue
listitems
buttonnone

What bindings cannot do

A binding does not convert between types, with one exception: a number or boolean bound to a text property is shown as text. A value of the wrong type for a property is treated as missing.

Bindings have no arithmetic, no functions, no conditionals beyond the conditions above, and no way to combine conditions. This is deliberate: a template is data, and a viewer never evaluates anything an author wrote as code. An application that needs a derived value, such as canDrink, computes it and publishes it as state.

Components

components maps a component ID to a definition. The set of component types is closed: a template cannot define new ones.

JSON
{ "components": {
    "hpText": {
      "type": "text",
      "text": { "format": "{player.hp} / {player.hpMax}" },
      "style": { "color": "#FFFFFF", "size": 4 }
    }
} }

There are eight types: text, image, dial, bar, list, button, toggle and slider.

Fields every component has

FieldTypeBindableDefaultDescription
typestringNononeRequired. One of the eight types.
visiblebooleanYes (condition)trueWhen false the component is not drawn but keeps its space in the layout.
styleobjectNo{}Appearance. Every component takes the box style, plus the style parts listed for its type.
variantsarrayNo[]Style applied while a condition holds. See variants.

text

A run of text.

PropertyTypeBindableDefaultDescription
text (primary)stringYes""The text shown.

Style: the text style keys sit at the top level of style, beside the box style. Common fields: visible, style, variants.

JSON
{ "type": "text",
  "text": { "format": "Level {hero.level} {hero.class}" },
  "style": { "size": 3.6, "color": "#B8A27C", "align": "start" } }

image

A picture: a package asset, or a runtime asset the application supplies.

PropertyTypeBindableDefaultDescription
src (primary)image referenceYesnoneA package asset path, or a runtime asset reference.
Style pathTypeDefaultMeaning
fitstringcontaincontain, cover or fill.

Common fields: visible, style, variants.

A viewer loads images only from the server it is connected to. An src that is neither a package asset path nor a runtime asset reference is treated as missing.

JSON
{ "type": "image",
  "src": { "bind": "media.poster", "default": "assets/poster-idle.svg" },
  "style": { "fit": "cover" } }

dial

A circular gauge.

PropertyTypeBindableDefaultDescription
value (primary)numberYesminClamped to min…max.
minnumberYes0
maxnumberYes100
labelstringYesnoneA caption under the dial.
Style pathTypeDefault
track.colorcolour#333333
fill.colorcolour#00CC66
needle.colorcolour#FFFFFF
showValuebooleantrue
valueTexttext style
labelTexttext style

Common fields: visible, style, variants.

JSON
{ "type": "dial",
  "value": { "bind": "cpu.load" },
  "label": "Processor",
  "style": {
    "track": { "color": "#14303A" },
    "fill": { "color": "#FFB23E" },
    "valueText": { "size": 11, "color": "#FFD9A0" }
  } }

bar

A bar that fills in one direction. It covers progress bars, meters, and image-based fills such as a potion globe.

PropertyTypeBindableDefaultDescription
value (primary)numberYesminClamped to min…max.
minnumberYes0
maxnumberYes100
labelstringYesnone
Style pathTypeDefaultMeaning
directionstringrightright, left, up or down: the way the fill grows.
track.colorcolour#222222
track.imageasset pathnoneDrawn over the whole bar.
fill.colorcolour#00CC66
fill.imageasset pathnoneDrawn over the whole bar, then clipped to the filled portion.
showValuebooleanfalse
valueTexttext style
labelTexttext style

Common fields: visible, style, variants.

track.image and fill.image are scaled to fit inside the bar's box with their aspect ratio kept, and centred. The filled portion is measured against the drawn artwork, not the surrounding box, so the level is right whatever shape the box is.

With track.image set to an empty globe and fill.image to a full one, "direction": "up" gives a globe that drains:

JSON
{ "type": "bar",
  "value": { "bind": "hero.hp" },
  "max": { "bind": "hero.hpMax" },
  "style": {
    "direction": "up",
    "track": { "image": "assets/globe-empty.svg" },
    "fill": { "image": "assets/globe-health.svg" }
  } }

list

A scrolling list of rows, one for each element of an array in state. Each row has four fixed slots.

PropertyTypeBindableDefaultDescription
items (primary)array of objectYes[]The rows.
itemobject{}What each row shows; see the slots below.
actionnameNononeIf present, rows can be tapped.
actionValueitem bindingthe row's indexThe value sent when a row is tapped. Needs action.
enabledbooleanYes (condition)true
feedbackobjectNo{}See feedback. Needs action.

item slots, all optional, each a literal or a binding:

SlotTypePosition
imageimage referenceLeading edge.
titlestringFirst line.
subtitlestringSecond line.
trailingstringTrailing edge.

Inside item and actionValue, a path beginning $item. is resolved against the row's own element instead of the state document, and $index is the row's zero-based position.

Style pathTypeDefault
rowHeightlength12
divider.colorcolournone
titleText, subtitleText, trailingTexttext style

Common fields: visible, style, variants.

Viewers render at least the first 100 elements.

JSON
{ "type": "list",
  "items": { "bind": "party" },
  "item": {
    "image": { "bind": "$item.portrait" },
    "title": { "bind": "$item.name" },
    "subtitle": { "format": "Level {$item.level} {$item.class}" },
    "trailing": { "format": "{$item.hp:.0} HP" }
  },
  "action": "selectMember",
  "actionValue": { "bind": "$item.id" } }

button

Something to press. A button has no primary property.

PropertyTypeBindableDefaultDescription
labelstringYesnone
iconimage referenceYesnoneDrawn above the label, or alone.
actionValueanyNononeA literal sent as the action's value. Lets several buttons share one action.
actionnameNothe component IDThe action name sent to the application.
enabledbooleanYes (condition)trueWhen false the button is drawn at reduced opacity and sends nothing.
feedbackobjectNo{}See feedback.
Style pathType
labelTexttext style

Shape and colour come from the box style. Common fields: visible, style, variants.

JSON
{ "type": "button",
  "icon": "assets/potion-health.svg",
  "label": { "format": "Health  {potions.health}" },
  "action": "drinkPotion",
  "actionValue": "health",
  "enabled": { "bind": "potions.health", "gt": 0 },
  "feedback": { "animation": "scaleDown", "sound": "assets/sounds/glug.wav", "haptic": "medium" } }

toggle

An on/off switch. Flipping it sends its action with the state the user asked for; see toggles and sliders.

PropertyTypeBindableDefaultDescription
value (primary)booleanYes (condition)false
labelstringYesnone
actionnameNothe component ID
enabledbooleanYes (condition)true
feedbackobjectNo{}
Style pathTypeDefault
track.colorcolour#555555
track.onColorcolour#00CC66
thumb.colorcolour#FFFFFF
labelTexttext style

Common fields: visible, style, variants.

JSON
{ "type": "toggle",
  "value": { "bind": "pet.away" },
  "label": { "format": "{pet.name} to town" },
  "action": "setPetAway" }

slider

A draggable value. Moving it sends its action with the value the user asked for; see toggles and sliders.

PropertyTypeBindableDefaultDescription
value (primary)numberYesmin
minnumberYes0
maxnumberYes100
stepnumberNo1The snap increment. Greater than 0.
labelstringYesnone
actionnameNothe component ID
enabledbooleanYes (condition)true
feedbackobjectNo{}
Style pathTypeDefaultMeaning
directionstringrightAs for bar.
track.colorcolour#333333
fill.colorcolour#00CC66
thumb.colorcolour#FFFFFF
labelTexttext style

Common fields: visible, style, variants.

JSON
{ "type": "slider",
  "value": { "bind": "fans.target" },
  "min": 20, "max": 100, "step": 5,
  "label": { "format": "Fan speed   {fans.target:.0}%" },
  "action": "setFanTarget" }

Style

style holds appearance only. Nothing in style is bindable; appearance that depends on state is expressed with variants.

A viewer ignores style keys it does not recognise, so a template written for a later minor version still draws. A manifest must not use keys that are not defined for the specVersion it declares.

Box style

Every component, every container and every layout accepts:

PathTypeDefaultMeaning
background.colorcolourtransparentFill behind the content.
background.imageasset pathnoneAn image behind the content, above the colour.
background.fitstringcovercontain, cover or fill.
border.colorcolournoneThe border colour. No border is drawn without it.
border.widthlength0.3Border thickness.
border.radiuslength0Corner radius. "round" makes a pill or a circle.
paddinglength0Space between the box edge and the content.
opacitynumber10 to 1, applied to the whole box.

Text style

Wherever this reference says a style part is a text style (labelText, valueText, titleText and so on), it accepts:

PathTypeDefaultMeaning
colorcolour#FFFFFFText colour.
sizelength4Font size.
fontnamethe viewer's defaultA name from the manifest's fonts.
weightstringnormalnormal or bold.
alignstringcenterstart, center or end.

Variants

A variant applies extra style while a condition holds. Each one has a when and a style, both required.

JSON
{ "variants": [
    { "when": { "bind": "cpu.hot" },
      "style": { "fill": { "color": "#FF5D4D" }, "valueText": { "color": "#FFC2BB" } } }
] }

Viewers evaluate variants in array order and merge the style of each one whose when is true over the component's base style, so later variants win. A variant's style takes the same keys as the component's own. A component should not declare more than 8 variants.

Interaction

Three component types are interactive: button, toggle and slider. A list is when it has an action.

FieldTypeBindableDefaultDescription
actionnameNothe component IDThe action name sent to the application. A list has no default: without action its rows cannot be tapped.
enabledbooleanYes (condition)trueWhen false the component is drawn at reduced opacity and emits nothing.
feedbackobjectNo{}Local feedback, below.

Several components may share an action name, as long as they produce the same type of value. The value each type sends is listed under the action message.

Feedback

feedback is played by the viewer the moment the user touches the component, before and regardless of anything the application does.

FieldTypeValues
animationstringscaleDown, scaleUp, pulse, glow, fade, shake, bounce
soundasset pathAn audio asset: MP3, OGG or WAV.
hapticstringlight, medium, heavy

A viewer treats an unknown animation or haptic value as absent, and ignores sound or haptics on hardware without them.

Toggles and sliders are requests

A toggle or slider displays the value bound from state: the application's truth. When the user changes it, the viewer emits the action and may display the requested value for up to 1000 ms. After that, or as soon as a state change for the bound path arrives, it displays the state value. An application accepts a change by publishing the new value, and rejects it by doing nothing.

While a slider is being dragged a viewer emits at most 10 actions per second, and always emits the final value on release.

Scenarios and layouts

A scenario is a named arrangement of components. An application might have exploring and bossFight. Each scenario provides a landscape layout, a portrait layout, or both; the viewer picks by its own orientation and falls back to the other when only one exists.

JSON
{ "scenarios": {
    "exploring": { "landscape": { "root": "hp" }, "portrait": { "root": "hp" } },
    "bossFight": { "landscape": { "root": "bossHp" } }
} }

Choosing the scenario

The active scenario is the value of the manifest's scenario binding when that value names a scenario, and defaultScenario otherwise. With "scenario": { "bind": "mode" } in the manifest, an application switches scenario by changing state:

JSON
{ "type": "state", "patch": { "mode": "bossFight" } }

Layouts

A layout is either a single node, or a grid of named areas.

FieldTypeDescription
rootnodeThe whole layout is this one node. Cannot be combined with grid.
gridobjectA grid definition, below. Needs areas.
areasobjectArea name to node. One entry for every named area in the grid.
styleobjectBox style for the layout as a whole.

Grids

FieldTypeDescription
columnsarray of numberThe relative widths of the columns.
rowsarray of numberThe relative heights of the rows.
maparray of array of stringOne area name per cell: rows.length arrays of columns.length names.
gaplengthSpace between cells. Default 0.

Cells that share an area name must form a rectangle, and are merged into one region. The name "." marks an empty cell. Grids should not exceed 6 × 6.

JSON
{ "grid": {
    "columns": [1, 2, 1],
    "rows": [1, 4],
    "map": [
      ["title", "title",  "title"],
      ["hp",    "center", "mana"]
    ]
  },
  "areas": { "title": "name", "hp": "hp", "center": "actions", "mana": "mana" }
}

Nodes and containers

A node is one of three things:

  • A component ID, as a string: "hp".
  • A component placement, which adds a weight: { "component": "hp", "weight": 2 }.
  • A container, which arranges other nodes:
FieldTypeDefaultDescription
typestringnonerow, column or stack. Required.
childrenarray of nodenoneRequired. At least one.
gaplength0Space between children, for row and column.
weightnumber1Its share of its parent's space.
styleobject{}Box style.

row and column divide their space among their children in proportion to each child's weight. stack gives every child the whole space and draws them in order, first child at the back; it is how a value is laid over artwork. Containers nest. The same component may appear in more than one node.

JSON
{ "type": "stack",
  "children": [
    "hp",
    "hpGlass",
    { "type": "column", "children": [
        { "component": "blank", "weight": 9 },
        { "component": "hpValue", "weight": 5 },
        { "component": "blank", "weight": 8 }
    ] }
  ] }

Validation

The server validates every manifest when it is registered, and rejects one that:

  • does not conform to the JSON Schema for its specVersion;
  • names a defaultScenario, a component in a layout, an area, or a font that does not exist;
  • has a grid whose map does not match its columns and rows, or whose areas are not rectangles;
  • refers to an asset path that is not in the package;
  • declares the same action name on components that produce different value types.

The JSON Schema cannot express the last four, so a manifest that passes in your editor can still be refused. The refusal is an error with the code invalidPackage and a message that says where.

Viewers are lenient where authors are strict. A viewer draws a component of unknown type as an empty region and renders the rest; treats a binding with an operator it does not recognise as a missing value; and refuses only a manifest whose major version it does not support.

Wire messages

Remora runs over WebSocket. Every message is one text frame holding one JSON object with a string field type. Clients request the subprotocol remora.v1.json, and the server refuses a connection that does not offer it. Receivers ignore messages whose type they do not recognise.

EndpointDefault addressWho connectsSecurity
Applicationws://127.0.0.1:7310/v1/appApplications on the same machineLoopback only.
Viewerwss://<host>:7311/v1/viewerViewers on the networkTLS, pinned to the server's key.

Binary frames carry file bytes and appear in exactly two places: a package upload after packageRequired, and a runtime asset upload after asset.

typeDirectionFields
helloClient to serverrole, versions, client
welcomeServer to clientversion, server, limits
errorEithercode, fatal, message, details
registerApplication to serverone of manifest, package, app; label
packageRequiredServer to applicationnone
readyServer to applicationinstance, package
stateApplication to serverpatch
assetApplication to serverpath, contentType
watchersServer to applicationcount
actionServer to applicationaction, value, device
pairViewer to servercode, device
authViewer to serverdevice, token
pairedServer to viewerdevice, token
appsServer to viewerapps
watchViewer to serverinstance
snapshotServer to viewerinstance, state
stateServer to viewerinstance, patch
actionViewer to serverinstance, action, value

Opening a connection

hello

The first message on any connection, from the client.

JSON
{ "type": "hello", "role": "viewer", "versions": ["1.0"],
  "client": { "name": "Remora for Android", "version": "0.4.0" } }
FieldTypeRequiredDescription
rolestringYesapp or viewer. Must match the endpoint.
versionsarray of stringYesThe protocol versions the client speaks, as major.minor.
clientobjectNoThe name and version of the client software, for diagnostics.

welcome

The server's answer to hello.

JSON
{ "type": "welcome", "version": "1.0",
  "server": { "id": "k3Jx0Qe9Tz", "name": "Andrew's PC" },
  "limits": { "messageBytes": 1048576, "packageBytes": 16777216,
              "stateBytes": 262144, "updatesPerSecond": 60 } }
FieldTypeDescription
versionstringThe highest protocol version both sides support.
serverobjectThe server's id and display name.
limitsobjectmessageBytes, packageBytes, stateBytes and updatesPerSecond. A sender must not exceed them.

If there is no common version, the server sends error with the code unsupportedVersion instead, listing its own versions in details.versions.

error

Sent by either side.

JSON
{ "type": "error", "code": "invalidPackage", "fatal": true,
  "message": "components.hp.type: \"gauge\" is not a component type in specVersion 1.0",
  "details": { "path": "components.hp.type" } }
FieldTypeRequiredDescription
codestringYesOne of the codes below. An unknown code is treated as internal.
fatalbooleanYesIf true, the sender closes the connection after this message.
messagestringYesHuman-readable. For logs and developers, not for matching.
detailsobjectNoSpecific to the code.
CodeFatalMeaning
unsupportedVersionYesNo common protocol version.
invalidMessageYesNot JSON, missing type, wrong role, or a message out of sequence.
unauthorizedYesA bad or revoked device token.
pairingFailedYesAn unknown, expired or exhausted pairing code.
invalidPackageYesThe package failed validation. message says where.
packageTooLargeYesAbove limits.packageBytes.
unknownAppYesregister named an application ID with no installed package.
stateTooLargeNoThe patch was not applied.
assetRejectedNoThe runtime asset was not stored.
unknownInstanceNowatch or action named an instance that does not exist.
actionRejectedNoThe action failed the server's checks.
internalYesThe server cannot continue.

WebSocket close codes: 1000 normal; 1002 protocol error; 1008 policy (unauthorized, pairingFailed); 1009 too large; 1011 otherwise.

Application messages

register

Names the template, in exactly one of three ways. A connection registers once; to change template, an application reconnects.

JSON
{ "type": "register", "package": "5d41402abc4b2a76b9719d911017c592ae8f1a1b3c2d4e5f60718293a4b5c6d7", "label": "Vasha" }
FieldTypeMeaning
manifestobjectAn inline package: the manifest itself. For templates with no asset files.
packagestringThe hash of an archive package.
appstringAn application ID. The server uses a package its operator has installed for it.
labelstringOptional. Tells instances apart in viewer listings when the same application runs more than once: a character name, a window title.

packageRequired

JSON
{ "type": "packageRequired" }

Sent when register gave a package hash the server does not hold. The application's next frame must be a single binary frame containing the .remora archive. The server verifies that its SHA-256 equals the hash.

ready

The registration succeeded.

JSON
{ "type": "ready", "instance": "9fQ2mB7cX1vLr8Td", "package": "5d41402abc4b2a76b9719d911017c592ae8f1a1b3c2d4e5f60718293a4b5c6d7" }
FieldTypeDescription
instancestringIdentifies this registration until the connection closes.
packagestringThe hash of the package actually in use. It differs from the one offered if the operator has installed a replacement template for the application ID.

On failure the server sends error with the code invalidPackage, packageTooLarge or unknownApp, and closes.

state

From the application, a change to its state document:

JSON
{ "type": "state", "patch": { "player": { "hp": 412, "potions": 3 }, "mode": "bossFight" } }

patch is a JSON Merge Patch (RFC 7386): objects merge key by key, any other value replaces what was there, and null removes a key. Arrays are replaced whole.

An application may send only what changed, or its entire state every time. It should not exceed limits.updatesPerSecond; the server merges excess patches rather than rejecting them. A patch that would grow the document beyond limits.stateBytes is rejected with a non-fatal stateTooLarge.

To a viewer, the same message also names the instance:

JSON
{ "type": "state", "instance": "9fQ2mB7cX1vLr8Td", "patch": { "player": { "hp": 371 } } }
FieldTypeDescription
patchobjectThe merge patch.
instancestringServer to viewer only: the instance the patch belongs to.

The server sends a viewer only keys whose value actually changed, nothing when a patch changed nothing, and may merge consecutive patches into one.

asset

Announces a runtime asset: an image that is not in the package.

JSON
{ "type": "asset", "path": "cast/reeves.jpg", "contentType": "image/jpeg" }
FieldTypeDescription
pathasset pathWhere the file will live. It is then referred to from state as runtime:cast/reeves.jpg.
contentTypestringimage/png, image/jpeg, image/svg+xml or image/webp.

The application's next frame must be a single binary frame containing the file. Uploading to an existing path replaces it. Runtime assets belong to the instance and are deleted when its connection closes.

watchers

JSON
{ "type": "watchers", "count": 1 }
FieldTypeDescription
countintegerHow many viewers are watching this application.

Sent after ready, and whenever the number changes. While count is 0 an application may stop sending state; when it rises from 0 it should send its current state promptly.

action

Something the user did. From the viewer:

JSON
{ "type": "action", "instance": "9fQ2mB7cX1vLr8Td", "action": "setVolume", "value": 65 }

And as the server passes it to the application:

JSON
{ "type": "action", "action": "setVolume", "value": 65, "device": "d_7Yc2" }
FieldTypeDescription
actionnameThe action name the template declares.
valueanyDepends on the component; see below. Absent when there is none.
instancestringViewer to server only: the instance acted on.
devicestringServer to application only: the paired device the action came from.
Source componentvalue
buttonThe component's actionValue, or absent.
toggleThe requested new state: a boolean.
sliderThe requested new value: a number within min…max.
list rowThe row's actionValue, or its index.

The server forwards an action only when the viewer is watching that instance, the instance's manifest declares an interactive component with that action name, and value has the type that component produces. Otherwise it drops it, and may send the viewer a non-fatal actionRejected.

Actions are delivered at most once and are not acknowledged. The application shows the outcome by changing state.

Viewer messages

pair

Sent once, after welcome, by a viewer that has no credentials for this server yet.

JSON
{ "type": "pair", "code": "7K2M-9QXA", "device": { "name": "Andrew's Pixel", "kind": "phone" } }
FieldTypeDescription
codestringThe pairing code the server is showing.
deviceobjectname is required and is what the operator sees in the list of paired devices. kind is an optional hint: phone, tablet, watch, desktop or other.

paired

The answer to a successful pair: the credentials to use from then on.

JSON
{ "type": "paired", "device": "d_7Yc2", "token": "<token>" }
FieldTypeDescription
devicestringThe Device ID the server assigned.
tokenstringA secret with at least 256 bits of randomness. The server stores only a hash of it.

auth

Sent after welcome by a viewer that has paired before.

JSON
{ "type": "auth", "device": "d_7Yc2", "token": "<token>" }
FieldTypeDescription
devicestringThe Device ID from paired.
tokenstringThe token from paired.

A failure of pair or auth is an error with the code pairingFailed or unauthorized.

apps

The list of registered instances, sent after authentication and again in full whenever it changes.

JSON
{ "type": "apps", "apps": [
    { "instance": "9fQ2mB7cX1vLr8Td",
      "app": "com.runicgames.torchlight2",
      "name": "Torchlight II",
      "label": "Vasha",
      "description": "Health, mana, potions and pet.",
      "version": "0.3.1",
      "package": "5d41402abc4b2a76b9719d911017c592ae8f1a1b3c2d4e5f60718293a4b5c6d7",
      "icon": "assets/icon.png" }
] }
FieldTypeDescription
appsarrayOne object per instance, with instance, app, name, version and package always, and label, description and icon when there are any.

The viewer fetches the manifest and assets for a package over HTTP, once, and caches them by hash.

watch

Start watching an instance, or stop.

JSON
{ "type": "watch", "instance": "9fQ2mB7cX1vLr8Td" }
JSON
{ "type": "watch", "instance": null }
FieldTypeDescription
instancestring or nullThe instance to watch, or null to stop.

A viewer watches at most one instance per connection; a new watch replaces the previous one. The server replies with snapshot, or with a non-fatal unknownInstance.

snapshot

The whole state document, sent when a viewer starts watching. It always arrives before any state for that instance.

JSON
{ "type": "snapshot", "instance": "9fQ2mB7cX1vLr8Td",
  "state": { "player": { "name": "Vasha", "hp": 412, "hpMax": 500, "potions": 3 },
             "pet": { "away": false }, "party": [], "mode": "bossFight" } }
FieldTypeDescription
instancestringThe instance being watched.
stateobjectIts state document.

Files over HTTP

The viewer listener also serves files, on the same host, port and certificate as the viewer endpoint.

ResourceURL
A package's manifest/v1/packages/{hash}/manifest.json
A package asset/v1/packages/{hash}/{assetPath}
A runtime asset/v1/instances/{instance}/assets/{path}

These URLs need no credentials. A package hash and an instance ID are disclosed only to authenticated viewers, and an instance ID is unguessable, so possession of the URL is the authorisation. There is no listing of packages or instances.

Keepalive

The server sends a WebSocket ping at least every 30 seconds and closes a connection that has not answered within 60 seconds. Clients must answer pings. Viewers should reconnect with exponential backoff; applications may reconnect immediately.