Develop an App
Scaffold a new Aura app — manifest, lifecycle, first run.
The shortest path from idea to running iframe. Every step happens inside the OS — open a Terminal, scaffold, launch, jump in to edit.
The workflow at a glance
1. Open the Terminal app (launcher → Terminal, or Ctrl+Alt+Space)
2. aura dev new com.example.hello → wizard scaffolds the app
3. aura app start com.example.hello → backend boots
4. Click the new icon in the launcher → iframe loads
5. aura jump com.example.hello → drop into the sandbox to edit
→ run `claude` for Claude CodeSteps 1-4 are this page. Step 5 is its own page — Develop in the App Sandbox.
Scaffold with aura dev new
aura dev new com.example.helloThe wizard walks you through:
| Prompt | What it sets |
|---|---|
| Package name | Reverse-domain id, equals the directory name. Regex ^[a-z][a-z0-9]*(\.[a-z][a-z0-9]*)+$. |
| Display name | Shown in launcher, dock, Process Manager. |
| Icon | 1-3 char glyph (e.g. H, HL, HEL). Falls back to the first letter of the name. |
| Shape preset | activity (default, has a UI) · activity-bg (UI + stays alive when last window closes) · service (headless backend, no UI). |
| Instance mode | single (reuse one backend) · multi (new backend per launch). |
| Sandbox | proot (cheap, ~3 ms spawn) · container (kernel-namespace isolation, ~80-150 ms). |
| Tools | Allowlist of binaries bind-mounted into /aura/my-tools. Pick from the registry; bash and node are almost always wanted. |
The scaffold writes a minimal manifest (only the fields you actually
chose differ from defaults), the Astro config wired to
auraAppIntegration(), lifecycle stubs that call SDK factories, and a
CLAUDE.md primer for the new app.
What the scaffold ships
apps/com.example.hello/
├── app.manifest.json
├── astro.config.mjs
├── package.json
├── CLAUDE.md
└── src/
├── pages/
│ ├── index.astro
│ └── api/
│ └── lifecycle/
│ ├── onCreate.ts
│ ├── onStart.ts
│ ├── onResume.ts
│ ├── onPause.ts
│ ├── onStop.ts
│ ├── onDestroy.ts
│ └── onActivityCreate.ts (activity-mode only)You don't need to ship entrypoint.sh or health.ts — the runner
synthesises the entrypoint, and auraAppIntegration() injects the
health route. Both can still be overridden by shipping your own files.
Launch the new app
aura app start com.example.helloThe AppManager allocates a port, spawns the sandbox, runs
onCreate → onStart → onResume, then marks the instance resumed.
Click the icon in the launcher (it appears as soon as the manifest
loads) — the iframe boots and renders src/pages/index.astro.
Manifest fields that actually matter
The full schema lives in packages/core/src/types/manifest.ts. In
practice, after scaffolding you'll touch six fields. Run
aura dev clean-manifest apps/<id> to drop everything else back to
schema defaults.
{
"id": "com.example.hello",
"name": "Hello",
"instanceMode": "single", // or "multi"
"activityMode": "none", // or "multi"
"tools": ["bash", "node"],
// Optional but common:
"intentFilters": [ // declare you can handle SEND text/plain
{ "action": "aura.intent.action.SEND", "dataMime": ["text/plain"] }
],
"dataProvider": { // expose data to other apps
"authority": "com.example.hello",
"providers": [{ "path": "/api/data/notes" }]
}
}See Core Concepts for what each policy does at runtime.
Lifecycle hooks
Six plain hooks + two activity hooks. Every one is a one-line
createLifecycleHandler call from @aura/app-sdk. Add behaviour by
passing impl:
// src/pages/api/lifecycle/onDestroy.ts
import { createLifecycleHandler } from '@aura/app-sdk';
import { state } from '../../../state.js';
export const POST = createLifecycleHandler('onDestroy', async () => {
state.activities.clear();
await flushPendingWrites();
});Activity hooks return path/title for new windows:
// src/pages/api/lifecycle/onActivityCreate.ts
import { createActivityCreateHandler } from '@aura/app-sdk';
import { state } from '../../../state.js';
export const POST = createActivityCreateHandler(({ activityId }) => {
state.activities.add(activityId);
return { path: '/', title: `Hello #${activityId.split('#').pop()}` };
});Inside a hook: identity + runtime context
Hooks (and every other server-side handler in your app) can read
their runtime identity via two SDK helpers. Don't reach for
process.env directly:
import { getAppContext, readIdentityHeaders } from '@aura/app-sdk';
const ctx = getAppContext();
// ctx = { appId, instanceId, appPort, osApiBase, dataDir, layerTag }
//
// In a request handler — pull headers the proxy stamps on every
// incoming request:
export const POST: APIRoute = async ({ request }) => {
const { appId, instanceId, activityId } = readIdentityHeaders(request);
// … your logic
};activityId is only present on requests coming through an activity-
scoped iframe URL; for non-activity apps it's null.
Persisting app data
Every app instance has a private, writable directory at /data inside
the sandbox. It is bind-mounted by the OS from the scope's per-instance
data path — scopes/<scope>/apps/<appId>/<instanceId>/ on the
aura-app-data Docker volume — so anything written there survives
container restarts and OS reboots.
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs';
const DATA_DIR = process.env['AURA_DATA_DIR'] ?? '/data'; // always /data inside sandbox
const DATA_FILE = `${DATA_DIR}/mydata.json`;
// Load on module init (runs once per process lifetime)
const saved = existsSync(DATA_FILE)
? JSON.parse(readFileSync(DATA_FILE, 'utf-8'))
: { defaultValue: 0 };
// Write on every mutation
function save(value: unknown) {
mkdirSync(DATA_DIR, { recursive: true });
writeFileSync(DATA_FILE, JSON.stringify(value), 'utf-8');
}Or use getAppContext() from the SDK which resolves the same path:
import { getAppContext } from '@aura/app-sdk';
const { dataDir } = getAppContext(); // → '/data'Two storage patterns — don't mix them up
AuraOS has two distinct persistence mechanisms. Use the right one:
| Need | Use | Where data lives |
|---|---|---|
| App-private state (documents, history, counter values, scrollback) | /data filesystem | aura-app-data volume, per-instance path |
| OS-level settings (theme, keymap, workspaces, clock format) | KV store (/api/kv/os/<key>) | Redis in aura-app-data volume |
/data is per-instance, isolated, and only visible to your app.
The KV store is shared OS infrastructure — apps that write there
need the correct permission (system.kv.os.*) and the data is
visible to every app that reads the same namespace.
Do not write app state to the KV store. Use it only for OS-wide
settings that the shell or other apps need to react to. For everything
else — documents, user preferences private to your app, caches,
session state — write to /data.
/data is also distinct from /run/context: that path holds OS-managed
environment variables and secrets injected by Aura Context and is
read-only to your app. Write your own persistent data to /data.
What survives a restart
| State | Survives? |
|---|---|
Files written to /data | ✅ yes |
| KV store values | ✅ yes |
In-memory variables / globalThis singletons | ❌ no |
| Active instances and activities | ❌ no (need relaunch) |
If your app holds user-facing state in a globalThis singleton without
writing it to /data, that data is lost every time the container
restarts — either from a crash, an aura app restart, or a code
change. The reference apps Notepad, Counter, and Terminal all persist
their state to /data as working examples of this pattern.
Need a non-Astro framework?
The com.aura.docs app you're reading right now runs on Next.js +
fumadocs via runtime: 'raw'. Set runtime: "raw" in the manifest,
ship an entrypoint.sh that execs your framework on $APP_PORT,
serve /api/lifecycle/{onCreate,onStart,onResume,health} from inside
the framework, and the rest is identical to an Astro app. Until that
gets its own page, the docs app is the working reference —
apps/com.aura.docs/ in the repo.
Reference apps to learn from
| App | Pattern |
|---|---|
com.aura.terminal | Multi-instance shell + PTY over WebSocket. |
com.aura.notepad | Multi-activity shared state (one backend, many views). |
com.aura.counter | Multi×multi archetype: N instances, M activities per. |
com.aura.settings | Content provider + theme + permissions. |
com.aura.console | Background WebSocket log persistence. |
com.aura.example | Scaffold reference (minimal Astro). |
com.aura.docs | Raw runtime (Next.js + fumadocs). |
What to read next
- Publish an App — ship it to a registry so others can find and install it from the app store.
- Develop in the App Sandbox —
aura jump, Claude Code inside the sandbox, log tailing. - Core Concepts — the mental model behind every field above.
- SDK Reference — every
osClient.*method and loose helper export with a copy-paste example. - CLI Reference — every
aura <cmd>with usage and key flags.