Skip to main content

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" not import * 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 --analyze to 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 useCallback when passing them to child components
  • Keep component state as local as possible — avoid a single top-level state object for everything
  • Use React.memo on 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.