Skip to main content

Shared Storage

All three surfaces share useExtensionStorage(). When any surface writes to storage, all other surfaces receive the update in real time via WebSocket.

How it works

Editor writes: setStorage({ visible: true })
-> postMessage to Lumio host (iframe boundary)
-> Lumio API: POST /v1/extension-installs/{id}/storage
-> PostgreSQL: UPDATE storage SET value = ...
-> Redis pub/sub: PUBLISH lumio:ext-storage:{install_id} ...
-> WebSocket broadcast: ext-storage:{install_id} channel
-> Layer receives: storage update event -> re-renders
-> Interactive receives: storage update event -> re-renders

Usage

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

function MyComponent() {
const [storage, setStorage] = useExtensionStorage();

// Read a value with a default fallback
const isVisible = storage.visible ?? false;
const teamName = storage.homeTeam ?? "Home Team";

// Write: always spread to preserve other keys
const toggle = () => {
setStorage({ ...storage, visible: !isVisible });
};

return (
<Toggle label="Visible" checked={isVisible} onChange={toggle} />
);
}
warning

Always spread the existing storage when writing: setStorage({ ...storage, key: value }). Passing only { key: value } replaces the entire storage object, deleting all other keys.

Storage type

The storage object is a flat key-value store where values can be any JSON-serializable type:

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

For structured data with relations and queries, use server functions instead.

Initial value

On first load, before any data is written, storage is an empty object ({}). Always provide fallback defaults:

// Good: provide defaults for all fields you read
const text = storage.text ?? "Default text";
const count = storage.count ?? 0;
const enabled = storage.enabled ?? true;

// Bad: assuming fields always exist (may be undefined)
const text = storage.text;

Storage persistence

Storage is persisted to the database. It survives:

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

Storage vs server functions

useExtensionStorage()Server functions
Where storedext_\{id\} storage rowext_\{id\}.* PostgreSQL tables
Who can writeAny surfaceOnly server function mutations
Real-time syncAutomatic (WebSocket)Manual (call refetch())
Data modelFlat key-value objectRelational tables with schema
QueriesRead entire objectFiltered, sorted, paginated rows
Best forSettings, toggles, countersLists, leaderboards, complex state

Scoping

Storage is scoped to the extension installation — one record per overlay that has the extension installed. If a streamer installs your extension on two different overlays, each installation has its own independent storage.

Storage size limit

The storage object is serialized to JSON and stored as a single database row. The maximum size is 64 KB of serialized JSON. For larger data volumes, use server functions with defineTable.

Optimistic updates

setStorage() applies an optimistic update — the local state updates immediately, and the network write happens asynchronously. If the network write fails, the storage reverts to the last server-confirmed value. You do not need to handle loading states for storage writes.