Cookbook

Moving from UniFFI

You already have a Rust core. Undra adds what sits above the bindings: state the UI observes, a data layer, ports with fakes and a dev loop. The move is mostly attributes.

Three steps#

StepWhat movesWhat it removes
L1 side by sideA new Undra crate beside your UniFFI one. Plain-data functions first: #[uniffi::export] becomes #[undra::api].Cancellation from either side, panics as typed values, a typed error for a mismatched library (UNI-3 to UNI-5).
L2 one featureState a screen polls, or is pushed through callbacks, becomes a store; its fetches a query and mutations.The state-sync glue, and the cache, retry and offline logic you did not have (UNI-1, UNI-9).
L3 whole domainPlatform services reached through callbacks become ports.The test doubles you wrote by hand (UNI-12).

Row ids are those of the limitations catalogue, as of October 2026. Undra vs UniFFI and Crux compares the two and says where UniFFI is ahead.

What maps to what#

UniFFIUndra
#[uniffi::export] fn, async fn#[undra::api] fn, async fn
uniffi::Record, uniffi::Enum#[undra::api] struct, enum (payloads in all three languages)
uniffi::Error#[undra::error]
an Object with methods#[undra::api] impl; a #[undra::store] if the UI observes it
a callback that pushes statea signal the UI observes
a foreign trait for a platform servicea port (#[undra::port]), one per platform
a callback interface passed as an argumenta #[undra::callback] trait, taken as Arc<dyn Trait>

What does not map yet#

Objects, callbacks and newtypes map: an object crosses as a parameter or a return value, as itself (objects), a #[undra::callback] trait taken as Arc<dyn Trait> is an argument the core calls back (callbacks), and a one-field tuple struct crosses as the value it wraps and arrives as a type of its own (newtypes). L1 still starts with plain data, because it moves without touching the platform code. Several cores load side by side, so the rest can wait. Auth and offline-first show what L2 adds.

Edit this page on GitHub