Build a Möbius app
Reference for an agent or human authoring a new Möbius mini-app. Concrete contract first, schema and patterns below. Not a tutorial — runs from the assumption that the reader is already inside a Möbius session and asking "what does this file need to contain."
Install model
A Möbius app is a public git repo with one required file:
mobius.json at the repo root. The Möbius App Store
mini-app installs from the manifest URL:
https://raw.githubusercontent.com/<org>/<repo>/main/mobius.json
Pasting that URL into the App Store's "From URL" tab installs the app end-to-end (manifest fetch → permission preview → JSX compile → cron registration → storage seed). Sharing an app means sharing the URL. There is no submission queue and no curated-list gate — the catalog on this site is a hand-picked starter pack, not a registry.
Required files
| Path | Required | Purpose |
|---|---|---|
mobius.json |
yes | Manifest. Validates against mobius.schema.json. |
index.jsx |
yes | Single-file React entry. Default-exports a component that
receives { appId, token }. |
icon.png |
recommended | Any size square PNG. Server center-crops + resizes to 1024×1024. |
fetch.sh / job script |
only if cron | Referenced by schedule.job. Installed to
/data/apps/<slug>/ and wrapped by
init-cron-scaffold.sh. |
README.md + LICENSE |
recommended | Not used by the runtime. Helpful for humans browsing the repo. |
Manifest contract
Full reference + JSON Schema at /spec. The minimum viable manifest:
{
"id": "my-app",
"name": "My App",
"version": "1.0.0",
"description": "One-sentence pitch.",
"author": "your-github-handle",
"license": "MIT",
"homepage": "https://github.com/your-handle/app-my-app",
"entry": "index.jsx",
"icon": "icon.png",
"permissions": {
"cross_app_access": "none",
"share_with_apps": "none"
},
"runtime": {
"imports": ["react", "react-dom"],
"esm_deps": []
}
}
Additional fields when the app needs them:
offline_capable: true— opt the app into the service-worker frame cache + thewindow.mobius.storageoutbox. The runtime queues writes when offline and replays them when the network returns.storage_seeds— initial files written to per-app storage on first install. String values are paths in the repo (fetched at install time); object values are inline JSON literals.schedule— declare a cron with{"default": "0 10 * * *", "user_configurable": true, "job": "fetch.sh"}. The job script runs server-side with the long-lived service token; treat it as a worker, not interactive code.runtime.esm_deps— extra ESM modules the app loads from esm.sh at runtime. Surfacing them here lets the App Store warn before install.
Per-app storage
Per-app data lives at /api/storage/apps/<appId>/<path>.
Two ways to use it:
// Preferred: the offline runtime queues writes when offline.
// Available when manifest.offline_capable === true.
const native = window.mobius?.storage;
if (native) {
const cfg = await native.get('config.json'); // null when 404 OR offline
const res = await native.set('config.json', cfg); // {synced:true} | {queued:true}
const n = await native.pendingCount(); // outbox depth
}
// Fallback: direct fetch (works whether or not offline_capable is set).
const cfg = await fetch(`/api/storage/apps/${appId}/config.json`, {
headers: { Authorization: `Bearer ${token}` }
}).then(r => r.ok ? r.json() : null);
await fetch(`/api/storage/apps/${appId}/config.json`, {
method: 'PUT',
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify(cfg)
});
Reads from the offline runtime hit the network — there's no read cache. An app that wants to render persisted data offline must mirror the read result in component state on mount and keep it there for the session.
Theme tokens
All colors and fonts come from CSS custom properties painted by the Möbius shell. Hardcoded hex values stay frozen in the original theme when users switch.
| Token | Use for |
|---|---|
--bg | App root background. |
--surface / --surface2 | Card and panel backgrounds (two depths). |
--text | Primary text. |
--muted | Secondary / metadata text. |
--border | Hairlines, dividers, card borders. |
--accent | Primary action / selection. |
--font | Body font stack. |
Sandbox constraints
window.confirm/alert/promptsilently no-op (sandbox excludesallow-modals). Build in-app modals instead.- Raw HTML rendered via
dangerouslySetInnerHTMLmust be sanitised with DOMPurify (or equivalent) when the source isn't guaranteed to be the app's own escaped output. - Service workers are owned by the Möbius shell. Apps don't register their own SW.
Integrating with the shell's back-stack
Apps with a "deeper" view (detail screen, modal, file drawer)
should integrate with the platform back-stack via the
moebius:nav-push / nav-pop / nav-back
postMessage protocol. Without integration, a device-back gesture
pops the whole app instead of the deeper view. Pattern (lifted
from klix-filter in prod):
// Open a deeper view
const requestId = `np-${Date.now()}`;
window.parent.postMessage(
{ type: 'moebius:nav-push', label: 'detail', requestId },
window.location.origin
);
// Wait for moebius:nav-push-ack with the same requestId, then render.
// Close it
window.parent.postMessage(
{ type: 'moebius:nav-pop' }, window.location.origin
);
// Listen for the shell handing the back-press to the app
window.addEventListener('message', (ev) => {
if (ev.origin !== window.location.origin) return;
if (ev.data?.type === 'moebius:nav-back') {
// user pressed back / swiped — close the deeper view, do NOT echo nav-pop
}
});
Cron jobs
A manifest declaring schedule ships a job script in
the repo. The runtime copies the script to
/data/apps/<slug>/<job> on install and
installs a crontab entry that runs it non-interactively. The
script gets:
$1= the app's numeric id.API_BASE_URLenv var pointing at the local backend (typicallyhttp://localhost:8000).- A long-lived service token at
/data/service-token.txt(chmod 600). - A log directory at
/data/cron-logs/.
Job scripts run as the mobius user. They can shell
out to the claude or codex CLI for
AI-driven background work (see app-news/fetch.sh as a
worked example).
Versioning
- Semver. Patch = code-only (hot rebase). Minor = backwards- compatible new schema fields. Major = breaking manifest changes.
- The App Store detects updates by comparing
manifest.versionto the installed App row'sversion. Bumping the version is what makes "Update" appear next to the app card.
Sharing an app
No registration step. The manifest URL is the install link. Common hosting paths:
- GitHub public repo →
https://raw.githubusercontent.com/<org>/<repo>/main/mobius.json - Codeberg / GitLab → equivalent raw URL.
- Personal site → any HTTPS URL serving valid manifest JSON plus the referenced files reachable from the same origin's raw paths.
The Möbius App Store recognises raw.githubusercontent.com,
codeberg.org, git.sr.ht, and
gitlab.com as soft-trusted hosts. Other hosts trigger
a "this manifest is from an unfamiliar source" warning in the
install confirm modal — the install still succeeds; the warning
is UX, not a gate.
Curated catalog
The five apps on mobius-os.github.io/apps are a hand-picked starter set. The catalog is currently not accepting external submissions. Apps shipped by other authors are installable via paste-a-URL just like the curated ones — they're simply not listed here.