Skip to main content

Shared Storage

Both surfaces (editor and layer) share useExtensionStorage(). The editor writes it; every surface that renders the installation reads it and receives each write in real time via WebSocket.

How it works​

Editor writes: setVisible(true) // useExtensionStorage("visible", false)
-> postMessage to Lumio host (iframe boundary)
-> Lumio API: POST /v1/extension-installs/{id}/storage
-> PostgreSQL: upsert the key in extension_storage
-> Redis pub/sub: PUBLISH lumio:ext-storage:{install_id} ...
-> WebSocket broadcast: ext-storage:{install_id} channel
-> Layer receives: storage update event -> re-renders

On start, each surface loads the installation's stored values once (GET /v1/extension-installs/{id}/storage) and then follows the ext-storage:\{install_id\} channel. A key that was never stored keeps the initial value your code passes.

Who can write​

Only the editor (the Lumio dashboard, running as the signed-in member) writes storage. A layer — an overlay in OBS or a widget — reads storage but never writes it: its link carries a token that streamers sometimes show on stream, and a raw storage write would let anyone with that link change what the layer renders.

When a layer calls a storage setter, the host refuses the write, rolls the value back to the stored one and reports the error STORAGE_READ_ONLY (Overlay and widget tokens cannot write extension storage). If a layer has to change state — a counter, a vote — call a server function mutation instead. You validate its input, and a layer may call it.

Usage​

import { Toggle, useExtensionStorage } from "@zaflun/lumio-sdk";

function MyComponent() {
// One hook call per key; the second argument is the value until one is stored
const [isVisible, setVisible] = useExtensionStorage("visible", false);
const [teamName] = useExtensionStorage("homeTeam", "Home Team");

return (
<Toggle label={`Show ${teamName}`} checked={isVisible} onChange={setVisible} />
);
}

A setter writes only its own key. Two surfaces or components writing different keys never overwrite each other.

Storage type​

Storage is a flat key-value store. Each key holds one JSON-serializable value:

type ExtensionStorageValue =
| string | number | boolean | null | string[] | number[] | Record<string, unknown>;

When you store an object under one key, the setter replaces that key's whole value - spread the current one (setScore({ ...score, home: 2 })). For structured data with relations and queries, use server functions instead.

Initial value​

Until the stored values have loaded, and for a key that was never stored, the hook returns the initialValue you pass:

const [text] = useExtensionStorage("text", "Default text");
const [count] = useExtensionStorage("count", 0);
const [enabled] = useExtensionStorage("enabled", true);

Storage persistence​

Storage is persisted in Lumio's PostgreSQL database, one row per key. It survives:

  • Page refreshes
  • Browser Source reloads
  • Browser restarts
  • Extension updates (unless you explicitly reset it in a mutation)

It is deleted together with the installation:

  • Uninstall with "delete data now" — deleted immediately.
  • Uninstall with the default 30-day retention — no longer readable or writable from the moment of uninstall; deleted when the retention window ends.
  • Account dissolution — deleted with the account.

Do not keep anything in storage that must outlive the installation.

Storage vs server functions​

useExtensionStorage()Server functions
Where storedOne row per key, per installationext_\{id\}.* PostgreSQL tables
Who can writeEditor (dashboard)Server function mutations (from any surface)
Real-time syncAutomatic (WebSocket)Manual (call refetch())
Data modelFlat key-value storeRelational tables with schema
QueriesRead one key per hookFiltered, sorted, paginated rows
Best forSettings, toggles, countersLists, leaderboards, complex state

Scoping​

Storage is scoped to the extension installation — one installation per channel (account). Every surface and every overlay of that channel that uses your extension reads and writes the same storage. Two channels that install your extension each have their own independent storage.

Storage limits​

Each key is stored separately. The limits apply per installation and are enforced on every write:

LimitValue
Key length256 bytes (UTF-8); not empty, no :
One value64 KiB of serialized JSON
Keys per installation1 000
Total size per installation1 MiB (keys + JSON values)

A write over a limit is rejected with 400 Bad Request (GraphQL: code: BAD_REQUEST) and nothing is stored, for example Extension storage is limited to 1000 keys per install. Overwriting an existing key with a smaller value always works, so an installation at its limit can still update its keys. For larger data volumes, use server functions with defineTable.

Optimistic updates​

A storage setter applies an optimistic update - the local state updates immediately, and the network write happens asynchronously. If the network write fails (for example over a limit), the host reports STORAGE_WRITE_FAILED with the API's message and the key reverts to its last stored value; a key that was never stored keeps the local value. You do not need to handle loading states for storage writes.