Status: Early development. Not yet ready for production use.
Dala is a native mobile framework for Elixir powered by the BEAM VM.
It brings OTP, lightweight processes, fault tolerance, and the actor model to iOS and Android development while using a Rust-powered native runtime for rendering and platform integration.
Unlike WebView-based frameworks, Dala focuses on native execution, concurrent application architecture, and local-first intelligent applications.
Modern mobile applications are becoming increasingly complex:
- AI/ML pipelines running on-device
- realtime synchronization
- local-first data systems
- streaming workloads
- background processing
- highly concurrent state management
These problems look more like distributed systems than traditional frontend applications.
Dala uses the strengths of the BEAM ecosystem to solve them naturally.
- Native iOS and Android runtime
- Real BEAM VM on mobile devices
- OTP and actor-model concurrency
- Rust-powered rendering and native integrations
- Declarative UI API
- Designed for local-first and AI-powered applications
- High-concurrency architecture
- Binary protocol bridge for low-overhead communication
- Future-focused AOT experimentation inspired by HiPE concepts
Elixir/Erlang
↓
BEAM VM
↓
Rust Native Runtime
↓
iOS / Android
Dala keeps the BEAM runtime as the core execution engine while using Rust for performance-critical systems such as rendering, layout, native APIs, and ML integrations.
Dala is not trying to be another web wrapper for mobile apps.
The goal is to build an OTP-native runtime for modern mobile applications — especially apps that require:
- concurrency
- realtime coordination
- offline-first architecture
- resilient background systems
- on-device AI/ML
- complex local data flows
Dala is experimental and evolving rapidly.
Add to mix.exs:
defdepsdo[{:dala,"~> 0.8"}]endThe dala_new package (separate) provides project generation, deployment tooling, and will import dala_dev which is a live dashboard. Install it as a Mix archive:
mix archive.install hex dala_newdefmoduleMyApp.CounterScreendouseDala.Spark.Dslattributesdoattribute:count,:integer,default: 0endscreenname: :counterdocolumndogap:space_smtext"Count: @count",text_size: :xlbutton"Increment",on_tap: :incrementendenddefhandle_event(:increment,_params,socket)do{:noreply,Dala.Socket.assign(socket,:count,socket.assigns.count+1)}endenddefmoduleMyApp.CounterScreendouseDala.Screendefmount(_params,_session,socket)do{:ok,Dala.Socket.assign(socket,:count,0)}enddefrender(assigns)do%{type: :column,props: %{padding: :space_md,gap: :space_md,background: :background},children: [%{type: :text,props: %{text: "Count: #{assigns.count}",text_size: :xl,text_color: :on_background},children: []},%{type: :button,props: %{text: "Increment",on_tap: {self(),:increment}},children: []}]}enddefhandle_event("tap",%{"tag"=>"increment"},socket)do{:noreply,Dala.Socket.assign(socket,:count,socket.assigns.count+1)}endenddefmoduleMyAppdouseDala.App,theme: Dala.Theme.Obsidiandefnavigation(_platform)doscreens([MyApp.CounterScreen])stack(:home,root: MyApp.CounterScreen)enddefon_startdo{:ok,_pid}=Dala.Screen.start_root(MyApp.CounterScreen)cookie=Dala.Connectivity.Dist.cookie_from_env("MY_APP_DIST_COOKIE","my_app")Dala.Connectivity.Dist.ensure_started(node: :"my_app@127.0.0.1",cookie: cookie)endend# Push a new screenDala.Socket.push_screen(socket,MyApp.DetailScreen,%{id: 42})# Pop backDala.Socket.pop_screen(socket)# Tab bar layouttab_bar([stack(:home,root: MyApp.HomeScreen,title: "Home"),stack(:profile,root: MyApp.ProfileScreen,title: "Profile")])# Named themeuseDala.App,theme: Dala.Theme.Obsidian# Override individual tokensuseDala.App,theme: {Dala.Theme.Obsidian,primary: :rose_500}# From scratchuseDala.App,theme: [primary: :emerald_500,background: :gray_950]# Runtime switch (accessibility, user preference)Dala.Theme.set(Dala.Theme.Citrus)Built-in themes: Dala.Theme.Obsidian (dark violet), Dala.Theme.Citrus (warm charcoal + lime), Dala.Theme.Birch (warm parchment).
All async — call the function, handle the result in handle_info/2:
# Haptic feedback (synchronous — no handle_info needed)Dala.Hardware.Haptic.trigger(socket,:success)# CameraDala.Media.Camera.capture_photo(socket)defhandle_info({:camera,:photo,%{path: path}},socket),do: ...# LocationDala.Platform.Location.start(socket,accuracy: :high)defhandle_info({:location,%{lat: lat,lon: lon}},socket),do: ...# Push notificationsDala.Platform.Notify.register_push(socket)defhandle_info({:push_token,:ios,token},socket),do: ...Also: Dala.Platform.Clipboard, Dala.Platform.Share, Dala.Media.Photos,
Dala.Storage.Files, Dala.Media.Audio, Dala.Ui.Sensor.Motion,
Dala.Hardware.Biometric, Dala.Hardware.Scanner, Dala.Hardware.NFC,
Dala.Ui.Scan, Dala.Permissions.
Additional APIs: Dala.Hardware.Bluetooth (BLE), Dala.Connectivity.Wifi, Dala.Wakelock,
Dala.Storage.Storage, Dala.Storage.Blob, Dala.Platform.Settings, Dala.Platform.State,
Dala.Platform.Linking, Dala.Platform.Background, Dala.Ui.Feedback.Alert, Dala.Ui.Embedded.Webview.
mix dala.connect # tunnel + connect IEx to running device
nl(MyApp.SomeScreen) # hot-push new bytecode, no restart# In IEx:
Dala.Test.screen(:"my_app_ios@127.0.0.1") #=> MyApp.CounterScreen
Dala.Test.assigns(:"my_app_ios@127.0.0.1") #=> %{count: 3, ...}
Dala.Test.tap(:"my_app_ios@127.0.0.1", :increment)test"increments count"do{:ok,pid}=Dala.Screen.start_link(MyApp.CounterScreen,%{}):ok=Dala.Screen.Screen.dispatch(pid,"tap",%{"tag"=>"increment"})assertDala.Screen.Screen.get_socket(pid).assigns.count==1end| Package | Purpose |
|---|---|
dala_dev | Dev tooling: mix dala.new, mix dala.deploy, mix dala.connect, live dashboard |
dala_new | Generator project tool |
dala_runtime | AOT compiler & runtime for BEAM, fix limitations of JIT |
Full documentation at hexdocs.pm/dala, including:
- Getting Started
- Architecture & Prior Art
- Screen Lifecycle
- Components
- Theming
- Navigation
- Device Capabilities
- Testing
MPL-2.0