mob
BEAM on device·Native UI·One language

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.

Get started See it work
v0.7 · early days · confirmed on simulator, emulator, and real iOS + Android devices
SCROLL

Elixir + BEAM + Native iOS / Android UI

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 screen is a GenServer

You write Elixir. The phone renders it natively.

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.

counter_screen.ex
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
9:41
Counter
0
taps so far
Home
Profile

Rendered by SwiftUI · driven by a GenServer · [email protected]

How it works

From your supervision tree to native pixels.

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.

01

Your Elixir app

GenServers, OTP supervision, Ecto, pattern matching, whatever you'd normally reach for.

OTP supervision tree
02

Mob.Screen, a GenServer

Your UI module. Holds state, handles events, returns a view tree from render/1.

component tree
03

Mob.Renderer

Serialises the tree, resolves theme tokens, and crosses the NIF boundary as JSON.

set_root NIF call
04

SwiftUI

iOS · native gestures, dark mode, accessibility

04

Compose

Android · native gestures, dark mode, accessibility

Three packages, one workflow:

mob

The framework you add to your app: screens, the renderer, themes, and the native bridge.

hexdocs ↗
mob_new

The project generator. mix mob.new my_app scaffolds a ready-to-run app.

hexdocs ↗
mob_dev

The dev tooling, a dev-only dep. Build, deploy, connect IEx, and hot-push to the device.

hexdocs ↗
The rackets

Everything the BEAM does. In your pocket.

Full native performance in the UI, full BEAM abilities on the logic side. Nothing crippled to fit a corporate policy.

Hot code reload

Connect over standard Erlang distribution and nl(MyApp.Screen): new bytecode, no restart. Not a proprietary inspector. Just OTP.

Offline by default

The BEAM runs on the device. No server required. Zero latency for UI, works on the subway, ships as a self-contained installable app.

Real native UI

Components render as SwiftUI and Compose primitives. Animations, accessibility, platform gestures and dark mode all work. The native layer handles them.

IEx into your phone

mix mob.connect tunnels in from your Mac. Then :sys.get_state, :observer and tracing all work against a running app.

The full power of the BEAM

Distribution. Tuning flags. Hot upgrades. It's all exposed. You decide what's right for your app, not the framework.

Themes that swap live

Obsidian, Citrus, Birch, or roll your own tokens. Switch at runtime for accessibility or user preference with Mob.Theme.set/1.

Hired guns

If the phone can do it, you can build it.

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.

Just a dependency

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 feature, not a function

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.

Signed, deny by default

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.

Where it sits

An unusual position in the landscape.

Mob borrows a little from everything that came before, and lands somewhere new: the runtime on the device, the UI native, the language singular.

LiveView Native

  • Server-driven; the BEAM lives in the cloud
  • Needs a connection for UI logic
  • Pioneered the three-letter sigil ❤

React Native / Flutter

  • JS or Dart over a bridge
  • A second language and toolchain
  • No OTP, no supervision trees

Native (Swift + Kotlin)

  • Two codebases, two languages
  • Best tooling, biggest investment
  • Backend devs start from zero

Image Mob

  • BEAM on the device, no server
  • Native SwiftUI & Compose views
  • One language: Elixir, all the way down
Mob is my one good album.
the author, on years of ruminating before writing a line
Named themes

Pick a look. Or invent one.

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.

Obsidian

Mob.Theme.Obsidian, dark violet

use Mob.App, theme: Obsidian

Citrus

Mob.Theme.Citrus, warm charcoal + lime

use Mob.App, theme: Citrus

Birch

Mob.Theme.Birch, warm parchment

use Mob.App, theme: Birch
An inside job

Hand it to an agent. Watch it drive.

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.

Reads the real state

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.

Drives the real UI

Tap by intent, navigate, type, scroll to a spot. The agent fires the same events a finger would, straight at the running screen.

Sees without leaving the wire

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.

Read the agentic coding guide
Quickstart

Four commands to a running app.

Generate a project, drop into the simulator, and start hot-reloading from IEx. The dev dashboard runs in your browser.

~/code
$ mix archive.install hex mob_new
$ mix mob.new my_app
creating my_app/lib/my_app/counter_screen.ex
ready: BEAM bundled, native shells generated
$ cd my_app
$ mix mob.deploy --ios
booting simulator · starting node [email protected]
live · run mix mob.connect to attach IEx

Then never restart again.

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.

Read the getting started guide
Early development · v0.7

Come build it with us.

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.