> ## Documentation Index
> Fetch the complete documentation index at: https://docs.layarva.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Build a Widget WebSocket Relay

> Create, secure, deploy, and test a WebSocket source for live Layarva Widget data.

## What you'll accomplish

You will understand the WebSocket data path, create a small Node.js relay, emit valid `RuntimeDataEnvelope v1` snapshots, test locally, and prepare a production `wss://` endpoint.

## Who must provide the WebSocket server?

Layarva Player is a **WebSocket client**. It connects to an endpoint and listens; it does not create the server that publishes your data.

For the current Secure WebSocket mode, the customer or integration partner normally provides the relay. You can:

* host a small relay on your own server or container platform;
* use a managed service that supports WebSocket broadcasting; or
* connect directly to an existing provider only when its endpoint already satisfies the Layarva message and security requirements.

Layarva does not currently provide a generic hosted WebSocket broker for customer Widget data.

<Tip>
  If updates every few seconds are sufficient, use [Structured Widget Push API](/developers/structured-widget-push). Push API is simpler to operate because no continuously connected server is required.
</Tip>

## How it works

```text theme={null}
1. Your source changes
   CMS / ERP / POS / IoT / database
                 |
                 v
2. Your relay builds RuntimeDataEnvelope v1
                 |
                 v  wss:// persistent outbound connection
3. Each Layarva Player receives and validates the message
                 |
                 v
4. Player stores Last Known Good and renders the Widget
```

The Player rejects malformed messages, messages larger than 1 MiB, and messages whose `sourceId` does not exactly match the Widget ID. When the connection closes, it reconnects with exponential backoff up to five minutes.

## Prerequisites

* Node.js 20 or later for this example.
* A host that supports long-lived WebSocket connections.
* A DNS name and valid TLS certificate for production.
* The Unique Widget ID and workspace ID.
* Data that can be transformed into the selected Widget's semantic schema.

## Create the relay

Create a new server project outside browser code:

```bash theme={null}
mkdir layarva-widget-relay
cd layarva-widget-relay
npm init -y
npm install ws
```

Create `server.mjs`:

```js theme={null}
import { createHash } from "node:crypto";
import { WebSocket, WebSocketServer } from "ws";

const port = Number(process.env.PORT ?? 8080);
const widgetId = process.env.LAYARVA_WIDGET_ID;
const workspaceId = process.env.LAYARVA_WORKSPACE_ID;

if (!widgetId || !workspaceId) {
  throw new Error("LAYARVA_WIDGET_ID and LAYARVA_WORKSPACE_ID are required");
}

const wss = new WebSocketServer({ port });
let sequence = 0;

function buildWidgetData(now) {
  return {
    schemaVersion: 2,
    title: "Today's Agenda",
    locale: "en-US",
    timezone: "Asia/Jakarta",
    date: now.toLocaleDateString("en-CA", { timeZone: "Asia/Jakarta" }),
    updatedAt: now.toISOString(),
    items: [
      {
        id: "agenda-001",
        startAt: "2026-08-27T09:00:00+07:00",
        endAt: "2026-08-27T09:30:00+07:00",
        title: "Morning Briefing",
        location: "Meeting Room A",
        status: "SCHEDULED"
      }
    ]
  };
}

function buildEnvelope() {
  const now = new Date();
  const data = buildWidgetData(now);
  const contentHash = createHash("sha256")
    .update(JSON.stringify(data))
    .digest("hex");

  return {
    schemaVersion: "1",
    sourceId: widgetId,
    workspaceId,
    sequence: ++sequence,
    contentHash,
    generatedAt: now.toISOString(),
    expiresAt: new Date(now.getTime() + 60_000).toISOString(),
    staleAt: new Date(now.getTime() + 86_400_000).toISOString(),
    status: "FRESH",
    data
  };
}

function sendSnapshot(client) {
  if (client.readyState === WebSocket.OPEN) {
    client.send(JSON.stringify(buildEnvelope()));
  }
}

wss.on("connection", (client, request) => {
  const expectedPath = `/widgets/${widgetId}`;
  const path = new URL(request.url ?? "/", "http://relay.local").pathname;

  if (path !== expectedPath) {
    client.close(1008, "Unknown Widget source");
    return;
  }

  sendSnapshot(client);
});

setInterval(() => {
  for (const client of wss.clients) sendSnapshot(client);
}, 30_000);

console.log(`Widget relay listening on port ${port}`);
```

This example broadcasts a complete schedule snapshot every 30 seconds and immediately after a Player connects. Replace `buildWidgetData()` with a database query, event subscription, or provider call.

## Run locally

macOS or Linux:

```bash theme={null}
export LAYARVA_WIDGET_ID="wdg_example"
export LAYARVA_WORKSPACE_ID="ws_example"
node server.mjs
```

Windows PowerShell:

```powershell theme={null}
$env:LAYARVA_WIDGET_ID = "wdg_example"
$env:LAYARVA_WORKSPACE_ID = "ws_example"
node server.mjs
```

Connect a test client:

```bash theme={null}
npx wscat -c ws://localhost:8080/widgets/wdg_example
```

Use this `ws://localhost` address only for local testing. Do not publish a Design with a localhost endpoint; a remote Player would interpret localhost as the Player device itself.

## Understand RuntimeDataEnvelope v1

| Field           | Required | Rule                                                                          |
| --------------- | -------- | ----------------------------------------------------------------------------- |
| `schemaVersion` | Yes      | String `"1"`                                                                  |
| `sourceId`      | Yes      | Exact Unique Widget ID                                                        |
| `workspaceId`   | Yes      | Owning workspace identity                                                     |
| `sequence`      | Yes      | Safe integer that increases for every new snapshot                            |
| `contentHash`   | Yes      | Lowercase SHA-256, exactly 64 hexadecimal characters                          |
| `generatedAt`   | Yes      | ISO 8601 generation time                                                      |
| `expiresAt`     | Yes      | Time after which the snapshot becomes stale                                   |
| `staleAt`       | Yes      | Time after which the snapshot must no longer be used                          |
| `status`        | Yes      | `FRESH`, `CACHED`, `STALE`, or `ERROR`                                        |
| `data`          | Yes      | Object containing semantic Widget data or source data for declarative mapping |
| `error`         | No       | `{ code, message, retryable }` when the source reports an error               |

Use this ordering:

```text theme={null}
generatedAt < expiresAt < staleAt
```

The Player accepts the envelope directly or inside `{ "envelope": { ... } }`.

## Deploy with TLS

Production Studio configuration accepts `wss://`. Deploy the Node process behind a TLS-terminating load balancer or reverse proxy that supports WebSocket upgrades.

Example reverse-proxy requirements:

```text theme={null}
Public:  wss://stream.example.com/widgets/wdg_example
Proxy:   preserve HTTP/1.1 Upgrade and Connection headers
Target:  ws://127.0.0.1:8080/widgets/wdg_example
```

Confirm that:

* the TLS certificate is valid for the hostname;
* the endpoint is reachable from every Player network;
* idle connections are not closed too aggressively by the proxy;
* the service restarts automatically after a crash or host restart;
* the relay can handle one connection per online Player using that Widget source.

## Configure Studio

1. Select the Widget in Studio.
2. Open **Properties → Data Source**.
3. Select **Secure WebSocket**.
4. Enter `wss://stream.example.com/widgets/<WIDGET_ID>`.
5. Set **Reconnect** to five seconds initially.
6. Keep valid Static JSON as a fallback.
7. Save and publish the Design.

## Security model and current limitation

TLS protects data in transit, but `wss://` alone does not decide which client may subscribe.

The current Studio WebSocket configuration does not provide arbitrary authentication headers, cookies, or a configurable subprotocol. Therefore:

* never put API keys, passwords, or long-lived bearer tokens in the URL;
* keep provider credentials inside the relay, not in Studio or the Player;
* expose only display-safe data;
* use network allowlisting, private connectivity, or a gateway appropriate to your deployment when access must be restricted;
* do not use the current direct WebSocket mode for sensitive data on an unauthenticated public endpoint.

If the upstream provider requires a secret, the relay authenticates to that provider server-side and publishes a separate display-safe WebSocket endpoint to Players.

## Reconnect and Last Known Good

1. The Player connects using the configured initial reconnect delay.
2. A valid message becomes the current snapshot and is persisted as Last Known Good.
3. A malformed message is rejected without replacing the valid snapshot.
4. On close, retries back off exponentially up to 300 seconds.
5. After `expiresAt`, data is treated as stale.
6. After `staleAt` or the Player's maximum stale policy, the snapshot is no longer eligible.
7. Static JSON remains the final safe fallback.

## Production validation

* [ ] `wss://` is used with a valid certificate.
* [ ] The endpoint is reachable from the actual Player network.
* [ ] A snapshot is sent immediately after connection.
* [ ] Every message is less than 1 MiB.
* [ ] `sourceId` exactly matches the Widget ID.
* [ ] `sequence` increases and `contentHash` is recomputed from `data`.
* [ ] Timestamps are parseable and ordered correctly.
* [ ] `data` passes the selected Widget schema after declarative mapping.
* [ ] Relay restart and Player reconnect have been tested.
* [ ] Last Known Good and Static JSON fallback have been observed during an outage.
* [ ] Capacity testing accounts for every connected Player.

## Troubleshooting

| Symptom                                      | Likely cause                                         | Action                                                             |
| -------------------------------------------- | ---------------------------------------------------- | ------------------------------------------------------------------ |
| Connection is rejected immediately           | Wrong path or proxy is not upgrading WebSocket       | Verify the public path and Upgrade forwarding                      |
| Studio accepts URL but Player never connects | DNS, firewall, certificate, or endpoint reachability | Test from the Player network using the production hostname         |
| Connection succeeds but Widget never changes | Envelope is invalid or `sourceId` differs            | Validate every required field and exact Widget ID                  |
| Updates stop after proxy idle timeout        | No traffic or proxy timeout is too short             | Send regular snapshots and increase the proxy idle timeout         |
| Old snapshot stays on screen                 | Last Known Good is still eligible                    | Review `expiresAt`, `staleAt`, and fallback settings               |
| Many Screens overwhelm the relay             | One connection is opened per Player                  | Add connection monitoring, horizontal scale, and broadcast fan-out |

## Related guides

* [Connect Data to a Widget](/studio/widget-data-sources)
* [Push Structured Widget Data](/developers/structured-widget-push)
* [Understand Publishing](/studio/publishing)
