Skip to content

Bridge protocol v1 ​

Status: Milestone B, 2026-10-14. Version 1 is what @tavora/sdk 0.1.0 speaks. It may still change before Tavora's first public release; after that, breaking changes need a new major version (see Versioning).

Module authors normally use @tavora/sdk and never see these messages. This page is for people writing another SDK, reviewing the host, or debugging.

Connecting ​

  1. The module posts { type: "tavora:hello", v: 1 } to its own window (window.postMessage(..., location.origin)).
  2. The host's preload sees it and asks the main process for a channel. The main process creates a MessagePort pair and binds it to the panel that asked: its module id, game and panel instance. Only the panel's top frame can ask; iframes are ignored.
  3. The preload posts { type: "tavora:port", v: 1 } back to the window with the port attached.
  4. The module sends bridge.handshake on the port. Every other request before a successful handshake fails with BAD_REQUEST, and the host sends no events until then.

Asking again (a new hello) closes the old port and creates a new one. A reload or navigation inside the module also closes the port; the new page connects again.

Identity is never part of a message. Fields such as moduleId or gameId in params are rejected or ignored: the host only uses what it bound to the port.

Messages ​

All messages are plain JSON-compatible objects (structured clone of JSON values).

Request, module to host:

json
{ "v": 1, "id": 7, "method": "storage.set", "params": { "key": "count", "value": 3 } }

id is a positive integer chosen by the module, unique among its requests in flight. Messages without a usable id are dropped, since there is nobody to answer.

Response, host to module, exactly one per request:

json
{ "v": 1, "id": 7, "ok": true, "result": null }
{ "v": 1, "id": 7, "ok": false, "error": { "code": "QUOTA_EXCEEDED", "message": "settings are limited to 5242880 bytes per module and game" } }

Event, host to module, after the handshake:

json
{ "v": 1, "event": "workspace.resize", "data": { "width": 500, "height": 624 } }

Ports a module attaches to a message are closed by the host.

Methods ​

Methods without params must be sent with params left out.

MethodParamsResult
bridge.handshake{ protocol: 1, sdkVersion: string }{ protocol: 1, host: { name: "tavora-desktop", version }, module: { id, version }, capabilities: [] }
game.getCurrentnone{ id, displayName } of the game this panel belongs to
storage.get{ key }the stored JSON value, or null
storage.set{ key, value }null
storage.delete{ key }null
storage.keysnonesorted array of keys
documents.listnoneDocumentSummary[], newest first
documents.get{ id }{ id, title, revision, updatedAt, bytes, data } or null
documents.put{ id?, title, data, ifRevision? }DocumentSummary of the saved document
documents.delete{ id, ifRevision? }null (also when the document did not exist)
panel.getStatenonethis panel's state, or null
panel.setState{ value }null; value: null removes the state
workspace.getViewportnone{ width, height } of the panel in CSS pixels
ui.getThemenone{ mode, colors: { background, surface, text, textMuted, accent }, tokens, radius }: tokens holds every semantic colour by its shadcn-vue name (THEME_TOKENS), radius is in CSS pixels

Where data lives ​

KindScopeRemoved when
storagemodule + game, shared by all its panelsnever by closing panels; later by uninstalling (Milestone C)
documentsmodule + game, shared by all its panelsas above
panel stateone panel instancethe panel is closed

A module never sees another module's data, or its own data for another game.

Documents ​

A document is a named JSON value with a title and a revision. DocumentSummary is { id, title, revision, updatedAt, bytes }; updatedAt is an ISO 8601 timestamp and bytes the size of data as stored.

  • id: 1 to 64 characters from A-Z a-z 0-9 _ -, starting with a letter or digit. Leave it out of documents.put to get a new random id.
  • revision starts at 1 and goes up by one on every put.
  • ifRevision makes a put or delete conditional: it fails with CONFLICT (and error.revision set to the current revision) unless the stored revision equals it. ifRevision: 0 means "only if it does not exist yet".
  • After a successful put or delete, every other connected panel of the same module and game gets a documents.changed event. The panel that made the change does not.

Writes are atomic: a crash leaves either the old or the new document, never a mix.

Events ​

EventDataWhen
workspace.resize{ width, height }the panel's size changed
ui.themetheme, as ui.getThemethe host theme changed
documents.changed{ id, revision, deleted }another panel of this module changed a document
lifecycle.suspendnullthe panel is hidden (window minimized or hidden, its tab is in the background, or another group is maximized); pause timers and animations. Also sent right after the handshake if the panel connects while hidden
lifecycle.resumenullthe panel is visible again
lifecycle.disposenullthe panel is closing. Best effort only: it may never arrive, so never rely on it to save

Default look ​

Tavora inserts a stylesheet into every module page before it renders, as a user stylesheet. It sits above the browser's defaults and below everything the module writes, so any rule of the module's own wins. It makes a page that styles nothing look like part of Tavora: no page margin, the theme's background and text colour, Inter at 14 px, dark buttons, fields and scrollbars, a gold focus ring, muted small and dt text, and headings sized like the host's.

It also defines the theme's tokens as CSS custom properties, the same values ui.getTheme returns, so a module can use them in its own CSS without the SDK:

PropertyUse
--tavora-background, --tavora-foregroundPage background and text
--tavora-cardRaised surfaces and fields
--tavora-primary, --tavora-primary-hover, --tavora-primary-foregroundThe one main action, its hover, and text on it
--tavora-secondary, --tavora-accentDefault buttons; hover and selection surfaces
--tavora-muted-foregroundSecondary text
--tavora-border, --tavora-input, --tavora-ringDividers, field outlines, focus ring
--tavora-destructive, --tavora-success, --tavora-warning, --tavora-infoStates
--tavora-radiusCorner radius of controls
--tavora-font-sans, --tavora-font-monoText and code fonts

Errors ​

CodeMeaningExtra fields
BAD_REQUESTMalformed message, wrong v, no handshake yet, over the size limit or too many requests in flightsupported for a wrong v
UNKNOWN_METHODThe method is not part of the protocol
INVALID_PARAMSParams missing, of the wrong shape or not JSON, or params sent to a method that takes none
PERMISSION_DENIEDThe method exists but needs a capability this module was not grantedcapability
QUOTA_EXCEEDEDA storage, document or panel-state limit would be exceeded
CONFLICTifRevision did not matchrevision
INCOMPATIBLEThe handshake asked for a protocol version this host does not speaksupported
INTERNALThe host failed; details are only in the host's log

The SDK adds two codes of its own, never sent by the host: TIMEOUT (no answer within the request timeout, 10 s by default) and HOST_UNAVAILABLE (no port within the connect timeout, 5 s by default, for example when the page is opened outside Tavora).

message is for people. Code against code and the extra fields only.

Capabilities ​

Some methods need a capability the player grants. In v1 nothing can be granted, the handshake always reports capabilities: [], and these reserved methods always fail with PERMISSION_DENIED:

MethodCapability
network.fetchnetwork
files.open, files.savefiles
clipboard.writeText, clipboard.readTextclipboard
sharedData.publish, sharedData.read, sharedData.subscribesharedData

They are reserved now so a module can feature-detect them safely: the error shape will stay the same once grants exist. Their params and results are not specified yet.

Limits ​

LimitValue
Request size (serialized)8 MiB
Requests in flight per panel64
Storage key length256 characters
Storage per module and game5 MiB (keys + serialized values)
One document's data4 MiB
All documents per module and game50 MiB, at most 1000 documents
Document title200 characters
Panel state256 KiB
JSON nesting depth64

Values must be JSON: null, booleans, finite numbers, strings, arrays and plain objects. undefined, Date, Map, typed arrays, NaN and similar are rejected with INVALID_PARAMS, not converted.

Versioning ​

  • v on every message is the major protocol version. A host answers a message with another v with BAD_REQUEST and supported.
  • The handshake carries the version the SDK wants. A host that cannot serve it answers INCOMPATIBLE with the versions it supports, and the SDK's connect() rejects with that error.
  • Within a major version, the host may add methods, events, optional params, result fields and error codes. Modules must ignore event names and result fields they do not know.
  • Removing or changing anything existing needs a new major version. A host should keep serving the previous major version for as long as published modules use it.
  • The manifest's apiVersion names the major version the module was built for.

Code under the MIT License. The Tavora name and logo are not covered by it.