WAID - one codebase, every platform

Write Vite, get every platform.

Write one ordinary Vite + React app and ship it everywhere - a real static website plus native desktop, iOS and Android apps, all GPU-rendered by the Rune runtime. No WebView, no rewrite. The same app, on every screen.

  • app.example.com

    counter.app

    3

    Real DOM - no runtime

  • app

    counter.app

    3

    GPU via Rune (wgpu)

  • counter.app

    3

    GPU via Rune (Metal)

  • 9:41

    counter.app

    3

    GPU via Rune (Vulkan)

One App.tsx, four artifacts. Tap Count up on any device - they share the exact same build. The web frame is real DOM; the others are GPU-rendered by Rune, no WebView.

A phone held in one hand running a mobile app
One app across devices. Photo is illustrative.

One app, every platform

One Vite app becomes web, desktop, iOS and Android.

You write one ordinary web app. WAID hands back a real website plus native apps for every screen - no browser bundled inside, no separate codebase to maintain.

Web - live

A real static website

The same source builds a standard static site. Deploy the dist to any CDN or nginx - no runtime, no shell.

iOS - live

A native iOS app

Your dist plus the Rune runtime, drawn with Metal. A signed device build, not a web page in a wrapper.

Desktop - landing

A native desktop app

A packaged desktop bundle rendered on the GPU with wgpu. Landing as the runtime publishes.

Android - landing

A native Android app

A generated Android app rendered with Vulkan / GLES. Landing as the runtime publishes.

Rendering

GPU-native, no WebView

Native targets are drawn on the GPU by Rune, never a browser in a shell - so they look the same across devices.

Your code

No rewrite to learn

You write an ordinary Vite + React + Tailwind app. There is nothing WAID-specific in your source.

One linear build

One dist in, native artifacts out.

The build is a straight line, not a fork. Your Vite + React dist goes in, waid build embeds the Rune runtime and rasterizes your icons per platform, and signed native artifacts come out.

Input

Vite + React dist

Your ordinary static build

waid build

Embed + rasterize

Rune runtime in, icons per platform

Output

Signed native artifacts

Desktop, iOS and Android

One codebase

One codebase, every platform.

You keep a single ordinary Vite + React + Tailwind app. The same source becomes a website and native apps for desktop, iOS and Android - so there is one thing to build, test and ship, not four. Every target renders the same app, so what you see on one screen is what ships on the rest.
  • One source of truth - no per-platform fork to keep in sync
  • The web build is a real static site, deployable anywhere today
  • Native targets are GPU-rendered by Rune, not a browser in a shell
A laptop on a desk running an applicationDesktop
The same app on a laptop. Illustrative photo.

No rewrite

The dist is the app. No rewrite.

WAID does not parse, transform or compile your code. There are no WAID-specific directives, no per-platform branches and no special components to adopt. Search your source for “waid” and the only hit is waid.config.json. Anything that builds to a static dist works - and the same dist is exactly what ships to every platform.
  • No new framework, language or API to learn
  • Your existing Vite + React app builds unchanged
  • The built dist is the material every artifact embeds
A laptop screen showing source codeYour code
An ordinary code editor. Illustrative photo.

Where it fits

Where WAID fits.

One codebase, every platform - without adopting React Native, shipping a WebView, or rewriting in Dart for Flutter. Flutter is the closest architectural cousin: its own GPU renderer, no browser. Here is where WAID sits. Each tool has real strengths; the table is about trade-offs, not winners.

WAIDElectronTauriReact NativeFlutter
RenderingGPU-native (Rune)Chromium / DOMOS WebViewOS-native widgetsGPU-native (Skia / Impeller)
Bundles a browser?NoYes - ships ChromiumNo - uses the OS WebViewNoNo
Your codebaseVite + React, unchangedWeb app + Node mainWeb app + Rust coreRN components - a rewriteDart + Flutter widgets - a rewrite
Web targetReal static site, no runtimeDesktop onlyDesktop / mobile shellVia react-native-webCanvas / Wasm build
Native artifactsDesktop + iOS + AndroidDesktop onlyDesktop + mobile (beta)iOS + AndroidDesktop + iOS + Android
Fidelity across platformsOne renderer - pixel-identicalChromium everywhere (heavy)Varies by OS WebViewPer-OS native widgetsOne renderer - consistent

Fair notes: Electron and Tauri are mature and battle-tested for desktop; React Native has a vast native-module ecosystem; Flutter is a mature, high-performance framework with its own GPU renderer (Skia / Impeller), a huge ecosystem and one of the strongest single-codebase stories around. The honest difference from Flutter: you write Dart and Flutter widgets, and its web output is a heavy canvas build - with WAID you keep ordinary Vite + React and also get a real static website from the same dist. WAID's bet is one GPU renderer, no WebView and no rewrite - at v0.1.0 maturity.

Under the hood

How it actually holds together.

The story above is the plain version. For engineers, here is the machinery - WAID is a thin native packager over your Vite dist and the Rune runtime. It never parses, transforms or compiles your code.

One command, a native artifact

No transpile, no code rewrite. WAID builds your Vite dist, resolves and embeds the pinned Rune runtime, and hands back a signed native artifact. This run produces a real .ipa; the desktop and Android slots stay hollow until their runtimes publish.

~/my-app · waid
$ waid new my-app && cd my-app  scaffolded vite + react + tailwind + shadcn$ waid build --src . --target ios  building web dist … 42 files (1.8 MB)  resolving runtime … rune 0.1.0 · ios · metal   [cache hit]  embedding DetirFFI.xcframework … 214 MB  generating xcode project … MyApp.xcodeproj  archiving … signing device buildbuilt ios artifact -> .waid/build/ios/MyApp.ipa (18.4 MB)$ waid deploy --target ios  resolving device … udid 00008130-0a11…3e! install + launch land next (Phase 15). build + sign done.$ 

.waid/build/

  • .waid/build/web/static site
  • .waid/build/ios/MyApp.ipaios artifact
  • .waid/build/desktop/MyApp.appdesktop bundle
  • .waid/build/android/MyApp.apkandroid artifact

One config, every platform

A single waid.config.json in your source root carries everything the packagers need. It often starts as just a name; capabilities are opt-in, and host functions are desktop-only - declaring them on mobile fails loudly.

waid.config.json
{  "name": "peco",  // bundle id + app name  "icon": "icon.svg",  // SVG master, rasterized per platform  "iconBackground": "#ffffff",  // Android adaptive-icon backdrop  "dist": "web/dist",  // Vite output (auto-inferred)  "media": {  // capture + playback    "camera_capture": true,  // + Apple usage strings    "microphone_capture": true  },  "host": {  // DESKTOP-ONLY - rejected on mobile    "fs": true,    "shell": true,    "vault": true  },  "iosBuildProfile": {  // iOS device signing    "bundleId": "com.splenta.peco",    "team": "XXXXXXXXXX"  }}

GPU-native rendering, no WebView

Native targets do not run your app in a WebView. The Rune runtime draws it on the GPU - wgpu on desktop, Metal on iOS, Vulkan / GLES on Android - and your app reaches real native capabilities too.

Desktop

Native macOS menus from JS

Author the real menu bar at runtime with waid.menus.set([...]). Accelerators fire before page handlers, exactly like a native app.

iOS today

Camera, microphone & media

Flip capture and playback flags in waid.config.json - with Apple usage strings - and the Rune runtime wires them to the OS.

Desktop-only

Desktop host functions

fs, shell, env, vault, path, and actions are exposed to your app on desktop. Declaring host.* on iOS / Android fails loudly - mobile host is planned.

wgpu / Metal / Vulkan

One GPU engine, per-platform backend

detir-scene draws every native target: wgpu on desktop, Metal (DetirFFI.xcframework) on iOS, Vulkan / GLES on Android. No WebView anywhere.

native macOS menus, from JS
if (waid.menus.supported) {  waid.menus.set([    { label: "Project", submenu: [      { label: "New Run", accelerator: "CmdOrCtrl+N", onSelect: startRun },      { type: "separator" },      { label: "Archive", enabled: hasSelection, onSelect: archive },    ] },  ])}

A runtime it never guesses

GB-scale Rune runtimes are resolved and pinned, never baked into the binary. For every native build WAID walks a fixed four-step order and stops at the first hit - so the runtime it embeds is always the exact, verified one. A checksum mismatch is a hard error, never a stale fallback.

01

Explicit override

--runtime-dir (or WAID_RUNTIME_DIR) points at a local runtime tree - the offline / dev path.

02

Source-built cache

If a Rune checkout is reachable, WAID builds the runtime and stages it under ~/.detir/runtimes/source/.

03

Published cache

A previously fetched runtime at ~/.detir/runtimes/<version>/<platform>/ is reused - offline after first use.

04

Verified fetch

Otherwise WAID downloads the pinned runtime, checks its sha256, and extracts it. A mismatch is a hard error - never a stale fallback.

Private - early access

One React app. Native everywhere.

WAID is private and early-access while the packager hardens across targets. There is no public download yet - the Rune runtime is a pinned private release asset. Tell us what you are building and we will get you in.