Start

Getting started

Three commands put the to-do app that every new project starts with on your screen, running on its Rust core. Then the live loop, the native apps, and what you ship.

In five minutes#

With Rust (and its wasm32-unknown-unknown target) and Node installed:

bash
curl -fsSL https://shreypdev.github.io/undra/install.sh | sh   # the CLI; cargo is the other way, below
undra init myapp                                               # a core, its bindings, and an app per platform
cd myapp/web && npm install && npm run dev                     # the web app on the core: http://localhost:5173

undra init prints Created …/myapp (… files, bindings for schema hash 0x…) and what to run next; nothing is downloaded, the templates are part of the binary. npm run dev starts Vite, whose undra() plugin builds the core to WebAssembly first (==> Building the core for web, then a table with the module's size against its budget; the first build compiles the Undra crates and takes a minute or two). The page at http://localhost:5173 is a to-do list whose state lives in the Rust core in core/src/lib.rs. Edit that file and save: the plugin rebuilds the core and reloads the page.

That is the whole loop on the web. The rest of this page installs what each platform needs, shows undra dev (one core served to every app, its state kept across a rebuild), and runs the iOS and Android apps. undra doctor says what your machine is missing, with the command that fixes each gap.

Already have an app? undra adopt ../MyApp adds a core to an existing iOS, Android or web app. It writes one new undra/ directory and the exact steps to wire it in, and never edits your project files.

What you need#

Install only what your target platforms need. undra doctor checks all of it and prints the fix for each gap.

You wantYou need
Any core (everything in crates/)Rust stable, plus the cross targets Undra ships to
WebNode 20+, and binaryen (for wasm-opt, optional: it makes the wasm core smaller)
AndroidJDK 17, Kotlin, Gradle, the Android SDK and NDK r27 (no cargo-ndk: Undra links with the NDK's clang itself)
iOS (macOS only)Full Xcode from the App Store, plus an iOS simulator runtime

1. Install the toolchain#

Rust comes first, with the targets Undra builds for.

bash
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
rustup target add wasm32-unknown-unknown \
  aarch64-apple-ios aarch64-apple-ios-sim \
  aarch64-linux-android x86_64-linux-android

Then the platform pieces you need.

bash
# Web and the TypeScript runtime
brew install node binaryen

# Android: JDK, Kotlin, Gradle, then the SDK and NDK
brew install openjdk@17 kotlin gradle
brew install --cask android-commandlinetools

For iOS, install Xcode from the App Store (the Command Line Tools alone have no XCTest and no simulators), then:

bash
sudo xcode-select -s /Applications/Xcode.app/Contents/Developer
sudo xcodebuild -license accept
xcodebuild -downloadPlatform iOS        # simulator runtime, one time, large

The Android SDK needs a few packages (platform 35, build-tools 35, NDK 27.2, the emulator and a system image). The onboarding guide has the exact sdkmanager line and an avdmanager line that creates the undra emulator used throughout the project.

2. Install the CLI#

Pick one channel. The installer puts a prebuilt undra binary on your machine (macOS and Linux, arm64 and x86_64); cargo builds it from source with Rust 1.85 or newer.

ChannelCommand
Installercurl -fsSL https://shreypdev.github.io/undra/install.sh | sh
cargocargo install --git https://github.com/shreypdev/undra undra-cli

The installer detects your OS and architecture, verifies the download against the release checksums, installs to ~/.undra/bin and prints the line to add to your PATH. It never uses sudo.

bash
undra --version
undra doctor      # prints what is missing, with the fix

undra doctor exits with status 1 when something the project needs is missing, so it works in CI too. Inside a project it only checks the platforms named in undra.toml. Each finding says what it found (ok, missing or the wrong version), the value it saw, the exact command that fixes it and the heading of the onboarding guide that explains it; undra doctor --fix prints every fix as one block to read and paste (it runs nothing) and --json prints the report for tools.

From a checkout (contributors)

bash
git clone https://github.com/shreypdev/undra.git && cd undra
cargo install --path crates/undra-cli    # the `undra` command
source scripts/env.sh                   # PATH, JDK and Kotlin jars, Xcode fallback, per shell

3. Create a project#

bash
undra init myapp                        # core + shells: SwiftUI on iOS, Jetpack Compose on Android, React on the web
undra init myapp --platforms ios,web    # only some of them
undra init myapp --id dev.acme.myapp    # application id / bundle identifier
cd myapp

Nothing is downloaded: the templates are part of the undra binary. The project you get is a directory with a undra.toml and:

PathWhat it is
core/The Rust core: a working to-do store with #[undra::store] and #[undra::api]
generated/Swift, Kotlin and TypeScript bindings for that core, already generated (how to review them)
ios/ android/ web/One minimal, real app per platform, using the generated bindings

The project depends on the release of the undra that made it, and on nothing but the Undra repository on GitHub: no registry account, no token. With undra 1.0.0 the lines are:

WhereThe dependency
core/Cargo.tomlundra = { git = "https://github.com/shreypdev/undra", tag = "v1.0.0" }
iOS (the Xcode project, and generated/swift/Package.swift).package(url: "https://github.com/shreypdev/undra", from: "1.0.0"), product UndraRuntime
android/app/build.gradle.ktsimplementation("com.github.shreypdev.undra:runtime:v1.0.0") and :android-adapters:v1.0.0, from https://jitpack.io, which settings.gradle.kts declares for that group only
web/package.json"@undra/runtime": "https://github.com/shreypdev/undra/releases/download/v1.0.0/undra-runtime-1.0.0.tgz"

Swift Package Manager reads the repository's tags, JitPack builds the Kotlin modules from the tag, and npm installs the release's tarball by its URL. undra upgrade moves all of them to a newer release together. Pass --undra-path ~/src/undra (or set UNDRA_PATH) to use a checkout's crates and runtimes instead.

4. Start the live loop#

bash
undra dev                          # serves the core on ws://127.0.0.1:7443
undra dev --addr 0.0.0.0:7443      # reachable from a phone on your network

undra dev builds your core for this machine and serves it over a WebSocket. A simulator, a phone or a browser tab connects with the remote transport of its runtime and uses this core instead of a built-in one. The banner says how each platform connects:

text
Undra dev server is running.

ws://127.0.0.1:7443

  schema hash   0x…
  devtools      http://127.0.0.1:7443/devtools?token=…
  web           … or ?undra=<url> in the page URL
  iOS           launch the app with UNDRA_DEV_URL set to the URL above
  Android       emulator  ws://10.0.2.2:7443

Edit Rust and save: the core is rebuilt and restarted with its state, the connected apps reconnect by themselves and stay on the screen they were on, and the terminal says so (Restarted: ws://127.0.0.1:7443 (schema hash 0x…); state kept (1 store, 1 KiB, restored in 157 µs)). An edit that changes the API, a field or an error's message, changes the schema hash: the server says so, and an app built from the old bindings reports the mismatch and stops until you run undra bindgen and rebuild it. Development log records (a line per transaction commit, port call and panic) print in the terminal; the devtools address opens a timeline of every change-set.

On the web, open the page with the server's address: http://localhost:5173/?undra=ws://127.0.0.1:7443 (development builds only). Vite's plugin watches core/src too and rebuilds the wasm core; it reloads the pages that run that core and leaves a page opened with ?undra= alone, so the page stays on the served core with its state.

No authentication. The server has none. Keep the default loopback address unless a device has to reach it, and never expose it beyond a network you trust.

5. Run each platform#

Each shell is a plain native project wired to your core, and its own README says how to run it. The short version:

Web#

bash
cd web && npm install && npm run dev

The page runs the core in the browser as a wasm module, which the undra() Vite plugin builds. With ?undra=ws://127.0.0.1:7443 in its URL it uses the core that undra dev serves instead; web/src/undra.ts does that with one call:

ts
await UndraMyappCore.load({ mode: "remote", url: devUrl });

iOS#

Open ios/ in Xcode and run on a simulator. Xcode resolves the Swift package undra from https://github.com/shreypdev/undra at the project's release the first time (it clones the repository, about 47 MiB). There is no undra build to run first: the Build the Undra core Run Script phase of the Xcode project builds the core before the app compiles, a Debug build with a debug core and a Release build with a release one, and Xcode skips it while the core is unchanged. In debug builds the generated app reads UNDRA_DEV_URL (for example ws://<your Mac>:7443) and attaches to undra dev when it is set.

Android#

Build and install from android/ with Gradle (./gradlew :app:installDebug); the runtime's modules come from JitPack, built from the release's tag. There is no undra build to run first, because the undraBuild Gradle task builds the core before everything else and Gradle skips it while the core is unchanged. A release variant (assembleRelease, bundleRelease) gets a release core, anything else a debug one: a debug core is tens of megabytes per ABI. Debug builds can run against undra dev instead (no native build needed; -PundraSkipBuild=true leaves the task out): ./gradlew -PundraDevUrl=ws://10.0.2.2:7443 :app:installDebug for an emulator, or adb reverse (undra dev --android) and ws://127.0.0.1:7443 for a USB device.

6. Build what you ship#

In a project made by undra init the app builds do this for you: Gradle's undraBuild task, Xcode's build phase and the undra() Vite plugin (@undra/runtime/vite) run undra build before each app builds, and skip it while the core is unchanged. They find undra on PATH; undra doctor checks that it is there. A repository built by Bazel uses the Bazel rules instead, which call the same CLI. The explicit form is for packaging and for CI steps that build the core once:

bash
undra build --release        # every platform in undra.toml
undra build --platform ios,android,web --release
PlatformArtifact, below build/, named after the core's namespace
iOSios/MyappCore.xcframework (device and simulator slices)
Androidandroid/jniLibs/<abi>/libmyapp_core.so, arm64-v8a and x86_64, 16 KB page aligned
Webweb/myapp_core.wasm, release profile then wasm-opt -Oz
Hosthost/libmyapp_core.{dylib,so}, for JVM tests and undra bindgen

undra init also writes .github/workflows/undra.yml: a job for the core (format, clippy, tests, undra bindgen --check) and one per app, each installing undra at the version pinned once at the top of the file. When a newer Undra is out, undra upgrade moves every pin of the project (the core's dependency, the runtimes, the workflow) in step, regenerates the bindings and prints the migration notes of every release it crosses; undra upgrade --dry-run shows the lines first.

Sizes are printed at the end next to the design's budgets. On the reference build the hello-world web core is 113.1 KB gzipped (budget 120 KB, gated in CI) and the arm64-v8a Android .so is 898.2 KB (budget 1.2 MB per ABI, gated in CI). To trade speed for size, or size for speed, set opt_level in undra.toml: Operating in production has the numbers for each setting.

A release core under a debug app

While you work on screens, the core rarely changes, and a debug core is much slower than the one you ship. Build the app in Debug and the core in release: the app keeps its breakpoints and its dev bar, the core runs at its real speed.

WhereHow
Android./gradlew :app:installDebug -PundraRelease=true (the undraBuild task reads the property; without it a debug variant builds a debug core)
iOSin the Build the Undra core Run Script phase, the last line (the one that starts exec env -i) ends --configuration "$CONFIGURATION": change that to --configuration Release (the echo "note: ..." line above it only prints the command). The app's own configuration stays Debug
Webnothing to do: the web core is always optimized
React Nativeundra build --platform rn --release
Bazelundra_core(release = True) on the core target the app links

Rust-level debugging of the core itself wants a debug core again; undra dev serves either kind to every running app.

Building with Bazel? The rules (undra_core, undra_bindings, undra_bindings_test and the Swift, Kotlin and TypeScript libraries over the bindings) run this same build inside Bazel actions, hermetic and offline: see the Bazel guide.

Prefer to explore first?#

Try it in your browser first: the live playground is the real Rust core running as WebAssembly. The playground source is the same thing, fully built: a to-do list, a counter, a 10,000-row keyed list and remote queries with an offline switch, on Chrome, an iPhone simulator and an Android emulator. It uses only public APIs, and proof screenshots are committed in its .proof/ folder.

bash
undra build -C examples/playground --platform web
cd examples/playground/web && npm install && npm run dev

If you are contributing#

Every suite runs locally and nothing needs the network after install. The table is the same one the CI matrix follows.

SuiteCommand
Rust workspacecargo test --workspace
Lintscargo fmt --check && cargo clippy --workspace --all-targets -- -D warnings
TypeScript runtimecd runtimes/ts/@undra/runtime && npm ci && npm test
Kotlin runtimeruntimes/kotlin/undra-runtime/scripts/test-local.sh
Swift runtimecd runtimes/swift/UndraRuntime && swift test
wasm ABIbash crates/undra-ffi/tests/wasm/run.sh
Contract scenarios, three platformsbash contract-tests/run-all.sh
Benchmark budget gatecargo test -p undra-bench --test budgets --release

Read CLAUDE.md (the constitution) and docs/SPEC.md before you write code; the architecture page digests the rules. Changes land one piece per worktree, with an adversarial review before merge; docs/AGENT_WORKFLOW.md describes the loop for humans and agents alike.

Edit this page on GitHub