Lumen Mod SDK
Documentation
Lumen mods are packages that hook into the browser at a level no extension can reach. They can restyle the entire browser chrome, intercept tab events, replace the alt-tab switcher, inject scripts into pages, play sounds, and run background music — all through a clean, versioned JavaScript API.
This reference covers everything from writing your first manifest.json to the full window.__lumen__ API surface.
experimental. Breaking changes follow semver with a deprecation window of at least two major releases.What is a mod?
A mod is a folder containing a manifest.json and one or more JS, CSS, or HTML files. When installed, Lumen sandboxes it, grants only the declared permissions, and loads it alongside the browser shell — not inside a tab.
Unlike browser extensions (which live in a tab's renderer process), mods run in a privileged shell context. That's what allows them to:
- Rewrite the browser chrome stylesheet without any DevTools hack
- Replace the built-in new tab page with a full WebGL canvas
- Hook into every tab open/close event natively
- Play audio through a dedicated low-latency context that survives navigation
- Replace the alt-tab overlay entirely
Quick Start
The fastest way to build a mod is to clone the starter template and load it in Dev Mode.
1. Clone the template
git clone https://github.com/Gav2011/lumen-mod-starter my-mod cd my-mod
2. Edit the manifest
{
"name": "My First Mod",
"id": "com.yourname.my-mod",
"version": "0.1.0",
"author": "yourname",
"description": "My first Lumen mod.",
"permissions": ["browser.css"],
"browser_css": "theme.css"
}
3. Load in Dev Mode
- Open Lumen → Settings → Mods → Dev Mode
- Click Load unpacked mod and select your folder
- Edit files and click Reload Mod to see changes live
id field must be a reverse-domain string like com.yourname.modname. IDs are permanent — changing after publishing creates a new listing.Dev Mode
Dev Mode lets you load any local folder as a mod without packaging. It auto-disables when Lumen restarts in production mode.
Dev toolbar features
- Reload Mod — hot-reloads JS and CSS without restarting the browser
- Open DevTools (Mod) — DevTools scoped to your mod's shell context
- View Permissions — shows active and denied permissions
- Console — streams
lumen.log()output in real time
Logging
const lumen = window.__lumen__; lumen.log("Mod loaded"); lumen.log("Tab count:", await lumen.tabs.count()); lumen.warn("Something unexpected"); lumen.error("Shows red in the dev console");
manifest.json
Every mod must have a manifest.json at its root. Lumen validates this file before loading anything from the package.
{
// ── Identity ────────────────────────────────
"name": "Mod display name", // required
"id": "com.author.modname", // required, permanent
"version": "1.0.0", // required, semver
"author": "Your Name",
"description": "Short description.",
"icon": "icon.png", // 128x128 PNG
"preview": "preview.jpg", // 640x360 min
"homepage": "https://yoursite.com",
"tags": ["dark", "minimal"],
// ── Permissions ─────────────────────────────
"permissions": [
"browser.css", // inject CSS into browser chrome
"browser.js", // run JS in browser shell context
"browser.html", // inject HTML (newtab, overlays)
"page.inject", // inject into web pages
"mod.fs", // save settings/data to disk
"network", // outbound fetch / WebSocket
"location", // geolocation (IP or OS)
"audio.ui", // browser event sounds
"audio.bg", // persistent background audio
"settings", // read/write browser settings
"tabs" // tab list, titles, screenshots
],
// ── Entry Points ────────────────────────────
"entry": "index.js",
"browser_css": "theme.css",
// ── Page Injection ──────────────────────────
"inject_rules": [{
"matches": ["https://github.com/*"],
"js": ["inject/github.js"],
"css": ["inject/github.css"],
"run_at": "document_end",
"world": "ISOLATED"
}],
// ── Sounds ──────────────────────────────────
"sounds": {
"tab.open": "sounds/open.wav",
"tab.close": "sounds/close.wav",
"tab.switch": "sounds/switch.wav",
"navigate.start": "sounds/nav.wav",
"navigate.done": "sounds/done.wav",
"download.done": "sounds/dl.wav",
"error.404": "sounds/err.wav"
},
// ── New Tab ─────────────────────────────────
"newtab": {
"type": "html",
"entry": "newtab/index.html",
"settings_widget": "newtab/settings.html",
"preview_image": "newtab/preview.jpg"
},
// ── Alt-Tab ─────────────────────────────────
"alttab_panel": "alttab/panel.html",
// ── Background Audio ────────────────────────
"background_audio": {
"playlist": ["music/track1.mp3"],
"loop": true,
"default_volume": 0.25,
"auto_duck": true,
"duck_volume": 0.05
},
"min_lumen_version": "1.0.0"
}
Fields Reference
| Field | Type | Req | Description |
|---|---|---|---|
name | string | ✓ | Display name in the store and settings. Max 64 chars. |
id | string | ✓ | Permanent reverse-domain ID, e.g. com.author.modname. Never changes after publishing. |
version | string | ✓ | Semver string. Used to trigger update prompts. |
author | string | — | Author name shown on the store card. |
description | string | — | Short description. Max 280 chars. |
icon | string | — | Path to a 128×128 PNG icon. |
preview | string | — | Store card image path (16:9, min 640×360). |
permissions | string[] | — | Array of permission strings. Undeclared permissions throw at runtime. |
entry | string | — | Main JS file loaded in the browser shell. Requires browser.js. |
browser_css | string | — | CSS injected into the browser chrome. Requires browser.css. |
inject_rules | object[] | — | Content script rules. Requires page.inject. |
sounds | object | — | Map of event names to audio file paths. Requires audio.ui. |
newtab | object | — | New tab config. Requires browser.html. |
alttab_panel | string | — | HTML for the alt-tab overlay. Requires browser.html. |
background_audio | object | — | Background music config. Requires audio.bg. |
min_lumen_version | string | — | Minimum Lumen version required to run this mod. |
Full Examples
Minimal CSS skin
{
"name": "Midnight Skin",
"id": "com.you.midnight-skin",
"version": "1.0.0",
"permissions": ["browser.css", "mod.fs"],
"browser_css": "theme.css",
"entry": "index.js"
}
Full newtab mod (like Stellar Newtab)
{
"name": "Stellar Newtab",
"id": "com.cosmicbytes.stellar-newtab",
"version": "2.1.0",
"author": "cosmicbytes",
"description": "Real-time WebGL star field for your new tab.",
"icon": "icon.png",
"preview": "preview.jpg",
"tags": ["webgl", "3d", "animated"],
"permissions": [
"browser.html", "browser.css", "browser.js",
"location", "network", "mod.fs"
],
"newtab": {
"type": "webgl",
"entry": "newtab/index.html",
"settings_widget": "newtab/settings.html",
"preview_image": "preview.jpg"
},
"entry": "index.js",
"min_lumen_version": "1.2.0"
}
Lumen JS API — Overview
When your mod's entry file loads, window.__lumen__ is available. Destructure it at the top of your entry point:
const lumen = window.__lumen__; // Or destructure only what you need const { css, browser, tabs, audio, storage, newtab, alttab, inject } = window.__lumen__;
Every async API returns a Promise. Calling an API without its required permission throws a LumenPermissionError synchronously.
lumen.hasPermission('perm.name') first.lumen.css requires: browser.css
Injects CSS into the browser chrome. Returns the injection ID. Re-injecting with the same ID replaces the previous rules in-place.
Removes a previously injected stylesheet by ID.
Read/write Lumen's exposed CSS variables. Changes apply immediately.
lumen.css.vars['--lumen-accent'] = '#cc5de8'; lumen.css.vars['--lumen-toolbar-height'] = '52px'; lumen.css.vars['--lumen-radius'] = '4px';
CSS Variables Reference
| Variable | Default | Description |
|---|---|---|
--lumen-tab-bg | #0c1220 | Inactive tab background |
--lumen-tab-active | #050810 | Active tab background |
--lumen-tab-text | #e8f4fd | Tab label color |
--lumen-toolbar-bg | #0c1220 | Address bar row background |
--lumen-toolbar-height | 44px | Height of the toolbar row |
--lumen-sidebar-bg | #0f1622 | Sidebar panel background |
--lumen-accent | #4dabf7 | Browser accent color |
--lumen-border | rgba(100,180,255,.1) | Default border color |
--lumen-radius | 8px | Tab/button border radius |
--lumen-font | 'Syne', sans-serif | Browser UI font stack |
lumen.browser requires: browser.js
Subscribe to a browser lifecycle event. Returns an unsubscribe function.
const off = lumen.browser.on('tab.open', ({ tab }) => { lumen.log('New tab:', tab.url); }); off(); // unsubscribe
| Event | Payload | Description |
|---|---|---|
| tab.open | { tab } | New tab created. |
| tab.close | { tabId } | Tab closed. |
| tab.switch | { from, to } | Active tab changed. |
| navigate.start | { tabId, url } | Navigation started. |
| navigate.done | { tabId, url } | Page finished loading. |
| download.start | { download } | Download began. |
| download.done | { download } | Download completed. |
| lumen.focus | — | Browser window gained focus. |
| lumen.blur | — | Browser window lost focus. |
Returns Lumen version, platform, and active mod list.
lumen.tabs requires: tabs
Returns all open tabs. Each Tab: id, title, url, favicon, screenshot, active, loading.
Switch to the specified tab.
Returns the number of open tabs.
Returns a screenshot of the tab as a data URL. Rate-limited to 5 per second per mod.
lumen.audio requires: audio.ui · audio.bg
Register a sound hook for a browser event. Requires audio.ui.
lumen.audio.on('tab.open', { src: 'sounds/pop.wav', volume: 0.4, pitch: () => 0.9 + Math.random() * 0.2 });
Controls background audio. Requires audio.bg.
const p = lumen.audio.BGPlayer; await p.setPlaylist(['music/01.mp3', 'music/02.mp3']); await p.play(); p.setVolume(0.3); p.setAutoDuck(true); p.on('track.change', ({ title }) => lumen.log('Now:', title)); p.on('duck.start', () => {}); // tab started playing p.on('duck.end', () => {}); // tab went silent
lumen.storage requires: mod.fs
Key-value storage scoped to your mod. Persists across restarts. 50MB limit per mod.
Read a value. Returns null if the key doesn't exist.
Write any JSON-serialisable value. Max 5MB per key.
Delete a key.
const DEFAULTS = { volume: 0.3, theme: 'dark' }; async function loadSettings() { const saved = await lumen.storage.get('settings'); return { ...DEFAULTS, ...(saved ?? {}) }; } async function saveSettings(patch) { const cur = await loadSettings(); await lumen.storage.set('settings', { ...cur, ...patch }); }
lumen.newtab requires: browser.html
Available inside your newtab entry HTML file.
Fired when the new tab panel is shown. Context contains canvas (WebGL type) or container (HTML type).
Fired when the panel is hidden. Clean up WebGL contexts and animation loops here.
lumen.newtab.onMount(({ canvas }) => { const gl = canvas.getContext('webgl2'); let raf; (function loop() { /* render */ raf = requestAnimationFrame(loop); })(); lumen.newtab.onUnmount(() => cancelAnimationFrame(raf)); });
lumen.alttab requires: browser.html · tabs
Fired when the user triggers the tab switcher. Receives the full Tab[] array.
Switch to a tab and dismiss the overlay.
Close the overlay without switching tabs.
lumen.inject requires: page.inject
Programmatic injection from shell JS. For static rules use inject_rules in the manifest.
Register a content script rule at runtime. Returns the rule ID.
let ruleId = null; function toggle(on) { if (on && !ruleId) { ruleId = lumen.inject.addRule({ matches: ['https://github.com/*'], js: ['inject/github.js'], run_at: 'document_end' }); } else if (!on && ruleId) { lumen.inject.removeRule(ruleId); ruleId = null; } }
Permission Reference
Declare every permission your mod uses in manifest.json. Lumen shows users the full list before installation.
| Permission | Chip | What it allows | Notes |
|---|---|---|---|
browser.css | Browser CSS | Inject/override stylesheets in the browser chrome via lumen.css.*. | Cannot affect webpage content. |
browser.js | Browser JS | Run JS in the browser shell context. Required for any entry file. | Separate from page JS worlds. |
browser.html | HTML Inject | Register HTML panels for the new tab page or alt-tab overlay. | Required for newtab and alttab_panel. |
page.inject | Page Inject | Inject JS/CSS into websites per declared match patterns. | Each new origin needs a one-time user approval. |
mod.fs | ModFileSystem | Read/write persistent storage via lumen.storage.*. | Scoped to your mod. 50MB limit. |
network | Network | Make outbound fetch() and WebSocket calls from shell JS. | No CORS bypass. Cannot send browser cookies. |
location | Location | Access geolocation — OS-provided first, IP fallback. | User dialog on first use. Revocable in Settings. |
audio.ui | Audio UI | Play sounds tied to browser events via lumen.audio.on(). | Respects OS system volume. |
audio.bg | Audio BG | Persistent background audio via lumen.audio.BGPlayer. | Auto-ducks when tabs play media. One mod owns this at a time. |
settings | Settings | Read or modify Lumen browser settings. | Write ops show a per-setting confirmation. |
tabs | Tab Access | Read tab metadata: titles, URLs, favicons, screenshots. | Screenshots rate-limited to 5/sec per mod. |
Guide: Writing a CSS Skin
A CSS skin only needs two files. Here's a full walkthrough with settings support.
File structure
my-skin/ ├── manifest.json ├── icon.png ├── preview.jpg ├── theme.css └── index.js
theme.css — targeting the shell
/* Override the default CSS variables */ :root { --lumen-tab-bg: #0a0a12; --lumen-tab-active: #1a1a2e; --lumen-toolbar-bg: #05050d; --lumen-accent: #cc5de8; --lumen-border: rgba(204,93,232,.15); --lumen-radius: 4px; } /* Target specific shell elements */ .lumen-tab.active { box-shadow: inset 0 2px 0 var(--lumen-accent); } .lumen-address-bar { font-family: 'DM Mono', monospace; border-radius: 6px; }
index.js — user-configurable themes
const lumen = window.__lumen__; const THEMES = { purple: { '--lumen-accent': '#cc5de8' }, blue: { '--lumen-accent': '#4dabf7' }, green: { '--lumen-accent': '#69db7c' }, }; async function init() { const s = await lumen.storage.get('settings') ?? { theme: 'purple' }; for (const [k, v] of Object.entries(THEMES[s.theme] ?? THEMES.purple)) { lumen.css.vars[k] = v; } } init();
Guide: Page Injection
Inject JS/CSS into the websites users visit. Common uses: removing ads, restyling a specific site, adding keyboard shortcuts.
ISOLATED scripts can't access page JS variables. MAIN world shares the page's context but needs a stronger permission prompt and extra review scrutiny.const lumen = window.__lumenMod__; // Inject CSS into this page only lumen.css.inject(` .author-name { color: #c77dff !important; } `); // Re-apply after SPA navigation lumen.browser.on('navigate.done', applyTweaks); function applyTweaks() { document.querySelectorAll('.js-navigation-item').forEach(el => { el.style.borderRadius = '8px'; }); } applyTweaks();
Guide: Custom New Tab
Replace Lumen's default new tab with your own HTML file. You get full access to the window — WebGL, WebAudio, geolocation, fetch — limited only by your declared permissions.
<!DOCTYPE html>
<html><head>
<style>
body { margin:0; background:#07050f; overflow:hidden; }
canvas { display:block; width:100vw; height:100vh; }
</style>
</head><body>
<canvas id="c"></canvas>
<script>
lumen.newtab.onMount(async () => {
const canvas = document.getElementById('c');
let coords = null;
if (await lumen.hasPermission('location')) {
coords = await lumen.location.get();
}
const gl = canvas.getContext('webgl2');
startStarField(gl, coords);
});
lumen.newtab.onUnmount(() => stopStarField());
</script>
</body></html>
settings_widget HTML file to let users configure your new tab. Lumen renders it in a popover from the ⚙ icon that appears on hover at the bottom-right of your new tab page.Submit a Mod
Package your mod as a .lmod file (a zip archive) and submit via the store button or by posting on Rift.
Pre-submission checklist
- All permissions have a clear, documented purpose
idis unique and follows reverse-domain formaticon.pngis 128×128,preview.jpgis at least 640×360- Tested in Dev Mode across multiple sessions
- No hardcoded API keys or secrets
- No
eval(),new Function(), or dynamic script injection min_lumen_versionset if you use post-1.0 APIs
Packaging
zip -r my-mod.zip . -x "*.DS_Store" -x ".git/*" mv my-mod.zip my-mod.lmod
Use Submit Mod in the Mod Store, or post on Rift if you have questions.
Mod Review Guidelines
All mods go through manual review before appearing in the store. Reviews typically take 2–5 business days.
What reviewers check
- Permission justification — every declared permission must be used and justifiable
- No data exfiltration — mods with
networkmust not send browsing data to remote servers without explicit user consent shown in the UI - No obfuscated code — minification is OK, obfuscation is not
- No eval —
eval(),new Function(), anddocument.write()are blocked at the sandbox level - Clean uninstall — disabling or removing the mod must fully revert all changes
- Accurate description — must accurately describe what the mod does
Appeals
Rejected? You'll receive a reason with the rejection. Fix the issues and resubmit, or open a discussion on Rift if you think the rejection was an error.
Changelog
SDK v1.2.0 — current
- Added
lumen.audio.BGPlayer.on('duck.start')and'duck.end' - Added
lumen.tabs.screenshot() - Added
lumen.inject.addRule()/removeRule()for runtime injection newtab.type: 'webgl'now passes a pre-configured canvas toonMount- CSS variable
--lumen-radiusexposed
SDK v1.1.0
- Added
browser.htmlpermission andnewtab/alttab_panelfields - Added
lumen.focusandlumen.blurbrowser events - Added
settingspermission - Dev Mode toolbar introduced
SDK v1.0.0
- Initial public SDK release
- Core permissions:
browser.css,browser.js,page.inject,mod.fs,network,location,audio.ui,audio.bg,tabs - Manifest v1 spec finalised
- Mod Store opens for submissions
Bug Reports
Found a bug? The fastest way to report it and get a response is via the ModemINC Rift community.
What to include
- Lumen version (Settings → About)
- OS and OS version
- Minimal reproduction — the smallest
manifest.json+ JS that triggers the bug - Expected vs. actual behaviour
- Any errors from the Dev Mode console
Security issues
For sandbox or permission vulnerabilities, Open a ticket on Rift. Our team is focused on rift, Responses can very.