🚧 The Passport Prime SDK is in public beta, currently 1.0.0-beta.1. Got an idea, or want a dev unit to play with? We'd love to hear from you, hello@foundation.xyz .
What is an app on KeyOS?
An app is a Rust binary crate that runs as an isolated process under the Xous microkernel. Each app gets its own address space, its own storage scope, and its own declared permissions, all enforced by the kernel. There is no runtime escalation path.
Apps compile to the armv7a-unknown-xous-elf target, are signed with cosign2, and are installed under apps/<app-name>/ on the device.
Project layout
my-app/
├── .foundation-sdk/ # symlink to the active SDK, see below
├── app-config.toml # app identity, publisher, permissions, theme
├── Cargo.toml
├── build.rs # Slint code generation (router, exports, i18n)
├── src/
│ ├── main.rs # entry point
│ └── theme.rs # applies your theme, follows the system light/dark setting
├── ui/
│ ├── app.slint # top-level UI component
│ ├── callbacks.slint # callbacks shared between Rust and Slint
│ └── pages/ # per-page components (multi-page-app template)
├── resources/
│ ├── icon.svg # launcher icon
│ ├── theme.json # your app theme
│ ├── images/ # app images, reached from Slint via Images.image("name")
│ └── fonts/
└── i18n/
└── en.json # translation strings
foundation new my-app scaffolds this layout for you. Three templates ship with the SDK:
default-app, single-page startermulti-page-app, router-based multi-screen starterkitchen-sink, a gallery of every@uicomponent in each variant, state, and size. The fastest way to see what a theme change actually looks like
Two things about this layout are worth calling out:
.foundation-sdk/is a symlink to the active SDK, and it is how yourCargo.tomlreaches the SDK crates. Because it is a symlink rather than a hardcoded version path, your project keeps building after an SDK update. It is generated, so it stays out of version control.resources/must be committed. Slint reaches images through it at build time, so an app whoseresources/is missing from the repo will not build for anyone else.
app-config.toml
This is the source of truth for your app's identity, publisher metadata, required permissions, and signing configuration. Every field maps one-to-one onto an internal AppConfig struct, no hidden defaults.
app-name = "my-app"
friendly-app-name = "My App"
launcher-app-name = "My App"
description = "Example application"
icon = "resources/icon.svg"
app-id = "0x00112233445566778899aabbccddeeff"
version = "0.1.0"
min-keyos-version = "1.0.0"
theme = "resources/theme.json"
[publisher]
name = "Example Company"
contact-email = "support@example.com"
support-url = "https://example.com/support"
[permissions]
"os/settings" = ["GetDeviceName"]
Field reference
| Field | Required | Notes |
|---|---|---|
app-name | ✅ | Cargo package name and build output directory name |
friendly-app-name | ✅ | Display name |
launcher-app-name | , | Falls back to friendly-app-name |
description | ✅ | One-line description |
icon | ✅ | Path relative to the project root; validated at build time |
app-id | ✅ | 0x-prefixed even-length hex; must be unique on the device |
version | ✅ | Semver |
min-keyos-version | ✅ | Minimum KeyOS version your app requires |
theme | ✅ | Path to your app theme JSON, resources/theme.json in new projects |
signing-identity | , | Selects an identity under ~/.foundation/signing/<name>/. See the note below before committing this |
cosign2-config | , | Explicit path to a cosign2.toml; overrides identity resolution. See the note below before committing this |
[publisher] | ✅ | name, contact-email, support-url |
[permissions] | ✅ | Per-service entries: "os/<service>" = ["MethodA", "MethodB"] |
Leave signing-identity and cosign2-config out of a repo you publish. Both point at signing material on one particular machine, so committing them leaks local detail and, more practically, anyone else who clones your project cannot build it: they do not have your key, which is the whole point. With neither key set, the build resolves an identity itself and prompts you to choose when a machine has more than one, which is the behaviour you want for a public repo.
The UI: Slint with a curated component library
KeyOS apps use Slint
for UI. The SDK exposes a stable @ui/... import surface, a curated library of Foundation-designed components, theme tokens, fonts, and icons. Your app imports from @ui and inherits a polished, on-brand starting point that you're free to fully restyle.
A minimal ui/app.slint:
import { BaseWindow } from "@ui/widgets.slint";
import { Button } from "@ui/widgets.slint";
export component AppWindow inherits BaseWindow {
Button {
text: "Hello, Passport Prime";
clicked => { debug("button pressed"); }
}
}
Components available from @ui/...:
- Form controls,
Input,Checkbox,RadioButton,Slider,Dropdown,Switch,Pagination,SegmentedSelector - Navigation,
Drawer,Dialog,ModalWindow,PopupMenu - Display,
Card,Chip,Badge,Button,IconButton,Progress, QR code renderer - Specialized,
AuthWidget,CryptoFiatField,SeedWords,PinEntry,SlideToButton,FileList - Theming,
ColorPicker,CircularProgress,Shimmer, full design tokens
Live-preview any .slint file with foundation preview ui/app.slint.
A minimal src/main.rs
The app_ui2! macro from slint_keyos_platform wires your top-level AppWindow into the KeyOS runtime. Your root component must be export component AppWindow inherits Window:
// SPDX-License-Identifier: Apache-2.0
use slint_keyos_platform::app_ui2;
app_ui2!("My App");
fn app_main(cx: AppContext, ui: AppWindow) {
log_server::init_wait(env!("CARGO_CRATE_NAME")).unwrap();
log::set_max_level(log::LevelFilter::Info);
log::info!("My App starting");
// The Slint event loop runs automatically inside app_ui2!()
// Wire up UI callbacks, spawn tasks, subscribe to IPC events here.
}
AppContext gives you gui, fs, router, config, and set_input_handler. The macro also binds the Images global for you, so images under resources/images/ are reachable from Slint as Images.image("name").
Theming
Your app ships a theme, declared in app-config.toml as theme = "resources/theme.json". The theme file itself is small: an id, a name, and the base theme it inherits from.
{
"id": "app_theme",
"name": "My App",
"parent": "base_theme"
}
Add a tokens section to override colours, spacing, typography, and control sizing, for light and dark independently. A components section can override individual components per variant, state, and size.
The generated src/theme.rs applies it and follows the system light/dark setting:
foundation_themes::apply_theme!(ui, app_theme::theme(), scheme);
// App-specific overrides go after this line
You can edit the JSON by hand, or open the visual editor:
foundation theme
foundation build and foundation sim compile the theme for you. For a brand palette shared across several apps, create a named theme with foundation themes new <brand> --from base_theme, fill in its tokens, and point your app's parent at it.
An unrecognised token key is dropped silently rather than reported, so a typo simply does nothing. If a change does not show up, check the key name before assuming the theme did not apply.
The launcher icon
Your icon lives at resources/icon.svg and has two hard requirements:
- Exactly 110×110 px. The build refuses anything else.
- A transparent-background glyph. The launcher draws the ring and disc behind your icon itself, so any background of your own, including a rounded rectangle, will show as a second shape on top of it. The build does not check for this, so it is worth looking at before you ship.
To supply a different icon for dark mode, add a -dark sibling next to it, for example resources/icon-dark.svg, following the same rules.
Build, simulate, preview
From inside an SDK shell (foundation develop):
foundation new my-app # scaffold from template
cd my-app
foundation preview ui/app.slint # live Slint preview, UI only
foundation sim # hosted simulator (debug)
foundation build # release-quality signed hardware build
foundation sideload # build + upload to Passport Prime + launch
foundation pack # single .app file to hand to other people
That ladder runs from the fastest feedback to the most complete: preview for UI work, the simulator for behaviour, hardware when it matters, then pack when you want to give the app to someone else.
Run foundation preview after a first build or sim, so it picks up your compiled theme rather than the SDK default.
After any SDK update, run foundation clean && foundation build in each app, then foundation doctor. Also restart any shell or editor session you started from inside foundation develop, or it will keep pointing at the previous SDK and quietly build against it.
See the CLI Reference for every flag.
Signing
Every app on Passport Prime is signed. foundation build signs app.elf in place with cosign2 using a secp256k1 key.
One-time identity setup:
foundation cert gen "My Company"
This writes four files to ~/.foundation/signing/My Company/:
private.pem, secp256k1 private key (keep secret)public.pub, compressed public key hexMy Company.crt, self-signed X.509 code-signing certificate, named after the identity so several certificates are easy to tell apart side by sidecosign2.toml, the configfoundation buildconsumes
Publish the certificate's fingerprint (foundation cert fingerprint) on your official website or GitHub, so users can verify a certificate really is yours before they allow it on their device.
Identity resolution order at build time:
cosign2-configinapp-config.toml(explicit path)signing-identityinapp-config.toml(name under~/.foundation/signing/)- An identity whose name matches
[publisher].name - The only configured identity, if exactly one exists
- Interactive prompt (errors in non-interactive contexts)
Passport Prime's app launcher verifies the signature against the embedded certificate chain before starting your app. Tampered or unsigned binaries are refused.
Registering your developer certificate on Passport Prime
Passport Prime only runs apps signed by a publisher it has been told to allow. Registering your certificate is what makes your own builds runnable on your device.
foundation cert install "My Company"
The name is optional and defaults to the publisher of the app you are in. The command:
- Resolves the identity and reads its certificate
- Prints the certificate fingerprint, along with a warning that Foundation has not verified this publisher
- Asks you to confirm, so allowing a publisher is always a deliberate act
- Connects to the device over the USB debug interface and installs the certificate
Passport Prime must be unlocked, connected over USB, with Developer Mode enabled, and no other process may be using the USB debug interface.
Allowing a publisher means every app signed by that publisher will run on your Passport Prime. Only allow a certificate you generated yourself, or one whose fingerprint you have checked against the publisher's official website or GitHub. Foundation does not vet third-party publishers.
Because trust is tied to the publisher key rather than to an individual build, keep your signing identity stable across releases. Re-signing an app under a different key makes the device treat it as a different publisher, and the user has to remove the installed app first, which takes its stored data and granted permissions with it.
Toolchain
| Component | Detail |
|---|---|
| Rust toolchain | Nightly, pinned by the SDK |
| Target triple | armv7a-unknown-xous-elf |
| Build flags | RUSTFLAGS="--cfg keyos -C relocation-model=pic -C link-arg=-pie" for hardware, --cfg keyos for simulator |
| Signing | cosign2 (secp256k1 + X.509) |
| Build orchestration | cargo + foundation CLI (cargo xtask for SDK maintainers) |
| Environment | Nix flake (foundation develop enters it) |
You don't install Rust or manage the cross-compiler manually, foundation develop hands you a shell with everything already in place.
Next
- CLI Reference , full command and flag reference
- Capabilities , services your app can declare and call
- API Reference , full rustdoc for the generated KeyOS API crates
- KeyOS, Permissions , the security model behind the manifest