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.

Best place to ask questions: The ModemINC Rift community is the fastest way to get help from the dev and other mod authors.
SDK Status: The Lumen Mod SDK ships with the public beta. All APIs on this page are considered stable unless marked 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
💡
Extensions vs. Mods: Extensions (Manifest V3 / Lumen Extended) still work in Lumen and are better suited for per-site automation. Mods are for browser-level changes that apply everywhere, all the time.

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

bash
git clone https://github.com/Gav2011/lumen-mod-starter my-mod
cd my-mod

2. Edit the manifest

manifest.json
{
  "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

  1. Open Lumen → Settings → Mods → Dev Mode
  2. Click Load unpacked mod and select your folder
  3. Edit files and click Reload Mod to see changes live
ID format: The 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

javascript
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.

manifest.json — full schema
{
  // ── 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

FieldTypeReqDescription
namestringDisplay name in the store and settings. Max 64 chars.
idstringPermanent reverse-domain ID, e.g. com.author.modname. Never changes after publishing.
versionstringSemver string. Used to trigger update prompts.
authorstringAuthor name shown on the store card.
descriptionstringShort description. Max 280 chars.
iconstringPath to a 128×128 PNG icon.
previewstringStore card image path (16:9, min 640×360).
permissionsstring[]Array of permission strings. Undeclared permissions throw at runtime.
entrystringMain JS file loaded in the browser shell. Requires browser.js.
browser_cssstringCSS injected into the browser chrome. Requires browser.css.
inject_rulesobject[]Content script rules. Requires page.inject.
soundsobjectMap of event names to audio file paths. Requires audio.ui.
newtabobjectNew tab config. Requires browser.html.
alttab_panelstringHTML for the alt-tab overlay. Requires browser.html.
background_audioobjectBackground music config. Requires audio.bg.
min_lumen_versionstringMinimum Lumen version required to run this mod.

Full Examples

Minimal CSS skin

manifest.json
{
  "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)

manifest.json
{
  "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:

javascript — index.js
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.

🚫
Permission errors are synchronous. Wrap calls in try/catch, or check lumen.hasPermission('perm.name') first.

lumen.css requires: browser.css

functionlumen.css.inject(css: string, id?: string) → string

Injects CSS into the browser chrome. Returns the injection ID. Re-injecting with the same ID replaces the previous rules in-place.

functionlumen.css.remove(id: string) → void

Removes a previously injected stylesheet by ID.

propertylumen.css.varsCSSVarsProxy

Read/write Lumen's exposed CSS variables. Changes apply immediately.

javascript
lumen.css.vars['--lumen-accent']         = '#cc5de8';
lumen.css.vars['--lumen-toolbar-height'] = '52px';
lumen.css.vars['--lumen-radius']         = '4px';

CSS Variables Reference

VariableDefaultDescription
--lumen-tab-bg#0c1220Inactive tab background
--lumen-tab-active#050810Active tab background
--lumen-tab-text#e8f4fdTab label color
--lumen-toolbar-bg#0c1220Address bar row background
--lumen-toolbar-height44pxHeight of the toolbar row
--lumen-sidebar-bg#0f1622Sidebar panel background
--lumen-accent#4dabf7Browser accent color
--lumen-borderrgba(100,180,255,.1)Default border color
--lumen-radius8pxTab/button border radius
--lumen-font'Syne', sans-serifBrowser UI font stack

lumen.browser requires: browser.js

eventlumen.browser.on(event: string, handler: Function) → Unsubscribe

Subscribe to a browser lifecycle event. Returns an unsubscribe function.

javascript
const off = lumen.browser.on('tab.open', ({ tab }) => {
  lumen.log('New tab:', tab.url);
});
off(); // unsubscribe
EventPayloadDescription
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.focusBrowser window gained focus.
lumen.blurBrowser window lost focus.
functionlumen.browser.getInfo() → Promise<BrowserInfo>

Returns Lumen version, platform, and active mod list.

lumen.tabs requires: tabs

functionlumen.tabs.list() → Promise<Tab[]>

Returns all open tabs. Each Tab: id, title, url, favicon, screenshot, active, loading.

functionlumen.tabs.activate(tabId: string) → Promise<void>

Switch to the specified tab.

functionlumen.tabs.count() → Promise<number>

Returns the number of open tabs.

functionlumen.tabs.screenshot(tabId: string) → Promise<string>

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

functionlumen.audio.on(event: string, opts: SoundOpts) → void

Register a sound hook for a browser event. Requires audio.ui.

javascript
lumen.audio.on('tab.open', {
  src: 'sounds/pop.wav',
  volume: 0.4,
  pitch: () => 0.9 + Math.random() * 0.2
});
classlumen.audio.BGPlayer — background audio controller

Controls background audio. Requires audio.bg.

javascript
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.

functionlumen.storage.get(key: string) → Promise<any>

Read a value. Returns null if the key doesn't exist.

functionlumen.storage.set(key: string, value: any) → Promise<void>

Write any JSON-serialisable value. Max 5MB per key.

functionlumen.storage.remove(key: string) → Promise<void>

Delete a key.

javascript — settings pattern
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.

eventlumen.newtab.onMount(callback: (ctx) => void)

Fired when the new tab panel is shown. Context contains canvas (WebGL type) or container (HTML type).

eventlumen.newtab.onUnmount(callback: () => void)

Fired when the panel is hidden. Clean up WebGL contexts and animation loops here.

javascript — newtab/index.html
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

eventlumen.alttab.onOpen(callback: ({ tabs }) => void)

Fired when the user triggers the tab switcher. Receives the full Tab[] array.

functionlumen.alttab.select(tabId: string) → void

Switch to a tab and dismiss the overlay.

functionlumen.alttab.dismiss() → void

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.

functionlumen.inject.addRule(rule: InjectRule) → string

Register a content script rule at runtime. Returns the rule ID.

javascript
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;
  }
}
User approval: The first time a rule matches a new origin, Lumen shows a one-time permission prompt. Subsequent visits proceed silently unless revoked.

Permission Reference

Declare every permission your mod uses in manifest.json. Lumen shows users the full list before installation.

Minimum permissions: Only declare what you actually use. Unnecessary permissions slow installs and are flagged in the store review.
PermissionChipWhat it allowsNotes
browser.cssBrowser CSSInject/override stylesheets in the browser chrome via lumen.css.*.Cannot affect webpage content.
browser.jsBrowser JSRun JS in the browser shell context. Required for any entry file.Separate from page JS worlds.
browser.htmlHTML InjectRegister HTML panels for the new tab page or alt-tab overlay.Required for newtab and alttab_panel.
page.injectPage InjectInject JS/CSS into websites per declared match patterns.Each new origin needs a one-time user approval.
mod.fsModFileSystemRead/write persistent storage via lumen.storage.*.Scoped to your mod. 50MB limit.
networkNetworkMake outbound fetch() and WebSocket calls from shell JS.No CORS bypass. Cannot send browser cookies.
locationLocationAccess geolocation — OS-provided first, IP fallback.User dialog on first use. Revocable in Settings.
audio.uiAudio UIPlay sounds tied to browser events via lumen.audio.on().Respects OS system volume.
audio.bgAudio BGPersistent background audio via lumen.audio.BGPlayer.Auto-ducks when tabs play media. One mod owns this at a time.
settingsSettingsRead or modify Lumen browser settings.Write ops show a per-setting confirmation.
tabsTab AccessRead 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

file tree
my-skin/
├── manifest.json
├── icon.png
├── preview.jpg
├── theme.css
└── index.js

theme.css — targeting the shell

css
/* 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

javascript
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.

World choice matters. 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.
javascript — inject/github.js
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.

newtab/index.html
<!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: Add a 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
  • id is unique and follows reverse-domain format
  • icon.png is 128×128, preview.jpg is 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_version set if you use post-1.0 APIs

Packaging

bash
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 network must 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 evaleval(), new Function(), and document.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
🚫
Instant rejection: Mods that collect user data without consent UI, crypto miners, ad injectors, or mods that interfere with other mods without declaring a conflict.

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 to onMount
  • CSS variable --lumen-radius exposed

SDK v1.1.0

  • Added browser.html permission and newtab / alttab_panel fields
  • Added lumen.focus and lumen.blur browser events
  • Added settings permission
  • 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.