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"}
  ]
end

One 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()
end

The 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:

  • name is 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.
  • partitions is 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_ttl is 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}
end

Keys 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 owner

call 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.