Skip to content

Light Table Remote Control API (LAN mode)

The Light Table runs an embedded HTTP/WebSocket server on TCP port 7878 while the Light Table screen is open and the remote server is started. Any client on the same network can control the table with the protocol below.

Versioning & compatibility

The API is versioned so it can evolve without breaking existing integrations:

  • GET /version reports { "apiVersion": 1, "app": "...", "device": "..." }.
  • Unversioned paths are permanent aliases for the current major version. /state and /command behave exactly like /v1/state and /v1/command.
  • Changes within a major version are additive — new command actions and new fields in state/messages. Older clients simply ignore what they don't know, so they keep working unchanged.
  • A breaking change ships as a new major (/v2/..., apiVersion: 2) while /v1 continues to work. Clients that care can branch on apiVersion from GET /version or the WebSocket hello message.

HTTP endpoints (interactive)

The plain HTTP endpoints are documented as an OpenAPI spec and rendered below. The live WebSocket/LTRC protocol — state push, scenes, effects — is documented in full further down this page.

WebSocket / LTRC protocol

Two transports speak the same JSON message protocol:

Transport Who uses it Framing
WebSocket Browsers (the built-in web remote) and any WS client RFC 6455 text frames
LTRC The native iOS remote (works over peer-to-peer/AWDL too) Client sends the 5 bytes LTRC\n, then newline-delimited JSON both directions

Access to each transport can be disabled independently on the table (Remote sheet → "How devices connect"). Disabled transports answer 403 Forbidden (HTTP/WS) or close immediately (LTRC).

Discovery (Bonjour)

When Nearby device access is enabled, the table advertises:

  • Service type: _lighttable._tcp
  • TXT record keys: name (device name), ip (WiFi IPv4), port (7878)

Advertising includes peer-to-peer (AWDL), so nearby iOS devices can connect without a shared network. Browsers can't use Bonjour — the web remote learns about other tables from the peers message (below) or manual IP entry.

HTTP endpoints

Method Path Description
GET / Serves the web remote UI (HTML)
GET /state Current state as JSON (see state message)
POST /command One-shot command; body is a command object (see below). When a PIN is required, include "pin": "1234" in the body; otherwise responds {"error":"authRequired"}
OPTIONS any CORS preflight (204)
curl http://192.168.1.50:7878/state
curl -X POST http://192.168.1.50:7878/command \
     -d '{"action":"brightness","value":0.5,"pin":"1234"}'

Responses send Access-Control-Allow-Origin: *; connections are one-shot (Connection: close). The QR code shown on the table encodes http://<ip>:7878/?pin=<pin> when a PIN is set; the web UI stores the pin from the query string and removes it from the address bar.

WebSocket / LTRC session

Connect with WebSocket to any path (the web UI uses /ws), or open a TCP connection and send LTRC\n. All messages are JSON objects with a type.

Authentication (only when the table requires a PIN)

On connect, the server sends:

{"type":"authRequired","name":"Studio iPad"}

Every message except auth is ignored (and re-answered with authRequired) until the client authenticates:

{"type":"auth","pin":"1234"}

Server replies {"type":"authOK"} followed by the welcome bundle, or {"type":"authFailed"} (connection stays open for another attempt). When no PIN is required, the welcome bundle arrives immediately on connect.

Welcome bundle (server → client)

{"type":"hello","name":"Studio iPad","apiVersion":1}
{"type":"state", ...}
{"type":"peers","peers":[{"name":"Desk iPhone","host":"192.168.1.51","port":7878}]}
{"type":"scenes","scenes":[...]}
{"type":"fxPrograms","programs":[...]}

state (server → client, pushed on every change)

{
  "type": "state",
  "brightness": 0.75,        // 0..1
  "whiteBalance": 0.3,       // 0..1 (warm → white); ignored when colorHex set
  "colorHex": "FF4500",      // RRGGBB, or null when using color temperature
  "showGrid": true,
  "gridSpacing": 42,         // points, 20..150
  "keepAwake": false,        // screen auto-lock disabled
  "fxMode": "off",           // off | breathe | cycle | candle | custom:<uuid>
  "fxSpeed": 1.0             // 0.25..2.0
}

State broadcasts are coalesced to ~25/s during slider drags. peers, scenes, and fxPrograms are re-broadcast whenever they change.

Commands (client → server)

{"type":"command","action":"brightness","value":0.5}      // 0..1, clamped
{"type":"command","action":"whiteBalance","value":0.3}    // 0..1, clamped
{"type":"command","action":"color","hex":"4169E1"}        // RRGGBB; null/omit = back to temperature
{"type":"command","action":"grid","enabled":true,"spacing":50}  // spacing 20..150
{"type":"command","action":"keepAwake","enabled":true}
{"type":"command","action":"fx","mode":"breathe","speed":1.0}  // effect + speed 0.25..2.0
{"type":"command","action":"reset"}                        // restore defaults (fx → off)

Commands don't get individual replies; the resulting state broadcast is the acknowledgement.

Effects

Built-in fxMode values are off, breathe, cycle, and candle. A table's user-programmed effects are addressed as custom:<uuid> and listed in the fxPrograms message:

{"type":"fxPrograms","programs":[{"id":"<uuid>","name":"Sunset Loop"}]}

Request the list any time with {"type":"fx.list"}. To run an effect, send an fx command with the desired mode (a built-in name or custom:<uuid>) and speed. All effects are intentionally slow and smooth for photosensitivity safety; custom programs are edited on the table itself, not over the API.

Scenes

{"type":"scenes.list"}                                   // → scenes message
{"type":"scenes.apply","id":"<uuid>"}
{"type":"scenes.save","name":"Sunset"}                   // saves the table's current settings
{"type":"scenes.rename","id":"<uuid>","name":"Sunset 2"}
{"type":"scenes.delete","id":"<uuid>"}

The scenes message lists each scene as:

{"id":"<uuid>","name":"Sunset","brightness":0.8,"whiteBalance":0.2,
 "showGrid":false,"gridSpacing":50,"customColorHex":"FF4500",
 "lastUsed":1783123456.0}

Export

{"type":"export.scene","id":"<uuid>"}   // one scene (.flps)
{"type":"export.db"}                    // whole library (.flpdb)

Server replies with the file inline:

{"type":"export","filename":"Sunset.flps","mime":"application/json",
 "dataB64":"<base64 of the JSON file>"}

Both formats are JSON arrays of scene objects (the app's own import/export format), so a .flpdb can be re-imported from Settings.

Keepalive

The server pings WebSocket clients every 25 s (frame-level ping; browsers answer automatically). LTRC connections rely on TCP; no application-level ping is sent.

Notes

  • All traffic is plain HTTP/WS on the local network — the PIN deters casual meddling but is not transport security.
  • The server only runs while the Light Table screen is open on the device.
  • Limits: requests up to 64 KB, WebSocket frames up to 1 MB.