Apsule Project format
Distribution unit and working layout
The canonical share/import unit is a single .apsule file. It is a ZIP container with a documented internal layout. Development tools may work on the unpacked directory directly; packing must not change project semantics.
MyApsule/
├─ app.json
├─ theme.json
├─ views/
│ ├─ home.json
│ ├─ detail.json
│ └─ settings.json
├─ components/
│ ├─ glass-card.json
│ └─ item-row.json
├─ scripts/
│ ├─ main.js
│ ├─ api.js
│ └─ bluetooth.js
├─ assets/
│ ├─ icon.png
│ └─ images/
└─ data/The development folder is the source tree shared by AI, Builder and the iPhone Host. A packed .apsule contains application code/assets only: project Storage, Database, Files sandbox and Keychain data are not included. User-data backup/export is a separate feature.
Executable JSON Schemas live under schemas/. Reference projects under examples/ are validated against those schemas by npm run validate.
The shared SDK under Spec/sdk/ is the canonical tooling implementation for loading, validation, safe paths, file/content hashes, packing/unpacking, diffing and runtime compatibility checks. CLI, CI and future Builder integrations should reuse that logic instead of reimplementing the Project contract.
The cross-platform CLI is available through npm run apsule -- <command>. Current commands are create, validate, pack, unpack, inspect, diff, compatible, devices, push, run and stop. Device/sync commands use Conductor's normal loopback discovery and normalized apsule.* capabilities; they do not call Relay directly.
app.json
Recommended v1 shape:
{
"schemaVersion": "1.0",
"id": "01KAPSULEPROJECTEXAMPLE",
"name": "Example",
"entry": "home",
"script": "scripts/main.js",
"runtime": {
"minimum": "1.0"
},
"routes": {
"home": "views/home.json",
"detail": "views/detail.json",
"settings": "views/settings.json"
},
"presentation": {
"hostChrome": "visible",
"ignoresSafeArea": false
},
"permissions": {
"network": true,
"storage": true,
"bluetooth": false,
"location": "none",
"camera": false,
"microphone": false,
"photos": false,
"notifications": false,
"liveActivities": false,
"background": false,
"push": false,
"speech": false,
"contacts": false,
"calendars": false,
"reminders": false,
"motion": false,
"biometrics": false,
"nfc": false,
"health": false,
"home": false,
"localNetwork": false,
"music": false,
"authentication": false,
"weather": false,
"translation": false,
"nearbyInteraction": false,
"spotlight": false,
"wallet": false
}
}Rules
schemaVersion: version of the Apsule Project schema.id: opaque, automatically generated, stable project identifier. It is not a reverse-domain semantic contract and is not user-chosen. For safe cross-platform storage it is limited to 1–128 ASCII letters, digits,.,_, and-. Changing display name must not change it.name: user-facing name.entry: initial route.script: optional application bootstrap JavaScript.runtime.minimum: minimum Host runtime required.routes: stable route name to view JSON path.permissions: Host API families the project may request.presentation.hostChrome: whether the Host's project title/navigation chrome is visible while the project runs.hiddenis intended for immersive full-screen projects; shake-to-exit remains available.presentation.ignoresSafeArea: allows immersive project content to extend under system safe areas. System privacy indicators remain controlled by iOS.
Permission keys are additive and default to denied/none when absent. Declaring a permission only allows the Host to request/use that family after the user approves the Project permission update; it does not bypass the corresponding iOS privacy prompt or Apple signing/App Service requirement. Current boolean families are network, storage, bluetooth, camera, microphone, photos, notifications, liveActivities, background, push, speech, contacts, calendars, reminders, motion, biometrics, nfc, health, home, localNetwork, music, authentication, weather, translation, nearbyInteraction, spotlight, and wallet; location remains none | whenInUse | always.
Identity and provenance
Apsule separates identity from provenance:
userId: automatically generated, immutable Apsule user identity. A user-facing name may be chosen and changed independently.- manifest
id(project ID): automatically generated when a new project is created and never regenerated for rename, restyle or update. - importing the same project ID is an update candidate; duplicating a project creates a new project ID.
- the original author may be displayed when known.
- revisions record the authenticated user that submitted a revision when that identity is known. This is not claimed to be the human who typed every edit.
- revision/provenance metadata must be produced by the Apsule sync/tooling layer, not trusted to COS, Codex, another AI, an IDE, or hand-written project code.
- the UI may show creator, revision timestamp and submitted-by identity. Unknown provenance stays unknown rather than being guessed.
This makes the project format usable from COS, Codex, other agents, third-party IDEs and hand editing without relying on any one tool to write trustworthy metadata.
View node model
All ordinary UI nodes use a consistent shape:
{
"id": "refreshButton",
"type": "Button",
"props": {
"text": "Refresh"
},
"style": {
"padding": 16,
"glass": true
},
"events": {
"tap": {
"action": "refresh"
}
},
"children": []
}Stable IDs
Every editable node should have a stable id. This enables:
- visual-builder selection;
- AI-targeted edits;
- meaningful diffs;
- Undo/Redo;
- runtime inspection;
- future collaborative editing.
IDs should not be regenerated merely because a node moved.
Data binding
String interpolation uses expressions such as:
{
"type": "Text",
"props": {
"text": "{{state.player.name}}"
}
}Bindings may read from state, global, route parameters and supported iteration context. Arbitrary JavaScript should not be embedded inside JSON; complex logic belongs in scripts/actions.
Repetition
Collections use the ForEach component rather than duplicating nodes. The executable v1 semantics are:
{
"id": "matchRows",
"type": "ForEach",
"props": {
"items": "{{state.matches}}",
"item": "match",
"key": "id"
},
"children": [
{
"type": "MatchCard",
"props": {
"map": "{{match.map}}",
"score": "{{match.score}}"
}
}
]
}props.items must resolve to an array in runtime state or the active iteration scope. props.item names the alias available to descendants and props.key identifies a stable field on object items. If the key is absent, the Host falls back to the array index. Actions invoked from descendants receive the current iteration scope as their first argument.
Custom components
Reusable UI belongs in components/. Components accept explicit props and expand into normal view nodes.
{
"component": "MatchCard",
"props": ["map", "score"],
"view": {
"id": "root",
"type": "VStack",
"children": [
{
"id": "map",
"type": "Text",
"props": { "text": "{{props.map}}" }
},
{
"id": "score",
"type": "Text",
"props": { "text": "{{props.score}}" }
}
]
}
}Custom components are essential to prevent large projects from becoming repetitive JSON.
theme.json
Themes centralize design tokens:
{
"spacing": {
"sm": 8,
"md": 16,
"lg": 24
},
"radius": {
"card": 20
},
"typography": {
"title": {
"font": "title",
"fontWeight": "bold"
}
}
}Views may reference tokens, for example "$spacing.md". This is preferred over repeating raw values because the future Builder and AI can restyle a whole project consistently.
Preview data
The Builder should be able to supply mock state without calling real APIs:
{
"state": {
"player": {
"name": "R7",
"rank": "Diamond 2"
}
}
}Preview state is development metadata and must not silently become production persistent data.