Best Practices
Guidelines for building reliable, performant, and review-friendly Lumio extensions.
Performance
Keep bundle sizes small
OBS browser sources have limited memory. Aim for bundles under 1 MB gzipped.
- Import only what you use:
import { Box, Text } from "@zaflun/lumio-sdk"notimport * as SDK from "@zaflun/lumio-sdk" - Avoid lodash, moment.js, and other large utility libraries — use native browser APIs
- Prefer
<Image>over inline base64 data URIs for assets larger than 1 KB - Run
lumio build --analyzeto identify large dependencies
Minimize re-renders on the layer
The OBS layer runs in a browser source at 60 fps. Expensive renders cause dropped frames.
- Memoize computed values with
useMemo - Memoize callbacks with
useCallbackwhen passing them to child components - Keep component state as local as possible — avoid a single top-level state object for everything
- Use
React.memoon pure display components that receive stable props
Limit event-driven updates
useLumioEvent triggers a re-render on every event. If you only need the data after an event (not reactive to each one), debounce or queue events in a ref:
const lastEventRef = useRef<LumioEvent | null>(null);
const event = useLumioEvent("twitch:cheer");
useEffect(() => {
if (event === lastEventRef.current) return; // Already processed
lastEventRef.current = event;
// Handle event...
}, [event]);
Reliability
Handle the null state
useLumioEvent returns null before the first event. useLumioConfig returns null while loading. Always handle these:
const event = useLumioEvent("twitch:follower");
const { config, loading } = useLumioConfig<MyConfig>();
if (loading) return null; // or a skeleton
if (!config) return null; // not configured yet
if (!event) return <DefaultState />; // waiting for first event
Never store secrets in client code
Secrets (API keys, webhook secrets) must live in lumio secrets, not in the extension bundle or lumio.config.json. A user can inspect the bundle at any time.
Validate server function inputs
Even though v validators strip unknown fields, add semantic validation in your handler:
handler: async (ctx, args) => {
if (args.amount <= 0 || args.amount > 1000) {
throw new Error("Amount must be between 1 and 1000");
}
// ...
}
Handle external API failures gracefully
External APIs can be slow or unavailable. Always have a fallback:
handler: async (ctx, args) => {
try {
const res = await ctx.fetch("https://api.example.com/data");
return await res.json();
} catch {
// Return cached data if available
return await ctx.db.get("global", "last_known_data") ?? { empty: true };
}
}
User experience
Design for 1920×1080 first
Most OBS users run at 1080p. Test at 1080p first, then check at 720p. Avoid fixed pixel positions that break at different canvas sizes — use percentage or anchor-based positioning instead.
Respect the overlay canvas
Overlays sit on top of game footage or IRL video. Keep the overlay visually lightweight:
- Prefer semi-transparent backgrounds over solid fills
- Avoid covering more than 20% of the canvas area
- Ensure text is readable against both light and dark backgrounds
Support the streamer's theme
Use useLumioTheme() to adapt colors to the streamer's chosen theme instead of hardcoding colors. At minimum, use the mode field to choose light/dark variants:
const theme = useLumioTheme();
const bg = theme.mode === "dark" ? "rgba(0,0,0,0.7)" : "rgba(255,255,255,0.8)";
Provide sensible defaults
Every config field should have a default so the extension works out of the box before the streamer visits the editor:
const { config } = useLumioConfig<MyConfig>();
const position = config?.position ?? "top-right";
const fontSize = config?.fontSize ?? 14;
Review readiness
These practices help your extension pass review faster.
Match egress to purpose
Only list hosts in allowHosts that your extension actually calls, and ensure each one is clearly related to the extension's stated purpose. Reviewers reject extensions with overly broad or unexplained egress hosts.
Write a clear store listing
The review team reads your store description. Explain what the extension does, what permissions it needs and why, and what data it stores. Vague descriptions cause review delays.
Test before submitting
Run through the testing checklist before every submission. Extensions that crash or display a blank screen in testing are rejected without deeper review.
Use semantic versioning
Follow semver for version strings: MAJOR.MINOR.PATCH. Increment MAJOR for breaking config changes that require re-configuring the editor panel. Increment MINOR for new features. Increment PATCH for bug fixes.
Write meaningful changelogs
The changelog is shown to users when they update the extension. Write what changed from the user's perspective, not internal implementation notes:
# Good
Fixed score display not updating during overtime periods.
Added support for displaying 3-point shooting percentage.
# Bad
Refactored ESPN fetch handler. Fixed off-by-one in state reducer.