OptionalauthorOptionalbadgeReactive unread count for the launcher badge — unreadMailCount and friends.
OptionalbundleRemote bundle URL if dynamically loaded
tile flattened to the class string every consumer interpolates. Derived — do not
author it.
Still here because ten-odd call sites read it (AppIcon, Launcher, Dock,
AppDrawer, FolderPopup, DragGhost, ToastHost, the Store and Settings listings)
and because an add-on bundle published before tile existed still sets it. defineApp
derives whichever of the two was not given, so both are always present and always
agree; writing both is refused rather than merged.
Does this app ship with the phone, and is it therefore not uninstallable?
Required, and deliberately not inferred. It was once isSystem, defaulted from
author — a display string — so an app whose author was anything other than
'Community' silently became unremovable, and naming yourself 'micaOS' was enough to
do it. Two things followed from deriving a protection boundary instead of stating it:
the derivation was circular (isSystem read author, then author read isSystem),
and a second, subtly different copy of it grew in the Store — so the registry and the
uninstall button could disagree about the same app, and the button threw.
A remote app is never core. defineApp forces false when isRemote is set and
throws on an explicit core: true beside it: a downloaded bundle asking for protection
is broken or hostile. That closes a footgun rather than preventing an attack — an
add-on runs in the shell's own JS context regardless (§7).
OptionaldefaultDefault props passed when launching app component
OptionaldescriptionBrief description of app functionality
OptionaldevicesWhich devices this app appears on (MICA-260). Absent means ['phone'].
A visibility contract exactly as requires is: an app whose list lacks the device
on screen has no icon on it, in the launcher, the dock, the drawer, a folder or
search, and openApp refuses it. Nothing about the server changes.
Listing 'tablet' says the app is usable at 1280x800, not merely that it loads:
the shell renders tablet.svelte beside index.svelte when the app ships one and
index.svelte stretched to the frame when it does not. useDisplay().device says
which frame the app is in, for the app that wants one root and two layouts.
defineApp supplies no default, for the reason requires has none: absent is the
only thing a bundle published before this field existed can say, and it has to keep
meaning what it always did — a phone app.
A Svelte component, a snippet, or an image URL. Null renders no glyph, which is what a remote app that shipped without one gets.
Unique id — contacts, crypto_tracker. lower_snake_case.
The stable one. It is a directory name, the mica:<id>: storage namespace, the
<app> segment of every net event, a keybind claim and the ?app= deep link. Renaming
it is a data migration, not a rename, which is exactly why it is not derived from
name: a display string should be free to change without orphaning stored data or
altering event names that server code handles.
OptionalinstalledISO date string when app was installed
OptionalisFlag indicating whether app was dynamically loaded from a remote bundle
OptionalkeybindsDefault hotkeys this app wants, shown grouped under it in Settings > Shortcuts. Only apps for which this is set contribute app-scoped keybinds; most apps omit it.
Display name on the home screen. Cosmetic — the launcher label, the Store listing and the error-boundary message, nothing that is keyed on.
Optional on input. defineApp title-cases the id when it is omitted, so crypto_tracker
becomes "Crypto Tracker" and all twelve apps in this repo needed only the id. Give it
explicitly when the display name is not simply the id — "GPS", "My Bank".
OptionalnetworkMICA-24: the exact origins a core: false add-on's sandboxed frame may fetch().
The frame has no Content-Security-Policy otherwise, so an add-on's own JS — not just
the code an author wrote, but anything a supply-chain compromise slipped into its
bundle — could silently call out to any host on the internet. srcdoc.ts turns this
list into the frame's connect-src; an empty or absent list means connect-src 'none', blocking outbound fetch() entirely.
Declaring this without also declaring requiresNetwork: true is refused by
defineApp — asking for real network egress while saying the app does not need
network at all is a contradiction, not an oversight to let through quietly. The
reverse is fine and common: an app can need requiresNetwork (in-game cell signal,
for its ordinary server-proxied calls) without ever calling fetch() itself, and
should simply leave this empty rather than declare a host it does not use.
Full origins only (https://api.example.com, no path) — defineApp rejects
anything else, since a bare hostname or a wildcard is not a valid CSP source and
srcdoc.ts does no correction of its own.
OptionalpermissionsWhat this app reaches for, as shown to a player in the Store.
A disclosure, not only a sandbox. A core: true app runs in the shell's own JS
context, so nothing stops it importing a hook it did not declare; a core: false
add-on runs in a sandboxed iframe and has the shell re-check every call. Either way
§2.9 is the real boundary underneath: the server gates privileged actions and does not
take a NUI request as proof of intent.
What is enforced is that the disclosure is true. sdk/permissions.test.ts reads
each app's SDK imports and fails the build if it reaches for contacts, photos, the
camera, notifications or local storage without saying so. Declaring more than the scan
finds is allowed — network and location have no hook to infer them from.
OptionalpreloadLoad this app's data while the phone opens, before the launcher draws.
Declared by the app rather than listed by the shell. bootstrap.ts used to name each
store by hand, and nothing connected that list to the apps it was loading for — so an
app that shipped a badgeStore and was forgotten there showed a stale badge until
somebody opened it, which is precisely when a badge no longer matters.
Only for what has to be right before first paint. Everything else belongs in
onAppForeground, which reloads per visit; this runs once per phone-open.
OptionalrequiresServer capabilities this app cannot function without. Absent means it needs none.
A visibility contract, not a security boundary — the same caveat requiresAdmin
carries, and worth restating rather than inferring. All this does is keep the icon
away from a phone where the app could not work; the server still refuses every action
it would have refused anyway (§2.9), and an app hidden here is not a disabled server
endpoint. A modified client can still emit whatever it likes.
Not permissions, which is a disclosure of what the app reaches for, shown to a
player before they install it — that is a statement about the app, this is a statement
about what the deployment can do. Not requiresNetwork either: that blocks the app at
runtime, behind a "Not Network" screen, because cell signal comes back. A capability
the server does not have is not a transient condition, so the honest outcome is
absence rather than an app that is present and always errors.
Absent is the only thing a bundle published before this field existed can say, and it
means exactly the right thing: this app needs nothing of the server beyond what every
app needs. defineApp supplies no default, so nothing already installed changes
behaviour and no consumer has to distinguish undefined from [].
OptionalrequiresHide the app entirely unless the player holds an admin ace.
A visibility hint, not a permission. The server gates every privileged action it exposes; this only stops the icon appearing for everyone else.
OptionalrequiresDoes this app need the NUI bridge to work at all?
Used to be inferred from permissions.includes('network') — but network disclosed
nothing (every app talks to its own service, so marking it on everyone would mean
marking it on no one) and conflated two different questions: what a player should be
told an app reaches for, and whether the phone should block the app while signal is
out. This is the second question, stated on its own rather than smuggled through the
permission list. Shell.svelte's "Not Network" gate reads this, not permissions.
OptionalsdkMICA-196: the SDK_CONTRACT_VERSION this bundle was compiled against.
sdk/version.ts published that number for an add-on to read and act on, and said
plainly what it did not do: "nothing consults it at install or boot time. There is no
runtime compatibility gate anywhere in the phone (MICA-173), and adding one is new
manifest surface that needs deciding on its own." This is that surface, decided.
Without it, an add-on built against a future contract installs cleanly and fails
wherever it first touches something that moved — a missing export, a renamed prop —
as an ErrorBoundary crash a player reads as "this add-on is broken". The version was
knowable at the bundle's build; nothing was carrying it across.
Checked for equality, not order. SDK_CONTRACT_VERSION is typed string and
deliberately carries no ordering — its own doc explains why a literal type there would
turn a comparison into a type error — so "newer" and "older" are not questions this can
ask. What it can say is whether the phone is running the contract the bundle was built
for, and a bundle built for a different one is refused at install and again at hydrate.
Absent means "did not say", and is never refused. Every add-on published before
this field existed omits it, and a bundle built by a third-party bundler that never
heard of __MICA_VERSION__ omits it too. Declaring it is how an add-on asks to be
refused early and legibly rather than late and confusingly.
OptionalservicesMICA-196: the server services this app owns, named rather than inferred.
useService(id) is the generic door to a server service and the id is the caller's to
choose, so something has to decide whose service an add-on may name. Until this field
that decision was a string prefix — id === appId || id.startsWith(appId + '_') — over
a flat namespace that already holds blabber_dms and phone_call_log. Two apps
therefore reach the same service whenever one's id is the other's prefix: Blabber
reaches blabber_dms by the prefix rule, and an add-on that simply takes the id
blabber_dms reaches it by owning it. Neither is a bug in either app; the rule cannot
tell them apart, because it derives ownership from spelling instead of reading it.
So an app states what it owns, and the registry refuses an install whose claim collides with an installed app's — that check is what makes the declaration worth anything, and it is why this is not merely documentation.
Every entry must be the app's own id or sit under <id>_. That is the same namespace
the prefix rule enforced, kept deliberately: this field exists to make a claim
checkable, not to let an app claim more than it could before. A declaration outside
it is refused here, at definition time, where the author can see it.
Absent is not "owns nothing". Every add-on published before this field existed says
nothing, and for those the old prefix rule still answers — see IframeHostServer's
serviceAllowed. Refusing them, or reading absence as an empty list, would break every
one of them, which is exactly the failure @mica/sdk is built to avoid. Declaring it
is how an app opts into the stricter, checked answer.
The launcher tile: what it is painted, and what the glyph on it inherits.
The authored field (MICA-91). It replaces a free-form color string that held both
roles at once, positionally and unchecked — which is what let a tile and its glyph
disagree, and what MICA-88 had to go and reconcile by hand across sixteen apps.
OptionalupdatedISO date string when app was last updated
OptionalversionSemantic version string (e.g. "1.0.0")
OptionalwidgetMICA-245: what this app offers the home screen besides its icon. Absent means nothing.
sizes is the whole declaration for a core: false add-on: its bundle hands a widget
root to bootAddOn beside the app root, and the shell boots it in a sandboxed frame of
its own under exactly the permissions, grant and facet table the app runs with. load
is for a core: true app only — the widget component, lazily, as the registry loads the
app's own entry — and defineApp refuses it on an add-on, whose code the shell never
imports. sizes is non-empty, repeats nothing, and names only '2x1' and '2x2'.
App author or developer team