Write mobile apps in Elixir. The whole thing, UI and logic, runs on the BEAM, on the device. No server. No JavaScript. No Swift or Kotlin to learn.
OTP supervision, GenServers, pattern matching, the pipe operator, the same mental model on the phone as on your backend. One language, one runtime, one head to keep it all in.
A Mob.Screen holds your state and returns a view tree. The native layer turns it into real SwiftUI and Compose. Tap the button, it's the same handle_event/3 you already know.
defmodule MyApp.CounterScreen do use Mob.Screen def mount(_params, socket) do {:ok, assign(socket, count: 0)} end def render(assigns) do ~MOB""" <vstack spacing={20} padding={24}> <text style={:large_title}>{@count}</text> <button tag="increment" style={:filled}> Add one </button> </vstack> """ end # fires on tap, pure Elixir, on the device def handle_event("tap", %{"tag" => "increment"}, socket) do {:noreply, update(socket, :count, &(&1 + 1))} end end
The NIF bridge is a clean separation. You fully control the UI from Elixir, and you never write the native layer. It ships with the framework.
GenServers, OTP supervision, Ecto, pattern matching, whatever you'd normally reach for.
Mob.Screen, a GenServerYour UI module. Holds state, handles events, returns a view tree from render/1.
Mob.RendererSerialises the tree, resolves theme tokens, and crosses the NIF boundary as JSON.
iOS · native gestures, dark mode, accessibility
Android · native gestures, dark mode, accessibility
Three packages, one workflow:
mobThe framework you add to your app: screens, the renderer, themes, and the native bridge.
hexdocs ↗ mob_newThe project generator. mix mob.new my_app scaffolds a ready-to-run app.
The dev tooling, a dev-only dep. Build, deploy, connect IEx, and hot-push to the device.
hexdocs ↗Full native performance in the UI, full BEAM abilities on the logic side. Nothing crippled to fit a corporate policy.
Connect over standard Erlang distribution and nl(MyApp.Screen): new bytecode, no restart. Not a proprietary inspector. Just OTP.
The BEAM runs on the device. No server required. Zero latency for UI, works on the subway, ships as a self-contained installable app.
Components render as SwiftUI and Compose primitives. Animations, accessibility, platform gestures and dark mode all work. The native layer handles them.
mix mob.connect tunnels in from your Mac. Then :sys.get_state, :observer and tracing all work against a running app.
Distribution. Tuning flags. Hot upgrades. It's all exposed. You decide what's right for your app, not the framework.
Obsidian, Citrus, Birch, or roll your own tokens. Switch at runtime for accessibility or user preference with Mob.Theme.set/1.
Capabilities live in plugins now, not core. Each one is just a Hex package: a camera wrapper, a payment flow, a whole embedded app, or something nobody has thought of yet. Add the ones you want and leave the rest. Core stays lean and only knows how to host them, so the ecosystem can go places I never planned.
Add a plugin like any Hex package, then flip it on with one line in mob.exs. Remove it and it is genuinely gone, not hidden behind a flag.
A plugin ships the whole vertical slice: native bridge, screens, migrations, assets, lifecycle. Or as little as ten lines of pure Elixir. Same manifest, no cliff between the two.
Every activated plugin is checked at build time. Nothing native runs in your app until you have cleared its key, first party and third party alike.
Mob borrows a little from everything that came before, and lands somewhere new: the runtime on the device, the UI native, the language singular.
“Mob is my one good album.”the author, on years of ruminating before writing a line
Tokens, not hard-coded colors. Use a built-in theme, override a couple of tokens, or build one from scratch. Tap a card to preview it on this page.
Half the point of Mob is how little stands between an AI agent and a running app. There's no proprietary inspector to reverse engineer and no bridge to mock. It's plain Erlang distribution, so an agent attaches to the live app the same way you would, and gets the real thing: actual state, the real UI, and a feedback loop measured in milliseconds.
Mob.Test.assigns/1 hands back the live socket, not a guess from pixels. An agent asserts on what the app actually holds, then keeps moving.
Tap by intent, navigate, type, scroll to a spot. The agent fires the same events a finger would, straight at the running screen.
Screenshots and element positions come back in process over the same connection. No adb screencap, no xcrun, no out-of-band tooling to fumble.
And when it finds a fix, nl(MyApp.Screen) hot-pushes the new code into the live app, so the loop never stops for a rebuild.
Generate a project, drop into the simulator, and start hot-reloading from IEx. The dev dashboard runs in your browser.
From a connected IEx session, nl(MyApp.CounterScreen) hot-pushes new bytecode to the live app. No rebuild, no relaunch.
Run mix mob.server for the dev dashboard: system logs and Elixir logs from both platforms, side by side, in the browser.
Because it's plain Erlang distribution, iex, adb and xcrun tooling all work, so AI coding agents get real access to a running app out of the box.
Confirmed on the iOS simulator, the Android emulator, and real iOS and Android devices. The framework is moving fast: the plugin system just landed, and there is plenty of room for early hands. Feedback welcome.