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:
manifest.v1.schema.jsondescribes a manifest.messages.v1.schema.jsondescribes every wire message.
Put this as the first field of a manifest and an editor that understands JSON Schema will complete and check it as you type:
{ "$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:
{
"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
| Field | Type | Required | Description |
|---|---|---|---|
specVersion | string | Yes | The version of the specification the manifest follows: "1.0". |
id | string | Yes | The application ID. Reverse-DNS style, lowercase, stable across versions: com.example.cpu. See names and identifiers. |
name | string | Yes | A short name, shown in viewer listings. |
version | string | Yes | The author's own version of the template. Informational. |
description | string | No | Longer text, shown in viewer detail views. |
icon | asset path | No | A square PNG or JPEG, at least 256 px. |
fonts | object | No | Font name to asset path. The names are then usable as font in a text style. |
state | object | No | The initial state document. Its contents are free-form. |
components | object | Yes | Component ID to component definition. See components. |
scenarios | object | Yes | Scenario name to scenario. At least one. See scenarios and layouts. |
defaultScenario | name | Yes | The scenario shown when none is selected. |
scenario | binding | No | A binding whose value names the active scenario. See choosing the scenario. |
$schema | string | No | A 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
| Identifier | Chosen by | Form |
|---|---|---|
| Name: a component ID, scenario name, action name, area name or font name | The template author | ^[A-Za-z][A-Za-z0-9_-]{0,63}$ |
| Application ID | The application author | Reverse-DNS style, lowercase: ^[a-z0-9]+([.-][a-z0-9]+)+$, at most 128 characters. |
| State path | The application author | Segments joined by ., each matching ^[A-Za-z0-9_-]+$. A segment made only of digits indexes an array: player.hp, team.0.name. |
| Package hash | Derived | 64 lowercase hexadecimal characters: the SHA-256 of the archive. |
| Instance ID, Device ID, Server ID | The server | Opaque 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.
{ "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
{ "bind": "player.hp" }The property takes the value found at the state path. An optional default is used while the path is missing:
{ "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:
{ "bind": "player.hp", "format": "{} HP" }{ "format": "{player.hp} / {player.hpMax}" }A placeholder may carry a number specifier after a colon:
| Placeholder | Value | Result |
|---|---|---|
{} | 1234.5 | 1234.5 |
{:.0} | 1234.5 | 1235 |
{:.2} | 1234.5 | 1234.50 |
{:,} | 1234.5 | 1,234.5 |
{:,.0} | 1234.5 | 1,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:
{ "bind": "pet.away" }One comparison operator may be added. The operand is a literal:
{ "bind": "player.potions", "gt": 0 }| Operator | True when the bound value is |
|---|---|
eq, ne | equal, or not equal, to the operand: the same JSON type and value. The operand is a string, number or boolean. |
gt, gte, lt, lte | a number that compares accordingly with a numeric operand. |
"not": true inverts the result:
{ "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.
{ "components": { "cpu": { "type": "dial", "label": "CPU" } } }This dial has the ID cpu and no value, so it shows the state key cpu.
| Component | Primary property |
|---|---|
text | text |
image | src |
dial, bar, toggle, slider | value |
list | items |
button | none |
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.
{ "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
| Field | Type | Bindable | Default | Description |
|---|---|---|---|---|
type | string | No | none | Required. One of the eight types. |
visible | boolean | Yes (condition) | true | When false the component is not drawn but keeps its space in the layout. |
style | object | No | {} | Appearance. Every component takes the box style, plus the style parts listed for its type. |
variants | array | No | [] | Style applied while a condition holds. See variants. |
text
A run of text.
| Property | Type | Bindable | Default | Description |
|---|---|---|---|---|
text (primary) | string | Yes | "" | The text shown. |
Style: the text style keys sit at the top level of style, beside the box style. Common fields: visible, style, variants.
{ "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.
| Property | Type | Bindable | Default | Description |
|---|---|---|---|---|
src (primary) | image reference | Yes | none | A package asset path, or a runtime asset reference. |
| Style path | Type | Default | Meaning |
|---|---|---|---|
fit | string | contain | contain, 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.
{ "type": "image",
"src": { "bind": "media.poster", "default": "assets/poster-idle.svg" },
"style": { "fit": "cover" } }dial
A circular gauge.
| Property | Type | Bindable | Default | Description |
|---|---|---|---|---|
value (primary) | number | Yes | min | Clamped to min…max. |
min | number | Yes | 0 | |
max | number | Yes | 100 | |
label | string | Yes | none | A caption under the dial. |
| Style path | Type | Default |
|---|---|---|
track.color | colour | #333333 |
fill.color | colour | #00CC66 |
needle.color | colour | #FFFFFF |
showValue | boolean | true |
valueText | text style | |
labelText | text style |
Common fields: visible, style, variants.
{ "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.
| Property | Type | Bindable | Default | Description |
|---|---|---|---|---|
value (primary) | number | Yes | min | Clamped to min…max. |
min | number | Yes | 0 | |
max | number | Yes | 100 | |
label | string | Yes | none |
| Style path | Type | Default | Meaning |
|---|---|---|---|
direction | string | right | right, left, up or down: the way the fill grows. |
track.color | colour | #222222 | |
track.image | asset path | none | Drawn over the whole bar. |
fill.color | colour | #00CC66 | |
fill.image | asset path | none | Drawn over the whole bar, then clipped to the filled portion. |
showValue | boolean | false | |
valueText | text style | ||
labelText | text 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:
{ "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.
| Property | Type | Bindable | Default | Description |
|---|---|---|---|---|
items (primary) | array of object | Yes | [] | The rows. |
item | object | {} | What each row shows; see the slots below. | |
action | name | No | none | If present, rows can be tapped. |
actionValue | item binding | the row's index | The value sent when a row is tapped. Needs action. | |
enabled | boolean | Yes (condition) | true | |
feedback | object | No | {} | See feedback. Needs action. |
item slots, all optional, each a literal or a binding:
| Slot | Type | Position |
|---|---|---|
image | image reference | Leading edge. |
title | string | First line. |
subtitle | string | Second line. |
trailing | string | Trailing 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 path | Type | Default |
|---|---|---|
rowHeight | length | 12 |
divider.color | colour | none |
titleText, subtitleText, trailingText | text style |
Common fields: visible, style, variants.
Viewers render at least the first 100 elements.
{ "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.
| Property | Type | Bindable | Default | Description |
|---|---|---|---|---|
label | string | Yes | none | |
icon | image reference | Yes | none | Drawn above the label, or alone. |
actionValue | any | No | none | A literal sent as the action's value. Lets several buttons share one action. |
action | name | No | the component ID | The action name sent to the application. |
enabled | boolean | Yes (condition) | true | When false the button is drawn at reduced opacity and sends nothing. |
feedback | object | No | {} | See feedback. |
| Style path | Type |
|---|---|
labelText | text style |
Shape and colour come from the box style. Common fields: visible, style, variants.
{ "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.
| Property | Type | Bindable | Default | Description |
|---|---|---|---|---|
value (primary) | boolean | Yes (condition) | false | |
label | string | Yes | none | |
action | name | No | the component ID | |
enabled | boolean | Yes (condition) | true | |
feedback | object | No | {} |
| Style path | Type | Default |
|---|---|---|
track.color | colour | #555555 |
track.onColor | colour | #00CC66 |
thumb.color | colour | #FFFFFF |
labelText | text style |
Common fields: visible, style, variants.
{ "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.
| Property | Type | Bindable | Default | Description |
|---|---|---|---|---|
value (primary) | number | Yes | min | |
min | number | Yes | 0 | |
max | number | Yes | 100 | |
step | number | No | 1 | The snap increment. Greater than 0. |
label | string | Yes | none | |
action | name | No | the component ID | |
enabled | boolean | Yes (condition) | true | |
feedback | object | No | {} |
| Style path | Type | Default | Meaning |
|---|---|---|---|
direction | string | right | As for bar. |
track.color | colour | #333333 | |
fill.color | colour | #00CC66 | |
thumb.color | colour | #FFFFFF | |
labelText | text style |
Common fields: visible, style, variants.
{ "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:
| Path | Type | Default | Meaning |
|---|---|---|---|
background.color | colour | transparent | Fill behind the content. |
background.image | asset path | none | An image behind the content, above the colour. |
background.fit | string | cover | contain, cover or fill. |
border.color | colour | none | The border colour. No border is drawn without it. |
border.width | length | 0.3 | Border thickness. |
border.radius | length | 0 | Corner radius. "round" makes a pill or a circle. |
padding | length | 0 | Space between the box edge and the content. |
opacity | number | 1 | 0 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:
| Path | Type | Default | Meaning |
|---|---|---|---|
color | colour | #FFFFFF | Text colour. |
size | length | 4 | Font size. |
font | name | the viewer's default | A name from the manifest's fonts. |
weight | string | normal | normal or bold. |
align | string | center | start, center or end. |
Variants
A variant applies extra style while a condition holds. Each one has a when and a style, both required.
{ "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.
| Field | Type | Bindable | Default | Description |
|---|---|---|---|---|
action | name | No | the component ID | The action name sent to the application. A list has no default: without action its rows cannot be tapped. |
enabled | boolean | Yes (condition) | true | When false the component is drawn at reduced opacity and emits nothing. |
feedback | object | No | {} | 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.
| Field | Type | Values |
|---|---|---|
animation | string | scaleDown, scaleUp, pulse, glow, fade, shake, bounce |
sound | asset path | An audio asset: MP3, OGG or WAV. |
haptic | string | light, 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.
{ "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:
{ "type": "state", "patch": { "mode": "bossFight" } }Layouts
A layout is either a single node, or a grid of named areas.
| Field | Type | Description |
|---|---|---|
root | node | The whole layout is this one node. Cannot be combined with grid. |
grid | object | A grid definition, below. Needs areas. |
areas | object | Area name to node. One entry for every named area in the grid. |
style | object | Box style for the layout as a whole. |
Grids
| Field | Type | Description |
|---|---|---|
columns | array of number | The relative widths of the columns. |
rows | array of number | The relative heights of the rows. |
map | array of array of string | One area name per cell: rows.length arrays of columns.length names. |
gap | length | Space 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.
{ "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:
| Field | Type | Default | Description |
|---|---|---|---|
type | string | none | row, column or stack. Required. |
children | array of node | none | Required. At least one. |
gap | length | 0 | Space between children, for row and column. |
weight | number | 1 | Its share of its parent's space. |
style | object | {} | 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.
{ "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
mapdoes not match itscolumnsandrows, 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.
| Endpoint | Default address | Who connects | Security |
|---|---|---|---|
| Application | ws://127.0.0.1:7310/v1/app | Applications on the same machine | Loopback only. |
| Viewer | wss://<host>:7311/v1/viewer | Viewers on the network | TLS, 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.
type | Direction | Fields |
|---|---|---|
hello | Client to server | role, versions, client |
welcome | Server to client | version, server, limits |
error | Either | code, fatal, message, details |
register | Application to server | one of manifest, package, app; label |
packageRequired | Server to application | none |
ready | Server to application | instance, package |
state | Application to server | patch |
asset | Application to server | path, contentType |
watchers | Server to application | count |
action | Server to application | action, value, device |
pair | Viewer to server | code, device |
auth | Viewer to server | device, token |
paired | Server to viewer | device, token |
apps | Server to viewer | apps |
watch | Viewer to server | instance |
snapshot | Server to viewer | instance, state |
state | Server to viewer | instance, patch |
action | Viewer to server | instance, action, value |
Opening a connection
hello
The first message on any connection, from the client.
{ "type": "hello", "role": "viewer", "versions": ["1.0"],
"client": { "name": "Remora for Android", "version": "0.4.0" } }| Field | Type | Required | Description |
|---|---|---|---|
role | string | Yes | app or viewer. Must match the endpoint. |
versions | array of string | Yes | The protocol versions the client speaks, as major.minor. |
client | object | No | The name and version of the client software, for diagnostics. |
welcome
The server's answer to hello.
{ "type": "welcome", "version": "1.0",
"server": { "id": "k3Jx0Qe9Tz", "name": "Andrew's PC" },
"limits": { "messageBytes": 1048576, "packageBytes": 16777216,
"stateBytes": 262144, "updatesPerSecond": 60 } }| Field | Type | Description |
|---|---|---|
version | string | The highest protocol version both sides support. |
server | object | The server's id and display name. |
limits | object | messageBytes, 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.
{ "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" } }| Field | Type | Required | Description |
|---|---|---|---|
code | string | Yes | One of the codes below. An unknown code is treated as internal. |
fatal | boolean | Yes | If true, the sender closes the connection after this message. |
message | string | Yes | Human-readable. For logs and developers, not for matching. |
details | object | No | Specific to the code. |
| Code | Fatal | Meaning |
|---|---|---|
unsupportedVersion | Yes | No common protocol version. |
invalidMessage | Yes | Not JSON, missing type, wrong role, or a message out of sequence. |
unauthorized | Yes | A bad or revoked device token. |
pairingFailed | Yes | An unknown, expired or exhausted pairing code. |
invalidPackage | Yes | The package failed validation. message says where. |
packageTooLarge | Yes | Above limits.packageBytes. |
unknownApp | Yes | register named an application ID with no installed package. |
stateTooLarge | No | The patch was not applied. |
assetRejected | No | The runtime asset was not stored. |
unknownInstance | No | watch or action named an instance that does not exist. |
actionRejected | No | The action failed the server's checks. |
internal | Yes | The 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.
{ "type": "register", "package": "5d41402abc4b2a76b9719d911017c592ae8f1a1b3c2d4e5f60718293a4b5c6d7", "label": "Vasha" }| Field | Type | Meaning |
|---|---|---|
manifest | object | An inline package: the manifest itself. For templates with no asset files. |
package | string | The hash of an archive package. |
app | string | An application ID. The server uses a package its operator has installed for it. |
label | string | Optional. Tells instances apart in viewer listings when the same application runs more than once: a character name, a window title. |
packageRequired
{ "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.
{ "type": "ready", "instance": "9fQ2mB7cX1vLr8Td", "package": "5d41402abc4b2a76b9719d911017c592ae8f1a1b3c2d4e5f60718293a4b5c6d7" }| Field | Type | Description |
|---|---|---|
instance | string | Identifies this registration until the connection closes. |
package | string | The 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:
{ "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:
{ "type": "state", "instance": "9fQ2mB7cX1vLr8Td", "patch": { "player": { "hp": 371 } } }| Field | Type | Description |
|---|---|---|
patch | object | The merge patch. |
instance | string | Server 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.
{ "type": "asset", "path": "cast/reeves.jpg", "contentType": "image/jpeg" }| Field | Type | Description |
|---|---|---|
path | asset path | Where the file will live. It is then referred to from state as runtime:cast/reeves.jpg. |
contentType | string | image/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
{ "type": "watchers", "count": 1 }| Field | Type | Description |
|---|---|---|
count | integer | How 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:
{ "type": "action", "instance": "9fQ2mB7cX1vLr8Td", "action": "setVolume", "value": 65 }And as the server passes it to the application:
{ "type": "action", "action": "setVolume", "value": 65, "device": "d_7Yc2" }| Field | Type | Description |
|---|---|---|
action | name | The action name the template declares. |
value | any | Depends on the component; see below. Absent when there is none. |
instance | string | Viewer to server only: the instance acted on. |
device | string | Server to application only: the paired device the action came from. |
| Source component | value |
|---|---|
button | The component's actionValue, or absent. |
toggle | The requested new state: a boolean. |
slider | The requested new value: a number within min…max. |
list row | The 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.
{ "type": "pair", "code": "7K2M-9QXA", "device": { "name": "Andrew's Pixel", "kind": "phone" } }| Field | Type | Description |
|---|---|---|
code | string | The pairing code the server is showing. |
device | object | name 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.
{ "type": "paired", "device": "d_7Yc2", "token": "<token>" }| Field | Type | Description |
|---|---|---|
device | string | The Device ID the server assigned. |
token | string | A 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.
{ "type": "auth", "device": "d_7Yc2", "token": "<token>" }| Field | Type | Description |
|---|---|---|
device | string | The Device ID from paired. |
token | string | The 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.
{ "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" }
] }| Field | Type | Description |
|---|---|---|
apps | array | One 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.
{ "type": "watch", "instance": "9fQ2mB7cX1vLr8Td" }{ "type": "watch", "instance": null }| Field | Type | Description |
|---|---|---|
instance | string or null | The 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.
{ "type": "snapshot", "instance": "9fQ2mB7cX1vLr8Td",
"state": { "player": { "name": "Vasha", "hp": 412, "hpMax": 500, "potions": 3 },
"pet": { "away": false }, "party": [], "mode": "bossFight" } }| Field | Type | Description |
|---|---|---|
instance | string | The instance being watched. |
state | object | Its state document. |
Files over HTTP
The viewer listener also serves files, on the same host, port and certificate as the viewer endpoint.
| Resource | URL |
|---|---|
| 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.