Möbius OS

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

PathRequiredPurpose
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:

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.

TokenUse for
--bgApp root background.
--surface / --surface2Card and panel backgrounds (two depths).
--textPrimary text.
--mutedSecondary / metadata text.
--borderHairlines, dividers, card borders.
--accentPrimary action / selection.
--fontBody font stack.

Sandbox constraints

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:

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

Sharing an app

No registration step. The manifest URL is the install link. Common hosting 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.