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} />
);
}
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 stored | ext_\{id\} storage row | ext_\{id\}.* PostgreSQL tables |
| Who can write | Any surface | Only server function mutations |
| Real-time sync | Automatic (WebSocket) | Manual (call refetch()) |
| Data model | Flat key-value object | Relational tables with schema |
| Queries | Read entire object | Filtered, sorted, paginated rows |
| Best for | Settings, toggles, counters | Lists, 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.