Packages
mob
0.9.21
0.9.22
0.9.21
0.9.20
0.9.19
0.9.18
0.9.17
0.9.16
0.9.15
0.9.14
0.9.13
0.9.12
0.9.11
0.9.10
0.9.9
0.9.8
0.9.7
0.9.6
0.9.5
0.9.4
0.9.3
0.9.1
0.9.0
0.8.4
0.8.3
0.8.2
0.8.1
0.8.0
0.7.39
0.7.38
0.7.37
0.7.36
0.7.35
0.7.34
0.7.33
0.7.32
0.7.31
0.7.30
0.7.29
0.7.28
0.7.27
0.7.26
0.7.25
0.7.24
0.7.23
0.7.22
0.7.21
0.7.20
0.7.19
0.7.18
0.7.17
0.7.16
0.7.15
0.7.14
0.7.13
0.7.12
0.7.11
0.7.10
0.7.9
0.7.8
0.7.7
0.7.6
0.7.5
0.7.4
0.7.3
0.7.2
0.7.1
0.7.0
0.6.26
0.6.25
0.6.24
0.6.23
0.6.22
0.6.21
0.6.20
0.6.19
0.6.18
0.6.17
0.6.16
0.6.15
0.6.14
0.6.13
0.6.12
0.6.11
0.6.10
0.6.9
0.6.8
0.6.7
0.6.6
0.6.5
0.6.2
0.6.1
0.6.0
0.5.18
0.5.17
0.5.16
0.5.15
0.5.14
0.5.11
0.5.10
0.5.7
0.5.6
0.5.5
0.5.4
0.5.3
0.5.2
0.5.1
0.5.0
0.4.0
0.3.10
0.3.9
0.3.8
0.3.7
0.3.6
0.3.5
0.3.4
0.3.3
0.3.2
0.3.1
0.3.0
0.2.0
0.1.0
BEAM-on-device mobile framework for Elixir
Current section
Files
Jump to
Current section
Files
guides/data.md
# Data & Persistence
Every generated Mob app ships with two persistence layers ready to use. They serve different purposes and work well together.
| | `Mob.State` | Ecto Repo |
|---|---|---|
| Backed by | `:dets` (OTP stdlib) | SQLite3 via `ecto_sqlite3` |
| Best for | App preferences, UI state | User records, structured data |
| API | `get/put/delete` | Schemas, queries, migrations |
| Setup | None — auto-started | `mix ecto.migrate` |
| Capacity | O(dozens) of keys | Millions of rows |
## Mob.State — app preferences
`Mob.State` is a key-value store for small amounts of app state that should survive kills and restarts. It is backed by [`:dets`](https://www.erlang.org/doc/apps/stdlib/dets.html) — Erlang's disk-based term storage from the OTP stdlib. No extra dependencies, no migrations, no setup. It is started automatically by the framework before `on_start/0` runs.
```elixir
# Persist any Elixir term
Mob.State.put(:theme, :citrus)
Mob.State.put(:onboarded, true)
Mob.State.put(:last_tab, :settings)
# Read back on next launch — returns default if not yet set
Mob.State.get(:theme, :obsidian) #=> :citrus
Mob.State.get(:missing, 0) #=> 0
# Remove a key
Mob.State.delete(:theme)
```
Writes call `:dets.sync/1` before returning, so data is on disk before the function returns — safe against `SIGKILL`.
Good candidates for `Mob.State`: selected theme, onboarding completion flag, last-opened tab, cached user ID, notification preferences. If you find yourself storing hundreds of keys or wanting to query across them, move that data to Ecto.
See `Mob.State` for the full API reference.
## Ecto — structured data
Every generated app includes [Ecto](https://hexdocs.pm/ecto) and [ecto_sqlite3](https://hexdocs.pm/ecto_sqlite3), giving you the full Ecto experience on-device backed by SQLite. If you have used Ecto with PostgreSQL in Phoenix, the API is identical for day-to-day use.
Your app's Repo is generated at `lib/my_app/repo.ex`. It is started in `on_start/0` and reads `MOB_DATA_DIR` (set by the native launcher) to place the database file in the platform's correct persistent storage directory — `getFilesDir()` on Android, `NSDocumentDirectory` on iOS.
### Defining a schema
```elixir
defmodule MyApp.Note do
use Ecto.Schema
import Ecto.Changeset
schema "notes" do
field :title, :string
field :body, :string
field :pinned, :boolean, default: false
timestamps()
end
def changeset(note, attrs) do
note
|> cast(attrs, [:title, :body, :pinned])
|> validate_required([:title])
end
end
```
### Writing migrations
```bash
mix ecto.gen.migration create_notes
```
```elixir
# priv/repo/migrations/20260422000000_create_notes.exs
defmodule MyApp.Repo.Migrations.CreateNotes do
use Ecto.Migration
def change do
create table(:notes) do
add :title, :string, null: false
add :body, :text
add :pinned, :boolean, default: false
timestamps()
end
end
end
```
```bash
mix ecto.migrate
```
### Querying from a screen
```elixir
import Ecto.Query
def mount(_params, _session, socket) do
notes = MyApp.Repo.all(from n in MyApp.Note, order_by: [desc: n.inserted_at])
{:ok, Mob.Socket.assign(socket, :notes, notes)}
end
def handle_info({:tap, :pin}, socket) do
note = hd(socket.assigns.notes)
{:ok, _} = MyApp.Repo.update(MyApp.Note.changeset(note, %{pinned: true}))
{:noreply, socket}
end
```
### SQLite differences from PostgreSQL
For most queries you will notice nothing different. A few things to be aware of:
- **`pool_size: 1`** — SQLite supports only one writer at a time. The Repo is pre-configured with a single connection; no tuning needed.
- **Limited `ALTER TABLE`** — SQLite cannot drop or rename columns in older versions. Write migrations that add columns or recreate tables instead.
- **No arrays or JSONB indexes** — use `string` fields with `Jason` encode/decode if you need nested data, or normalise into a separate table.
- **UUIDs stored as binary** — use `:binary_id` primary keys as normal; the adapter handles encoding transparently.
For a full reference see the [ecto_sqlite3 documentation](https://hexdocs.pm/ecto_sqlite3) and the [Ecto query API](https://hexdocs.pm/ecto/Ecto.Query.html).
## Running migrations on-device
The generated `on_start/0` starts the Repo but does not auto-run migrations. For most apps, running `mix ecto.migrate` in dev is sufficient — ship a database schema that matches what the app expects. If your app needs to migrate an existing on-device database (e.g. after an app store update), add a migration step before the Repo starts:
```elixir
def on_start do
Application.ensure_all_started(:ecto_sqlite3)
{:ok, _} = MyApp.Repo.start_link()
Ecto.Migrator.with_repo(MyApp.Repo, &Ecto.Migrator.run(&1, :up, all: true))
Mob.Screen.start_root(MyApp.HomeScreen)
end
```
The migration modules are compiled into your app's `.beam` files and copied to the device by the build scripts, so `Ecto.Migrator` finds them at the correct path via `:code.priv_dir/1`.