5.4k

Capabilities

Native SDK capabilities are native OS services and app events exposed through PlatformServices, runtime methods, lifecycle events, and — for apps that embed web content — guarded bridge commands. Native code reaches them directly; web content does not receive capability access by default. In a UiApp, clipboard access rides the effects channel (fx.writeClipboard / fx.readClipboard) so update never needs a runtime handle.

Current capability pack

CapabilityZig APIJavaScript commandBridge permissionCurrent native support
Open URL in system browserruntime.openExternalUrl(url)native-sdk.os.openUrlnetworkmacOS, Linux, and Windows system WebView; macOS Chromium
System notificationruntime.showNotification(options)native-sdk.os.showNotificationnotificationsmacOS, Linux, and Windows system WebView; macOS Chromium
Native dialogsruntime.showOpenDialog(...) / showSaveDialog(...) / showMessageDialog(...)native-sdk.dialog.openFile / saveFile / showMessagedialogmacOS system WebView and Chromium; Linux and Windows system WebView
Reveal path in file managerruntime.revealPath(path)native-sdk.os.revealPathfilesystemmacOS, Linux, and Windows system WebView; macOS Chromium
Recent documentsruntime.addRecentDocument(path) / runtime.clearRecentDocuments()native-sdk.os.addRecentDocument / native-sdk.os.clearRecentDocumentsfilesystemmacOS, Linux, and Windows system WebView; macOS Chromium
Clipboardruntime.readClipboard() / runtime.writeClipboard(text) / runtime.readClipboardData(mimeType) / runtime.writeClipboardData(data)native-sdk.clipboard.readText / native-sdk.clipboard.writeText / native-sdk.clipboard.read / native-sdk.clipboard.writeclipboardtext/plain on supported clipboard hosts; text/html and RTF on macOS system WebView, macOS Chromium, Linux system WebView, and Windows system WebView
Audio playbackfx.playAudio(options) / fx.pauseAudio() / fx.resumeAudio() / fx.stopAudio() / fx.seekAudio(ms) / fx.setAudioVolume(v)None. Effects-channel API; every report arrives as an on_event Msg.None. No bridge surface.macOS system-engine host (ONE AVPlayer for local files, verified cache entries, and streamed URLs — one tappable path), Windows system WebView (one Media Foundation media session for local files and streamed URLs), Linux system WebView (one GStreamer playbin for local files and streamed URLs, loaded at runtime — a host without GStreamer reports unsupported), the iOS toolkit host (the AVFoundation player behind the embed audio service, with an honest playback audio session — interruptions pause and report), and the Android toolkit host (one MediaPlayer for local files, verified cache entries, and progressive URL streams behind the same embed audio service, with honest audio focus — a focus loss pauses and reports); macOS Chromium and GStreamer-less Linux hosts report unsupported and playback degrades to one explicit failed Msg
Audio spectrum analysisNone to call — hosts that analyze deliver .spectrum audio events through the same on_event Msg as position ticksNone. Effects-channel events.None. No bridge surface.macOS system-engine host (an MTAudioProcessingTap on the app's single AVPlayer feeding a vDSP FFT — local files, cache entries, and streams alike), Windows system WebView (process-scoped WASAPI loopback capture of THIS app's audio session only — never other apps' audio — with an in-box FFT), and Linux system WebView (GStreamer's spectrum element as the playbin's audio-filter; requires gst-plugins-good, probed live); hosts that cannot analyze — macOS Chromium, pre-2004 Windows, spectrum-less GStreamer setups, iOS, Android — report audio_spectrum unsupported and simply never send the events: honest absence, never fabricated bands
File dropsEvent.files_droppedwindow.zero.on("drop:files")None. Host-emitted event.macOS, Linux, and Windows system WebView
Credential storeruntime.setCredential(options) / runtime.getCredential(key) / runtime.deleteCredential(key)native-sdk.credentials.set / native-sdk.credentials.get / native-sdk.credentials.deletecredentialsmacOS system WebView and macOS Chromium through Keychain; Linux system WebView through Secret Service/libsecret when available; Windows system WebView through Credential Manager
App activation eventsLifecycleEvent.activate / LifecycleEvent.deactivatewindow.zero.on("app:activate") / window.zero.on("app:deactivate")None. Host-emitted event.macOS, Linux, and Windows system WebView; macOS Chromium
File associations and URL schemesManifest.file_associations / Manifest.url_schemesNone. Packaging metadata.None. Manifest-only declaration.Parsed, validated, and emitted in macOS, Linux, and Windows package registration metadata.

Unsupported platform hosts reject with the standard unsupported-service error.

Audio streaming and the track cache

fx.playAudio resolves its source in a fixed order: the local path first; when it is absent or missing, the url — where a verified cache entry at cache_path plays as a plain local file (no network), and anything else streams progressively (playback starts while bytes are still arriving, never download-then-play) while the same bytes fill the cache for the next play. expected_bytes (the track's known size, from a manifest) is the integrity gate: a cache entry or a finished download whose size disagrees never plays — it is discarded and re-streamed. While a stream waits for bytes, position events carry buffering = true: an honest third UI state, distinct from playing and paused, since the transport is not paused but nothing is coming out of the speakers. Seek and volume work mid-stream; a network failure mid-stream (or offline with a cold cache) delivers one explicit .failed Msg.

The cache lives under the platform caches directory (the app_dirs .cache convention — ~/Library/Caches/<app>/audio/ on macOS, $XDG_CACHE_HOME/<app>/audio/ on Linux, %LOCALAPPDATA%\<app>\Cache\audio\ on Windows, the app sandbox container's Library/Caches/audio/ on iOS and the app data directory's cache/audio/ on Android — both namespaces are already per-app, and both systems treat those directories as reclaimable), keyed by the URL's hash (native_sdk.audioCachePath derives the path). Clearing the cache is deleting that one directory; the OS already treats caches there as reclaimable. Hosts with a player but no streaming path report audio_streaming unsupported and URL playback degrades to the same explicit failed Msg. For record/replay, streaming changes nothing: the delivered audio events are journaled at the effect boundary like every other result, so network timing — real-world nondeterminism — stays outside the recording and replay never touches the network.

Spectrum analysis

Hosts with audio_spectrum analyze the audio the app itself is producing and deliver .spectrum events through the active playback's ordinary on_event Msg: 32 band magnitudes with log-spaced center frequencies covering roughly 50 Hz–16 kHz, each band a byte mapping linearly in decibels from the −60 dBFS analysis floor (0) to full scale (255) — divide by 255 for a 0..1 level, and equal visual steps read as equal dB steps, the way hardware analyzers do. Per band the host reports the peak FFT-bin magnitude inside the bucket. Events arrive at a steady ~25 Hz (40 ms cadence) only while audio is audibly playing: pause and stop starve the stream, and a buffering stall goes quiet with the speakers, so everything a UI derives from the bands freezes honestly. Silence while playing is a row of floor bytes, still delivered — the cadence follows the transport, the magnitudes tell the truth. The bands also describe a display, so a host that knows the app has no visible window stops delivering them while the glass is away — macOS while every window is occluded (minimized, fully covered, hidden), Windows while every window is minimized — and resumes within one report of reveal; playback and the position ticks continue untouched, and an occluded stretch journals as honest silence. Linux keeps emitting regardless: no visibility signal holds across compositors, and a gate that might never clear would freeze visible bars.

The mechanisms are host-native and in-box: macOS taps the app's single AVPlayer with an MTAudioProcessingTap and folds a vDSP FFT into the bands (the tap sits pre-effects, so the bands describe the decoded track, not the app's volume fader); Windows captures the app's OWN audio session through process-scoped WASAPI loopback — never system-wide, so other apps' audio can never pollute the bands — and runs an in-box FFT; Linux installs GStreamer's spectrum element as the playbin's audio-filter and folds its linear bands into the documented log buckets. A host that cannot analyze (macOS Chromium, Windows before 10 2004, a GStreamer install without gst-plugins-good, iOS, Android) reports audio_spectrum unsupported and never sends the events — apps should show a resting state, not fake motion. For record/replay, the bands are honest non-determinism recorded at the edge: every .spectrum event journals verbatim at the effect boundary like a position tick, so replay repaints identical bars, and the null platform ships a deterministic fake generator so tests stay hermetic.

Support discovery

Use runtime.supports(...) before native code shows optional affordances. Trusted WebView code can make the same check with native-sdk.platform.supports when that built-in command is explicitly allowed:

const support = {
  notifications: await window.zero.platform.supports("notifications"),
  dialogs: await window.zero.platform.supports("dialogs"),
  clipboardRichData: await window.zero.platform.supports("clipboard_rich_data"),
  credentials: await window.zero.platform.supports("credentials"),
  fileDrops: await window.zero.platform.supports("file_drops"),
  audioPlayback: await window.zero.platform.supports("audio_playback"),
  audioStreaming: await window.zero.platform.supports("audio_streaming"),
  audioSpectrum: await window.zero.platform.supports("audio_spectrum"),
};

Security

OS bridge commands are default-deny. Add explicit builtin_bridge entries for each command your app calls:

.builtin_bridge = .{
    .enabled = true,
    .commands = &.{
        .{ .name = "native-sdk.platform.supports", .permissions = .{ "window" }, .origins = .{ "zero://app" } },
        .{ .name = "native-sdk.os.openUrl", .permissions = .{ "network" }, .origins = .{ "zero://app" } },
        .{ .name = "native-sdk.os.showNotification", .permissions = .{ "notifications" }, .origins = .{ "zero://app" } },
        .{ .name = "native-sdk.dialog.openFile", .permissions = .{ "dialog" }, .origins = .{ "zero://app" } },
        .{ .name = "native-sdk.dialog.saveFile", .permissions = .{ "dialog" }, .origins = .{ "zero://app" } },
        .{ .name = "native-sdk.dialog.showMessage", .permissions = .{ "dialog" }, .origins = .{ "zero://app" } },
        .{ .name = "native-sdk.os.revealPath", .permissions = .{ "filesystem" }, .origins = .{ "zero://app" } },
        .{ .name = "native-sdk.os.addRecentDocument", .permissions = .{ "filesystem" }, .origins = .{ "zero://app" } },
        .{ .name = "native-sdk.os.clearRecentDocuments", .permissions = .{ "filesystem" }, .origins = .{ "zero://app" } },
        .{ .name = "native-sdk.clipboard.readText", .permissions = .{ "clipboard" }, .origins = .{ "zero://app" } },
        .{ .name = "native-sdk.clipboard.writeText", .permissions = .{ "clipboard" }, .origins = .{ "zero://app" } },
        .{ .name = "native-sdk.clipboard.read", .permissions = .{ "clipboard" }, .origins = .{ "zero://app" } },
        .{ .name = "native-sdk.clipboard.write", .permissions = .{ "clipboard" }, .origins = .{ "zero://app" } },
        .{ .name = "native-sdk.credentials.set", .permissions = .{ "credentials" }, .origins = .{ "zero://app" } },
        .{ .name = "native-sdk.credentials.get", .permissions = .{ "credentials" }, .origins = .{ "zero://app" } },
        .{ .name = "native-sdk.credentials.delete", .permissions = .{ "credentials" }, .origins = .{ "zero://app" } },
    },
},

Opening external URLs is also gated by the external-link policy:

.security = .{
    .navigation = .{
        .external_links = .{
            .action = .open_system_browser,
            .allowed_urls = &.{ "https://example.com/docs/*" },
        },
    },
},

JavaScript

await window.zero.os.openUrl("https://example.com/docs/start");
await window.zero.os.showNotification({
  title: "Build finished",
  subtitle: "native-sdk",
  body: "All checks passed.",
});
await window.zero.os.revealPath("/Users/me/Downloads/report.pdf");
await window.zero.os.addRecentDocument("/Users/me/Downloads/report.pdf");
await window.zero.os.clearRecentDocuments();

await window.zero.clipboard.writeText("Copied from Native SDK");
const text = await window.zero.clipboard.readText();
await window.zero.clipboard.write({
  mimeType: "text/html",
  data: "<strong>Copied from Native SDK</strong>",
});
const html = await window.zero.clipboard.read({ mimeType: "text/html" });

window.zero.on("drop:files", (event) => {
  console.log(event.paths);
});

await window.zero.credentials.set({
  service: "com.example.app",
  account: "current-user",
  secret: "session-token",
});
const token = await window.zero.credentials.get({
  service: "com.example.app",
  account: "current-user",
});
await window.zero.credentials.delete({
  service: "com.example.app",
  account: "current-user",
});

window.zero.on("app:activate", () => {
  refreshForegroundState();
});

See also: Builtin Commands and Security.