Best Practices
Guidelines for building reliable, performant, and review-friendly Lumio extensions.
Performance
Keep bundle sizes small
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 layer runs in an 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. Handle that state before reading the event:
const event = useLumioEvent("twitch:follower");
if (!event) return <DefaultState />; // waiting for first event
useLumioConfig() returns a LumioConfig object rather than a loading state. Its settings field is optional; use defaults for missing values, as shown below.
Never store secrets in client code
Secrets (API keys, webhook secrets) must live in extension secrets (lumio env set), not in the extension bundle or lumio.config.json. A user can inspect the bundle at any time.
Render buyer text as text
If an extension or automation receives a Shopify shopify:order or shopify:order_paid payload, raw.customer_display is untrusted buyer text, especially when the merchant selected custom_name. Show it through the SDK's <Text content={...} /> or an equivalent text-only renderer; never interpolate it into HTML. Handle a missing key: the buyer may not have consented, or the name may have been hidden. See the useLumioEvent notes.
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. In a bot module, save a successful response so a later command can fall back to it:
Declare a table in server/schema.ts for the saved response:
import { defineSchema, defineTable, v } from "@zaflun/lumio-sdk/server";
export default defineSchema({
last_known_data: defineTable({
key: v.string(),
payload_json: v.string(),
}),
});
import { command } from "@zaflun/lumio-sdk/server";
export const latest = command("latest", async (ctx) => {
try {
const res = await ctx.fetch("https://api.example.com/data");
if (!res.ok) throw new Error(`API error: ${res.status}`);
const data = await res.json();
const [saved] = await ctx.db.list("last_known_data", [
{ field: "key", op: "eq", value: "feed" },
]);
if (saved) {
await ctx.db.patch("last_known_data", saved._id as string, {
payload_json: JSON.stringify(data),
});
} else {
await ctx.db.insert("last_known_data", {
key: "feed",
payload_json: JSON.stringify(data),
});
}
return { reply: JSON.stringify(data) };
} catch {
// Return the saved response if available
const [saved] = await ctx.db.list("last_known_data", [
{ field: "key", op: "eq", value: "feed" },
]);
return { reply: saved ? (saved.payload_json as string) : "No data available yet." };
}
});
User experience
Design for 1920×1080 first
Most streamers 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();
const position = String(config.settings?.position ?? "top-right");
const fontSize = Number(config.settings?.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.