Mob - Native BEAM / Elixir on Native Mobile

I’ve posted a couple times about this topic before:

I’m hoping the description below is close to the final iteration of what the Elixir community would like in a mobile app framework. Working title, mob.

BEAM-on-device mobile framework, looking for feedback

I am working on a mobile framework for Elixir that takes a different approach than what’s out there although it combines a bit of every approach so far. Looking for early feedback before going further.

The idea in one sentence:

Write mobile apps in Elixir using a LiveView-style lifecycle: mount, render, handle_event, where the BEAM runs directly on the device and drives native UI through NIFs. No server, no JavaScript, no WebView.

How it actually works:

The app shell (Android APK / iOS IPA) is a thin native wrapper that boots the BEAM on startup. From that point, Elixir owns the app. render/1 returns a component tree; the renderer walks it and makes NIF calls that create and mutate real native views — Android Views on Android, UIKit on iOS. State changes trigger a diff and only the changed views are updated. The native shell is just a display surface; all logic lives in Elixir.

  defmodule MyApp.CounterScreen do
    use Mob.Screen

    def mount(_params, _session, socket) do
      {:ok, assign(socket, :count, 0)}
    end

    def render(assigns) do
      %{type: :column, props: %{padding: 16}, children: [
        %{type: :text,   props: %{text: "Count: #{assigns.count}"}},
        %{type: :button, props: %{text: "+", on_tap: self()}}
      ]}
    end

    def handle_event("tap", _, socket) do
      {:noreply, assign(socket, :count, socket.assigns.count + 1)}
    end
  end

If you already know LiveView, that lifecycle is immediately familiar. The intent is that transitioning to mobile should feel like learning a new set of components, not a new paradigm. That being said, delivering to the web is a dream come true in terms of distribution. We can’t get away from having to submit via app stores, etc. There very well could be some platform related glue code using gradle or plist files that you may still have to alter yourself. I’m not deep enough into it to know at this stage.

How it relates to existing work:

  • LiveView Native — closest in spirit, but LVN requires a Phoenix server. Mob runs fully on-device; no server needed, works offline, no latency on interactions. When
    people first heard “LiveView Native” a lot of them expected something like this — BEAM on the phone, LiveView-style code, native UI. LVN is excellent but it’s a
    different tradeoff.
  • Elixir Desktop — proved BEAM-on-device is viable. Mob builds on that insight but targets mobile-native UI patterns (component trees, event model) rather than desktop
    wxWidgets.
  • React Native — the component/props/event model is familiar territory for people coming from that world. mount ≈ component lifecycle, handle_event ≈ event handlers,
    assigns ≈ state. The diff-and-patch render loop is the same idea.

Dev experience goals:

Dev mode connects the device to your machine over Erlang distribution (WiFi, mDNS), no USB required, works on Android and iOS identically). The device boots a minimal shell and dials home.

mix mob.dev

on the dev machine starts a file watcher; save a file and the new module is pushed to the device, same loop / experience as

mix phx.server

Full IEx shell into the running on-device BEAM: inspect GenServer state, trace function calls.

OTA updates are supported. Beam bytecode runs in the embedded BEAM VM; same interpreted-code exception that covers React Native/CodePush and LiveView Native.

Code-signed delivery.

Current status:

HelloScreen confirmed working on Android emulator, real Android phone (non-rooted), and iOS simulator.

What I’m looking for:

  • Does the LiveView-on-device model resonate? Is this what you were hoping LVN would be?
  • Component vocabulary: what would you reach for first beyond the basics?
  • Any prior art I’m missing?
  • Would you use this?

Non goals for the project

  • All things to all people
  • Not targeting anything outside of iOS, Android, although I think this approach is fairly easy to replicate if you want to build a desktop app for Windows, etc.

I have a larger planning document I coauthored with Claude but I wanted to avoid a wall of LLM text. I can post that as well if people are interested for a fuller vision of the project.

Working on Android / iOS and I’ve also run this on my own Android device.

One of the things I thought was a blocker but turned out to be fine was the BEAM boot time. It takes about half a second depending on hardware.


29 Likes

yes

yes

3 Likes

I like this idea.

I think we can join clusters between dev environments (dev machine ErlVM & mobile ErlVM), making it easier to debug, hot reload, and update state remotely.

Another interesting idea is integrating Nx for edge AI—if possible, this could save a ton of time when developing client apps with AI assistants.

For this I think adapt Impeller engine from flutter or other render engine.

Do you have a repo? I’d love to take a look.

I doubt you need to compute the diff. Just re-render the whole thing; you are not limited by the bandwidth or need to preserve the client state, like the Liveview does.

Repo:

Plan:

1 Like

I also grabbed the hex name so it doesn’t get scooped in the meantime. Not super useful yet unless you just want hello world. Probably missing some instructions too.

1 Like

I have took a look.

Currently, you wrapper to native components.

I will try to adapt Impeller engine in the near future. I think Flutter (using Impeller engine) has widget tree look like HTML then we can get some benefit from that.

I briefly considered Flutter.

I built a proof of concept using NativeScript with a remote server which does much of what LVN does.

You could do LiveView > LiveViewSvelte > Svelte > SvelteNative > NativeScript already if you were willing to build that chain. The proof of concept I built just cuts out everything having to do with Svelte. Then I realized you could just cut more out and do it direct. NativeScript utilizes the same direct access to the platform code.

So by putting the BEAM on the device and accessing the app via a NIF you cut out all the middlemen. You get the native behaviour and performance and you also get the BEAM which is kind of the whole point and writing in Elixir is the icing on the cake.

I worked for years in React Native and there are a lot of problems. One of them is you’re always one step removed. Whenever Android or iOS make big changes they facilitate them ahead of time in their IDEs but they don’t actually tell people necessarily. So the platforms like React Native will always be a half step behind because they have to react the day the new feature comes out. I want something where we can always have the option of just writing native code. When LVN was first announced I was very enthusiastic about the approach because you get a native app which is the industry gold standard.

4 Likes

Really interesting work. One thing I’m still trying to understand: Why do you dislike the idea of using LiveView as the runtime/rendering transport on mobile?

I’m working on something in a similar direction. It also embeds the BEAM on-device and uses a native bridge, but the rendering model is different: it boots Phoenix/LiveView locally and loads the loopback endpoint inside a WebView/WKWebView, then bridges native capabilities like notifications / clipboard / tray through the host layer.

So at a low level there seems to be a lot of overlap:

  • embedded BEAM
  • native bootstrap / bridge
  • local runtime
  • Elixir-first app logic

I think native components can be approached in a few ways. Wrapping Compose/Jetpack-style trees over JSON, going fully direct through a NIF/JNI view bridge on Android. Or through existing solutions like water-rs, NativeScript…

Your approach is also very cool. What @GenericJam is aiming is the full mobile app experience and the resulting apps are most likely delivered via app store. With your approach, you can probably get away with one standard graphic shell and let users bring their own elixir code (like from hex.pm) and assemble the app on device, bypassing the app store. I think both approaches have their own niche; can’t wait to see you guys to collaborate.

Thanks. I think there may be a slight misunderstanding though.

What I’m doing is also intended to run fully on-device and can absolutely be packaged and distributed as a normal App Store / Play Store app. The embedded BEAM, Phoenix, and LiveView runtime are bundled with the app itself.

So the difference I see is not really “app store app” vs “bypass app store”, but more the rendering boundary:

  • GenericJam seems to be pushing toward Elixir driving native views directly

  • my approach keeps LiveView as the UI layer locally on-device, with the host bridge handling native capabilities

So it’s still a packaged app, just with a different runtime/UI architecture.

In principle a generic shell is possible, but that’s not really the main thing I’m aiming for.

1 Like

Ok, I was just saying I want a way to bypass the app store.Delivering mobile app on hex.pm would be very nice.

Apple specifically forbids this and any app that attempts it will be rejected. If you care about getting your app on iOS, you simply cannot do this.

I don’t think that’s true. There are many Capacitor / Cordova apps (aka apps wrapping a webview) on the App Store. These can be updated without review unless you’re updating a feature that uses an iOS feature.

Sure, if you’re wrapping a web-view, but that’s not a native app. The native app review process is much more stringent. I’d think if you have the app running locally on the device (say, a LiveView app) that you could update remotely (specifically NOT loading a remote web app but updating the on-device code) that would be rejected.

1 Like

Mob seems cool. Would it offer any benefit versus writing a native iOS app other than being able to write things in elixir?

I wonder would AtomVM be a better fit? I’ve heard BEAM is a resource hog but maybe it’s fine on iOS, I don’t know.

@andyleclair there’s a bit of wiggle room here. If beam code can be defined as ‘interpreted code’ it can be allowed. This is essentially the space codepush has been operating in for years already and I believe LiveView Native apps have already made it through the app store approval process and they use the exact same mechanism.

@Grouvie why not LiveView? This is the approach (similar?) taken by elixir desktop. I don’t want to do it this way. It’s unnecessarily heavy to wrap the server and the client together although it makes sense for their use case. I briefly considered LiveView > LiveViewSvelte > SvelteNative > NativeScript but after making a proof of concept that was just LiveView > NativeScript I realized NativeScript is just referencing the native elements which I could do myself and cut out all the middle layers.
I had a few years with React Native and one of the things that annoyed me about it was all the layers. Something would break in one of the layers and then you have to unexpectedly spend a day fixing it because of a library change or something. So I’m trying to balance simplicity with convention.

@jam benefits of having the BEAM on board are numerous from my perspective.

Event handling suddenly becomes very easy. In general I love the BEAM runtime but for mobile apps it’s a great place to have something like the BEAM that handles errors well. Everything you love about the BEAM runtime is suddenly available inside your app and not on a remote server. Right in your app.

Hot code upgrade. You could get really crazy with it and hardcode your data in the app itself and just send an updated version of beam code every time you want to show something new in the UI. I’m not promoting this but it is possible. Going through the app store is a really annoying and time consuming process. Delivering to the web is a dream come true because there are no gatekeepers apart from the restrictions in the browser. I’ll put in some reasonable security precautions in the app like code signing but this will essentially come down to trusting that no one takes this library and builds something malicious with it and crashes this option for everyone else. So, yes, you could completely change your app after it gets on device but the ecosystem does have some defences against this like you can get reported. You also need to have permissions granted in order to use aspects of the phone for this very reason. It would be suspicious if your flashlight app needs access to the whole file system.

BEAM distribution: you can link nodes directly to each other. You could imagine phone to phone communication, privileged access to your own server via your phone, etc. For example, you could run a game on two phones with one phone being the screen and the other one being the controller.
Whatever you can already do with BEAM distribution you could do here. Obviously you should understand the caveats that BEAM distribution != server <> client. It is privileged access to another node so you should exercise caution.

Whatever else you love about the BEAM is now on your phone: pattern matching, etc.

I looked into AtomVM and it isn’t feature complete and it’s also trying to do things in a different way, etc. I’m very grateful for everything that project is doing but I didn’t think it was the right fit for this project.
I actually thought I was going to have to rewrite a minimalist BEAM to be the BEAM but smaller but after my initial experiments with getting the BEAM started on device I realized it’s already light enough. Using the actual BEAM greatly reduces my maintenance burden and keeps the framework feature complete with the BEAM.

2 Likes

There are many superapp/miniapp combos in Asia. Installing non-native code not through app store is something Apple hates but impossible to get rid off completely.

It’s not my intention to distribute through hex.pm. The distribution would be through the developer’s server. Perhaps there will be a business in it for someone to run this code server that signs, etc. but the intention for now is that the developer runs their own server for hot upgrade code.

This syntax seems like it might get a bit unwieldy. I wonder if there’s something a bit more elegant.