Build apps for astraOS
astraOS apps are written in QML (Qt 6) and shipped as .apkg packages. Every app runs in its own
sandbox (its own Linux user), talks to the system through the Astra.Core QML module and draws with the
Astra.Controls design system, so it looks at home and follows the system appearance and language.
Getting started
hello/ ├── app.json the manifest ├── icon.svg square artwork; the system rounds it ├── qml/Main.qml the first screen └── i18n/it.json translations (optional)
// qml/Main.qml
import QtQuick
import Astra.Core
import Astra.Controls
Rectangle {
color: Theme.background
Page {
anchors.fill: parent
title: qsTr("Hello")
ListGroup {
ListItem { text: qsTr("Battery"); value: System.batteryLevel + "%" }
ListItem {
text: qsTr("Send a notification")
chevron: true
onClicked: Notifications.post(qsTr("Hi!"), qsTr("Sent from my app"))
}
}
}
}
app.json
{
"package": "com.example.hello", // reverse domain, unique on Market
"name": "Hello", "names": { "it": "Ciao" },
"version": "1.0.0", "versionCode": 1, // versionCode must grow with every release
"description": "A friendly app.", "descriptions": { "it": "Un'app amichevole." },
"main": "qml/Main.qml",
"icon": "icon.svg",
"background": "#000000", // splash colour while the app starts
"appearance": "auto", // "dark" for apps that are always dark (camera, video)
"webengine": true, // web content (QtWebEngine): browsers, web apps
"permissions": ["astra.permission.INTERNET", "astra.permission.CAMERA"],
"opens": ["text/plain"], // offered in "Open with" for these types
"share": ["image/*"], // offered in the share sheet for these types
"widgets": [{ "id": "now", "name": "Now", "qml": "widgets/Now.qml", "size": "small" }]
}
UI: Astra.Controls
Use the design system instead of drawing your own controls: rounded surfaces, springs and colours that follow the system theme. Main components:
Page,PageStack— large titles, back navigation and edge swipeListGroup,ListItem(switch, chevron, value, selection, icon tiles)Button,IconButton,Switch,Slider,SegmentedControl,TextFieldCard,EmptyState,ProgressBar,Spinner,PinEntry,Sheet(bottom sheet),ShareSheetIcon(system symbols),AppIcon,Theme(colours, type scale, radii, accent)
System services: Astra.Core
System battery, brightness, torch, device info, power
Settings values["key"], setValue() (apps own "app.<package>." keys)
Notifications post(title, body), dismiss(id)
Packages list(), launch(package, args), targets(mime, "share" | "open")
Application args, intent(args) signal, dataDir, mediaDir, media(filters)
Files list(dir), copy/move/rename/remove, readText/writeText (shared storage)
Permissions check(permission, cb), request(permission, cb)
Dialogs show({ title, message, buttons }, cb) -> system bottom sheet
Location state.fix { lat, lon, accuracy }, call("get", { maxAge, watch }, cb) (LOCATION)
Calendar call("list" | "add" | "update" | "delete", ...), alerts as notifications (CALENDAR)
MailClient IMAP / SMTP over TLS for mail apps (INTERNET)
Weather, Network, Telephony, Contacts, Alarms, MediaController, Language, Fonts
Permissions
Declare every permission in app.json. Normal ones (internet, vibration, alarms) are granted at install.
Runtime ones (camera, microphone, location, contacts, photos, phone, SMS, notifications, nearby devices) must be asked
for with Permissions.request(): the system shows a popup the user must answer. The answer is final; people
change it later in Settings › Apps. When a permission grants access to a device (camera, media), the system restarts
the app after it is allowed.
Permissions.request("astra.permission.CAMERA", function(granted) {
if (granted) camera.start()
})
Files and sharing
Apps with the MEDIA permission read and write the shared storage (Files.home). To share, use
ShareSheet; to receive, declare "share" or "opens" and read
Application.args ({ action: "share" | "open", files: [paths], text }), also delivered later
through Application.intent.
Light and dark
Use Theme.background, Theme.surface, Theme.text and friends: they change with
the system appearance. Theme.light tells which one is active. Apps that must stay dark set
"appearance": "dark".
Translations
Write every string in English with qsTr() and add i18n/<language>.json, keyed by the
English text. Plurals use { "one": …, "other": … }. Missing strings fall back to English.
{ "Hello": "Ciao", "%n photos": { "one": "%n foto", "other": "%n foto" } }
Lock screen widgets
Each widget is a small QML file declared in "widgets" (size small or wide);
people add it in Settings › Lock Screen.
Packaging
python3 tools/apkgtool/apkgtool.py pack hello/ -o com.example.hello.apkg python3 tools/apkgtool/apkgtool.py verify com.example.hello.apkg
apkgtool is in the astraOS source tree. Packages are reproducible and end with a SHA-256 checksum that
Market verifies on upload.
Publishing on Market
- Ask to become a developer on the home page; you receive your sign-in once approved.
- In the developer panel create the app with its package name, then upload an
.apkg. The package name and a growingversionCodeare checked, and so is the design language: Market runsastra checkon the package and refuses a version with errors, listing them; warnings are shown but don't block publishing. - Add the description, category, icon and screenshots, then publish. Updates are new uploads.
Phones install apps only from Market. People can install an .apkg file directly only after turning on
Settings › Developer Options › Sideload Apps; ADU over USB debugging always can.
From CI, upload with your session token:
curl -X PUT --data-binary @com.example.hello.apkg \
-H "Authorization: Bearer $ASTRA_TOKEN" \
"https://<server>/api/dev/apps/com.example.hello/versions?notes=Bug%20fixes"
Get a token with POST /api/auth/login and a device object (it appears in AstraID as a device
you can remove at any time).
Testing with ADU
Turn on Developer Options (Settings › About › More Settings, tap Architecture seven times) and USB debugging, then:
adu install com.example.hello.apkg adu start com.example.hello adu logcat