For application developers adding Fief to an Elixir app. Assumes a working Ecto + Postgres setup and a supervision tree you control; no prior Fief knowledge.
By the end of this page you will have a Fief instance in your supervision
tree, a key module, and a call/cast round-trip — a process per key,
placed somewhere in your cluster, with the guarantees
holding underneath. This is the Fief.Key path; for a coherent
get/put/delete cache with no per-key processes, see Using
Fief.Cache instead.
Every snippet on this page is executed by
test/guides/getting_started_test.exs, which boots an instance with this
configuration and drives this exact key module. Where the test deviates
(its repo, instance name, partition count, and tightened timing intervals),
the deviation is named in that file's moduledoc.
1. Dependencies
# mix.exs
defp deps do
[
{:fief, "~> 0.1"},
# Fief itself has no hard dependency on Ecto — ecto_sql and postgrex
# are optional deps, required only by the Postgres authority adapter.
# A typical app already has both.
{:ecto_sql, "~> 3.10"},
{:postgrex, "~> 0.18"}
]
endOne platform requirement: the Fief.Key layer needs Erlang/OTP 28+
(it delivers the transfer freeze advisory as an EEP 76 priority message —
see the key lifecycle). The
floor is enforced at instance boot, so a too-old runtime fails loudly at
startup, not subtly under load. Elixir ~> 1.20 works as-is.
2. Create the coordination tables
Fief keeps all of its coordination state — leases, membership, the ownership table, leadership — in five small Postgres tables, created from your own migration:
defmodule MyApp.Repo.Migrations.AddFief do
use Ecto.Migration
def up, do: Fief.Authority.Postgres.Migrations.up()
def down, do: Fief.Authority.Postgres.Migrations.down()
endThe DDL is idempotent, and one set of tables serves every Fief instance sharing the database — adding an instance later is rows, never DDL. Details and operational queries: operating the Postgres adapter.
3. Start an instance
Fief is instance-based, Ecto/Oban-style: nothing runs under the :fief
application itself. You start instances in your own supervision tree:
# in MyApp.Application.start/2
children = [
MyApp.Repo,
{Fief,
name: MyApp.Fief,
authority: {Fief.Authority.Postgres, repo: MyApp.Repo},
vnode_impl: {Fief.Key.VnodeImpl, []},
partitions: 1024,
lease_ttl: 5_000},
# ... the rest of your tree
]Three of these options deserve a sentence before you copy them:
nameis the namespace — the identity of the coordination domain. It must be the same on every node participating in this instance, and it scopes every row in the Fief tables, so multiple instances coexist on one database and in one BEAM.partitionsis the one irreversible decision. The keyspace is split into this many virtual nodes by an immutable hash; changing it remaps every key. The first node ever to start the namespace writes it to the database, and every later joiner is checked against it — a mismatch is fatal at startup, before the node touches shared state. 1024 is the default and the right answer for most clusters; read tuning before choosing anything else.lease_ttlis your failover floor. A node that dies takes at least this long to be replaced, and no configuration can beat it — see guarantee 3. 3–5 seconds is comfortable against a healthy Postgres.
vnode_impl is the surface choice: {Fief.Key.VnodeImpl, []} above selects
the Fief.Key layer this page walks through; {Fief.Cache.VnodeImpl, []} is
the other first-class value, selecting Fief.Cache instead. An
instance runs exactly one.
Everything else has sensible defaults; the full option reference is configuration.
One thing Fief deliberately does not do: connect your nodes. Cross-node sends never auto-connect, and joining verifies distribution reachability rather than creating it — run libcluster or equivalent so the nodes of the cluster are meshed before Fief instances start. (Advice; the reachability check itself is join phase one — see join and leave.)
4. Write a key module
A key module defines what a key is: how its process starts, how it serves messages, and how its state moves between nodes.
defmodule MyApp.Counter do
use Fief.Key # on_fence: :terminate is the default
# Every incarnation starts here. :fresh — no in-memory state survived
# (first touch, or recovery after a crash or node death): locate durable
# truth, or create it. Here there is none, so create.
@impl true
def init(_key, :fresh, _ctx), do: {:ok, 0}
# {:residual, blob} — a planned handover shipped the previous
# incarnation's extract_state/1 product here: rehydrate from it.
def init(_key, {:residual, n}, _ctx), do: {:ok, n}
# Serve one message.
@impl true
def handle_message(:incr, _from, n), do: {:reply, n + 1, n + 1}
def handle_message(:get, _from, n), do: {:reply, n, n}
# Planned handover: serialize on the old owner. The blob is opaque to
# Fief; it round-trips into init's {:residual, blob} on the new owner.
@impl true
def extract_state(n), do: {:ok, n}
endKeys are binaries. (Other terms are supported via a custom hasher module
— see the key lifecycle.) You never
start, name, or look up these processes yourself: routing by key is the whole
interface, and the process starts on first touch, on whichever node currently
owns the key.
When each init/3 shape runs — and why :fresh must mean "locate durable
truth, or create it" rather than "this key is new" — is the subject of the
key lifecycle, along with the rest of the contract this
page skips (a key process can also have its own handle_info/2, for timers
and subscriptions). Read it before shipping; the short version is that
durable storage is your job, because a counter like this one loses its
value when a node dies. That is guarantee
territory: Fief moves and fences
processes, it does not persist their state.
5. Round trip
iex> MyApp.Counter.call(MyApp.Fief, "user:42", :incr)
{:ok, 1}
iex> MyApp.Counter.call(MyApp.Fief, "user:42", :get)
{:ok, 1}
iex> MyApp.Counter.cast(MyApp.Fief, "user:42", :incr)
:ok
iex> Fief.Key.owner_of(MyApp.Fief, "user:42")
{:ok, :"node@host"} # hint-grade: the presumed ownercall runs in the calling process — there is no router bottleneck — hashes
the key to a vnode, sends to the presumed owner, and transparently follows
{:moved, ...} corrections if ownership has changed. It returns {:ok, reply} or {:error, reason}; the :timeout you must handle means unknown
outcome, not "not executed" — see delivery
semantics.
Single-node development
Everything above works on one node with no Postgres at all:
Fief.Authority.Local implements the same authority contracts in-memory and
is the default when no authority: is given. iex -S mix just works:
{Fief, name: MyApp.Fief, vnode_impl: {Fief.Key.VnodeImpl, []}, partitions: 64}Local is a reference implementation and test substrate, not a production
authority — it is itself a single point of failure with none of Postgres's
durability. Its own docs say the same.
Where next
- The key lifecycle — every way an incarnation begins and ends, the two mailboxes (routed and validated vs. your own), and the freeze that moves a key between nodes.
- Fencing modes — what happens to your processes when a node can no longer prove it owns them.
- Guarantees — exactly what you were just promised, with scope conditions and the tests behind each claim.
- Operations — the five tables, dashboards, tuning, and incident behavior.
- Using Fief.Cache — the other first-class surface: a coherent cache with no per-key processes.