llms.txt
# Native SDK documentation
> Build native desktop applications with TypeScript and Native markup. Native SDK provides a native renderer, components, app state, and development tools.
## When to use Native SDK
- Category: Application framework
Common use cases:
- Build native desktop apps with TypeScript and Native markup
- Look up Native markup components and platform support
- Test and automate native apps
## Documentation
---
title: App Model
url: "https://native-sdk.dev/docs/app-model"
docs_index: /llms.txt
lastUpdated: 2026-10-15
---
> For an index of all documentation, see [/llms.txt](/llms.txt).
A Native SDK app is one loop with four parts:
- **Model** — a plain data structure holding all app state.
- **Msg** — a tagged union of everything that can happen.
- **update(model, msg)** — the only place state changes.
- **View** — a Native markup file (`.native`, or a Zig view function) that derives the UI from the model.
The runtime manages windows, rendering, input dispatch, timers, accessibility, and hot reload. A widget dispatches its bound message to `update`; the runtime then derives the view from the committed model and renders the changes.
Both TypeScript and Zig cores use this loop. The default project has `src/core.ts`, compiled to native code at build time. A `zig-core` project has `src/main.zig`. The wiring sections below show the runtime setup that the build generates for TypeScript apps and that a Zig app can configure directly. See [TypeScript Cores](/docs/typescript) for the TypeScript contract.
## The loop in full
A minimal counter declares state, messages, and a pure `update`:
```ts title="src/core.ts"
export interface Model {
readonly count: number;
}
export type Msg =
| { readonly kind: "increment" }
| { readonly kind: "decrement" }
| { readonly kind: "reset" };
export function initialModel(): Model {
return { count: 0 };
}
export function update(model: Model, msg: Msg): Model {
switch (msg.kind) {
case "increment":
return { ...model, count: model.count + 1 };
case "decrement":
return { ...model, count: model.count - 1 };
case "reset":
return { ...model, count: 0 };
}
}
```
```zig title="src/main.zig"
pub const Msg = union(enum) {
increment,
decrement,
reset,
};
pub const Model = struct {
count: i64 = 0,
};
pub fn update(model: *Model, msg: Msg) void {
switch (msg) {
.increment => model.count += 1,
.decrement => model.count -= 1,
.reset => model.count = 0,
}
}
```
The view in `src/app.native` binds the model and names the messages:
```html
<row gap="8" main="center" cross="center" grow="1">
<button variant="secondary" on-press="decrement">-</button>
<text>{count}</text>
<button variant="primary" on-press="increment">+</button>
</row>
```
Markup can never mutate state. `{count}` is a read; `on-press="increment"` names a `Msg` variant (in a TypeScript core, the arm's `kind`). Every state change flows through `update`, which makes the app's behavior testable as a plain function — the Zig template's generated `src/tests.zig` drives it with no GUI at all, and a TypeScript core runs under node the same way (`native dev --core`).
## Wiring
`native_sdk.UiApp(Model, Msg)` ties the loop to the runtime. A zero-config app never writes this — the build graph generates it (for a TypeScript core, over the compiled core's model) — but it is ordinary code you can own any time. From the Zig template's `main`:
```zig
const CounterApp = native_sdk.UiApp(Model, Msg);
pub fn main(init: std.process.Init) !void {
// `create` heap-allocates the multi-MB app struct and constructs the
// Model in place — neither ever rides the stack.
const app_state = try CounterApp.create(std.heap.page_allocator, .{
.name = "my_app",
.scene = shell_scene, // one window, one gpu_surface view
.canvas_label = "main-canvas", // must match the scene's view label
.update_fx = update, // update(model, msg, fx) - or .update for a pure app
.markup = .{ .source = app_markup, .watch_path = "src/app.native", .io = init.io },
});
defer app_state.destroy();
app_state.model = initialModel(); // boot state: assign through the pointer
try runner.runWithOptions(app_state.app(), .{ ... }, init);
}
```
`create` requires every `Model` field to carry a default; the model starts as `.{}` and boot state is assigned through the returned pointer. The `scene` declares the native window and its GPU surface view — see [Windows](/docs/windows) and [Native Surfaces](/docs/native-surfaces) for multi-view scenes.
## Rebuilds and widget identity
After `update`, the runtime derives the view from the committed model. Reconciliation follows these rules:
- **Widget identity is structural.** A widget keeps its id across rebuilds, reorders, and hot reloads, so engine-owned state — scroll offsets, text carets, focus — survives. List items carry `key` (or `global-key` for items that move between containers) to keep identity through reorders. Unkeyed same-kind siblings take positional identity (sibling index), so an `<if>` that inserts or removes an earlier same-kind sibling re-disambiguates the trailing ones — engine-owned state like carets and scroll can hop; keyed items and keyed ancestors hold identity.
- **The source wins.** Engine-retained state (a scroll offset, a toggle) survives rebuilds until the model asserts a different value; then the model's value applies. This is why controlled patterns echo runtime-applied values back through the model — see [State & Data Flow](/docs/state).
- **Update errors are recorded.** A returned `update` error is caught and recorded in a bounded error ring (visible in [automation](/docs/automation) snapshots as `dispatch_errors=`), and the app keeps running.
## Hot reload in development
With `watch_path` set, the runtime watches the `.native` file while the app runs. Edits preserve model state and widget identity. Parse failures keep the last good view on screen and record a diagnostic (`app_state.markup_diagnostic` carries line, column, and message).
## Compile the markup for release
In release builds the markup compiles at comptime — no parser in the binary, and markup or binding mistakes become compile errors with line and column:
```zig
const dev = @import("builtin").mode == .Debug;
const App = native_sdk.UiAppWithFeatures(Model, Msg, .{ .runtime_markup = dev });
const CompiledView = canvas.CompiledMarkupView(Model, Msg, @embedFile("app.native"));
// options:
.view = CompiledView.build,
.markup = if (dev) .{ .source = app_markup, .watch_path = "src/app.native", .io = init.io } else null,
```
Both engines produce the identical widget tree — same structural ids, same typed handler table — so tests, automation scripts, and goldens hold across dev and release.
## Hybrid views: a Zig root composing markup fragments
Compiled markup views compose the other way too: a hand-written Zig builder root can build markup fragments as ordinary children. This is the pattern for any UI that mixes custom Zig panes with declarative markup — the root places what the closed grammar cannot express (a scaled `ui.paragraph` display block, a `.band`-series `ui.chart`, per-row native context menus), and the markup keeps everything it can:
```zig
const CompiledHeaderView = canvas.CompiledMarkupView(Model, Msg, @embedFile("header.native"));
pub fn rootView(ui: *Ui, model: *const Model) Ui.Node {
return ui.column(.{ .gap = 12, .grow = 1 }, .{
CompiledHeaderView.build(ui, model), // the markup fragment, as a child
ui.paragraph(.{}, &.{
.{ .text = model.readout(ui.arena), .monospace = true, .scale = 1.6 },
}),
});
}
// options: .view = rootView,
```
`examples/calculator` is the smallest live reference (`CompiledKeypadView.build(ui, model)` inside a hand-written root); `deck` uses the same shape. Widget ids, handlers, and dispatch are identical to a pure-markup tree, so tests and automation address the fragment's widgets the usual way.
A Zig-root app keeps dev-time hot reload for its embedded fragments too: build with `UiAppWithFeatures(Model, Msg, .{ .runtime_markup = dev_markup_reload })` and pass `.markup = .{ .source = ..., .watch_path = "src/header.native", .io = init.io }` gated on `builtin.mode == .Debug` (`null` otherwise) — the wiring in `examples/notes`. Debug builds then reload edits to the watched file in place; release builds compile the runtime engine out entirely.
## Side effects
`update` stays pure by routing anything that leaves the model — subprocesses, HTTP, file persistence, timers, clipboard, desktop notifications — through the effects channel, and routed results come back as ordinary messages. In a TypeScript core, effects are `Cmd` data returned from `update` and recurring timers are declared `Sub` data — see [TypeScript Cores: Effects](/docs/typescript#effects-are-cmd-data). In a Zig core, declare `.update_fx` instead of `.update` and spawn from message arms; boot-time work goes in `.init_fx`, which runs exactly once before the first paint. See [Native UI: Effects](/docs/native-ui#effects).
## Dropping down
`UiApp` is a layer over the lower-level `App`/`Runtime` pair, which any app can use directly — for custom lifecycle callbacks, imperative window and view management, or embedding [web content](/docs/frontend). The [App & Runtime](/docs/runtime) reference documents that layer, and [Embedded App](/docs/embed) covers driving the runtime from an existing host (including iOS and Android).
---
For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)
For an index of all available documentation, see [/llms.txt](/llms.txt)
For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)
---
title: Config
url: "https://native-sdk.dev/docs/app-zon"
docs_index: /llms.txt
lastUpdated: 2026-10-15
---
> For an index of all documentation, see [/llms.txt](/llms.txt).
The `app.json` manifest declares app metadata, permissions, security rules, window layout, and packaging inputs. It is read by the CLI and tooling at build, package, and validation time. New projects use JSON so TypeScript developers get familiar syntax, completion, and inline validation through [`$schema`](https://schema.native-sdk.dev/app/v1.json). Existing `app.zon` manifests remain fully supported and expose the same fields—there is no reduced JSON feature set. The versioned URL stays compatible for the lifetime of the v1 manifest contract; `/app.json` is the current-version alias.
## Example: native-rendered app
The manifest `native init` generates — identity, one shell window with a GPU surface view, and the minimal permission set:
```json title="app.json"
{
"$schema": "https://schema.native-sdk.dev/app/v1.json",
"id": "dev.native_sdk.my-app",
"name": "my-app",
"display_name": "My App",
"description": "A counter that lives in one native window.",
"version": "0.1.0",
"icons": ["assets/icon.png"],
"platforms": ["macos"],
"permissions": ["view", "command"],
"capabilities": ["native_views", "gpu_surfaces"],
"shell": {
"windows": [{
"label": "main",
"title": "My App",
"width": 480,
"height": 320,
"views": [{
"label": "main-canvas",
"kind": "gpu_surface",
"fill": true,
"role": "Counter canvas",
"accessibility_label": "Counter",
"gpu_backend": "metal",
"gpu_pixel_format": "bgra8_unorm",
"gpu_present_mode": "timer",
"gpu_alpha_mode": "opaque",
"gpu_color_space": "srgb",
"gpu_vsync": true
}]
}]
},
"security": {
"navigation": {
"allowed_origins": ["zero://app", "zero://inline"],
"external_links": { "action": "deny" }
}
},
"web_engine": "system",
"cef": { "dir": "third_party/cef/macos", "auto_install": false }
}
```
## Example: app with web content, menus, and shortcuts
A fuller manifest for an app that also [embeds web content](/docs/frontend) and declares commands, shortcuts, menus, and packaging metadata:
```zig title="app.zon"
.{
.id = "dev.native_sdk",
.name = "native-sdk",
.display_name = "native-sdk",
.version = "0.1.0",
.icons = .{"assets/icon.png"},
.platforms = .{ "macos" },
.permissions = .{ "command", "view", "dialog", "window", "clipboard", "credentials" },
.capabilities = .{ "webview", "js_bridge", "native_views", "gpu_surfaces", "menus", "shortcuts", "dialog", "clipboard", "credentials" },
.bridge = .{
.commands = .{
.{ .name = "native.ping", .origins = .{ "zero://app" } },
.{ .name = "native-sdk.window.create", .permissions = .{ "window" }, .origins = .{ "zero://app" } },
.{ .name = "native-sdk.command.invoke", .permissions = .{ "command" }, .origins = .{ "zero://app" } },
.{ .name = "native-sdk.view.list", .permissions = .{ "view" }, .origins = .{ "zero://app" } },
.{ .name = "native-sdk.dialog.showMessage", .permissions = .{ "dialog" }, .origins = .{ "zero://app" } },
.{ .name = "native-sdk.clipboard.readText", .permissions = .{ "clipboard" }, .origins = .{ "zero://app" } },
.{ .name = "native-sdk.credentials.get", .permissions = .{ "credentials" }, .origins = .{ "zero://app" } },
},
},
.security = .{
.navigation = .{
.allowed_origins = .{ "zero://app", "http://127.0.0.1:5173" },
.external_links = .{ .action = "deny" },
},
},
.web_engine = "system",
.cef = .{ .dir = "third_party/cef/macos", .auto_install = false },
.windows = .{
.{ .label = "main", .title = "native-sdk", .width = 720, .height = 480, .restore_state = true },
},
.commands = .{
.{ .id = "command.palette", .title = "Command Palette" },
.{ .id = "app.reload", .title = "Reload" },
},
.shortcuts = .{
.{ .id = "command.palette", .key = "p", .modifiers = .{ "primary", "shift" } },
},
.menus = .{
.{
.title = "View",
.items = .{
.{ .label = "Command Palette", .command = "command.palette", .key = "p", .modifiers = .{ "primary", "shift" } },
.{ .separator = true },
.{ .label = "Reload", .command = "app.reload", .key = "r", .modifiers = .{ "primary" } },
},
},
},
.file_associations = .{
.{
.name = "Markdown Document",
.extensions = .{ "md", ".markdown" },
.mime_types = .{ "text/markdown" },
.icon = "assets/markdown.icns",
},
},
.url_schemes = .{
.{ .scheme = "native-sdk" },
},
}
```
## Fields
<table>
<thead>
<tr>
<th>
Field
</th>
<th>
Description
</th>
</tr>
</thead>
<tbody>
<tr>
<td>
`id`
</td>
<td>
Reverse-DNS bundle identifier (e.g.
`com.example.myapp`
)
</td>
</tr>
<tr>
<td>
`name`
</td>
<td>
Short machine name
</td>
</tr>
<tr>
<td>
`display_name`
</td>
<td>
Human-readable app name — the application menu, Dock, app switcher, and About panel all use it, in dev runs and packaged bundles alike
</td>
</tr>
<tr>
<td>
`description`
</td>
<td>
Optional one-line description shown in the About panel (max 256 bytes, single line)
</td>
</tr>
<tr>
<td>
`version`
</td>
<td>
Canonical
`X.Y.Z`
version with no leading zeroes — also the version the About panel shows
</td>
</tr>
<tr>
<td>
`icons`
</td>
<td>
The app icon: one square
`.png`
(1:1, ideally 1024x1024) or
`.svg`
source that packaging turns into every platform's icon artifacts — the macOS
`.icns`
gets the platform's rounded-rect icon shape applied automatically. A prebuilt
`.icns`
/
`.ico`
entry ships untouched for its platform instead
</td>
</tr>
<tr>
<td>
`platforms`
</td>
<td>
Target platforms:
`macos`
,
`linux`
,
`windows`
,
`ios`
,
`android`
,
`web`
</td>
</tr>
<tr>
<td>
`dmg`
</td>
<td>
Optional macOS DMG presentation: volume name, PNG/JPEG/TIFF background (with automatic adjacent
`@2x`
discovery), usable Finder canvas and icon sizes, simple app/Applications positions, or an explicit positioned
`items`
list of the app, Applications alias, project files/directories, and absolute links. The zero-config defaults produce a complete drag-to-Applications layout.
</td>
</tr>
<tr>
<td>
`permissions`
</td>
<td>
Runtime permissions (see
<a href="/docs/security">Security</a>
). Audio capture uses
`microphone`
and
`system_audio`
; macOS packaging emits the matching microphone, audio-capture, and screen-capture usage descriptions only when declared
</td>
</tr>
<tr>
<td>
`capabilities`
</td>
<td>
Feature declarations (see
<a href="/docs/security">Security</a>
).
`"store"`
links the engine-owned record store;
`"sqlite"`
links relational SQL effects. They share one capability-shed SQLite object but use separate databases; see
<a href="/docs/record-store">Record Store</a>
and
<a href="/docs/sqlite">Relational SQLite</a>
</td>
</tr>
<tr>
<td>
`dock_visible`
</td>
<td>
Initial macOS Dock and app-switcher presence. Defaults to
`true`
. Set
`false`
for an Accessory/menu-bar app; the policy is applied before startup windows are created, so no Dock tile flashes. Accessory apps must declare the
`"tray"`
capability. Runtime
`Cmd.setDockPresence`
can still promote or demote the app later.
</td>
</tr>
<tr>
<td>
`persist`
</td>
<td>
Engine-owned Model snapshot config: monotonic
`version`
, optional
`debounce_ms`
, and the
`restore`
Msg routes (
`ok`
/
`none`
/
`err`
). Requires
`"persist"`
in capabilities — see
<a href="/docs/persistence">Model Persistence</a>
</td>
</tr>
<tr>
<td>
`images`
</td>
<td>
Optional registered-image budget:
`.images = .{ .max_image_pixel_bytes = 8_388_608 }`
. The default is 1 MiB and accepted values are 1–8 MiB. Encoded photos decode aspect-preservingly to fit; storage is lazy per used slot, but 16 fully used 8 MiB slots are a declared 128 MiB high-water. See
<a href="/docs/dynamic-images">Dynamic Images</a>
.
</td>
</tr>
<tr>
<td>
`bridge`
</td>
<td>
Bridge command policies (see
<a href="/docs/bridge">Bridge</a>
)
</td>
</tr>
<tr>
<td>
`security`
</td>
<td>
Navigation and external link policies (see
<a href="/docs/security">Security</a>
)
</td>
</tr>
<tr>
<td>
`web_engine`
</td>
<td>
`system`
or
`chromium`
; Chromium is currently supported for macOS builds (see
<a href="/docs/web-engines