@gphone/sdk
    Preparing search index...

    Interface AppManifest

    interface AppManifest {
        author?: string;
        badgeStore?: Readable<number>;
        bundleUrl?: string;
        color: string;
        core: boolean;
        defaultProps?: Record<string, unknown>;
        description?: string;
        icon: string | Component<any, {}, string> | Snippet<[]> | null;
        id: string;
        installedAt?: string;
        isRemote?: boolean;
        keybinds?: AppKeybindInput[];
        name: string;
        networkHosts?: readonly string[];
        permissions?: (
            | "account"
            | "admin"
            | "call"
            | "camera"
            | "contacts"
            | "highscores"
            | "location"
            | "mail"
            | "marketplace"
            | "media"
            | "messages"
            | "notifications"
            | "reports"
            | "social"
            | "storage"
            | "app-events"
            | "app-registry"
            | "clock"
            | "devtools"
            | "display"
            | "keybinds"
            | "navigation"
            | "notification-settings"
            | "system-hardware"
            | "theme"
            | "wallpaper"
        )[];
        preload?: () => void | Promise<unknown>;
        requiresAdmin?: boolean;
        requiresNetwork?: boolean;
        updatedAt?: string;
        version?: string;
    }
    Index
    author?: string

    App author or developer team

    badgeStore?: Readable<number>

    Reactive unread count for the launcher badge — unreadMailCount and friends.

    bundleUrl?: string

    Remote bundle URL if dynamically loaded

    color: string

    Utility class for the launcher icon (from app-utilities.css) — bg-indigo-600.

    A class, not a color value. AppIcon interpolates this straight into a class attribute, so a hex string becomes a class name matching no rule and the icon renders with no background at all. The docstring here used to offer "or hex string", which is why that is worth stating outright.

    core: boolean

    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 'gPhone' 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).

    defaultProps?: Record<string, unknown>

    Default props passed when launching app component

    description?: string

    Brief description of app functionality

    icon: string | Component<any, {}, string> | Snippet<[]> | null

    A Svelte component, a snippet, or an image URL. Null renders no glyph, which is what a remote app that shipped without one gets.

    id: string

    Unique id — contacts, crypto_tracker. lower_snake_case.

    The stable one. It is a directory name, the gphone:<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.

    installedAt?: string

    ISO date string when app was installed

    isRemote?: boolean

    Flag indicating whether app was dynamically loaded from a remote bundle

    keybinds?: AppKeybindInput[]

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

    name: string

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

    networkHosts?: readonly string[]

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

    permissions?: (
        | "account"
        | "admin"
        | "call"
        | "camera"
        | "contacts"
        | "highscores"
        | "location"
        | "mail"
        | "marketplace"
        | "media"
        | "messages"
        | "notifications"
        | "reports"
        | "social"
        | "storage"
        | "app-events"
        | "app-registry"
        | "clock"
        | "devtools"
        | "display"
        | "keybinds"
        | "navigation"
        | "notification-settings"
        | "system-hardware"
        | "theme"
        | "wallpaper"
    )[]

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

    preload?: () => void | Promise<unknown>

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

    requiresAdmin?: boolean

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

    requiresNetwork?: boolean

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

    updatedAt?: string

    ISO date string when app was last updated

    version?: string

    Semantic version string (e.g. "1.0.0")