For operators setting up Fief.Authority.Postgres — the tables, the migration, and what your repo and supervision tree owe it. Assumes getting started; the dashboards and playbooks that read these tables live in the runbook.

Fief.Authority.Postgres implements the full authority — Fief.StateStore plus the Fief.Leadership.Store primitives — as raw SQL over five namespace-keyed tables in your database. There is no separate Fief database, no separate connection pool, and no Ecto schema: every statement runs through repo.query/2 in the caller's own process.

The five tables

One migration creates all five, and they are shared: every Fief instance pointed at the same database uses the same tables, distinguished only by the ns column (the instance name, stringified). Adding an instance later is rows, never DDL.

TableHoldsPrimary key
fief_leasesPer-node lease expiry — the failover clock(ns, node)
fief_membersMembership status (joining / active / leaving)(ns, node)
fief_tableVnode ownership: owner, prev_owner (open transfers), row epoch(ns, vnode)
fief_metaThe namespace epoch/term domain, plus the immutable first-writer-wins config (partitions, impl_fingerprint, and the full config term)ns
fief_leaderThe planner's leadership seat: holder, term, expires_atns

fief_table and fief_meta are what the runbook queries for routing/rebalance visibility; fief_leases and fief_members for cluster health; fief_leader for who is currently rebalancing.

Running the 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

Fief.Authority.Postgres.Migrations (not a runtime dependency of the adapter — it is only invoked from your own migration) ships the DDL as CREATE TABLE IF NOT EXISTS, so re-running up/0 is safe, including over tables another instance's migration already created. down/0 drops all five — shared by every namespace, so only run it when no Fief instance anywhere in the database still needs them.

Repo requirements

Fief.Authority.Postgres is duck-typed on opts[:repo]: anything exporting query/2 with Ecto's {:ok, %{rows:, num_rows:}} / {:error, term()} shape works. It calls no other Ecto API and defines no Ecto schemas, so fief itself compiles without Ecto — Fief.Authority.Postgres.Migrations is wrapped in Code.ensure_loaded?(Ecto.Migration) and only exists when your app pulls in ecto_sql. Both ecto_sql and postgrex are optional dependencies of fief, required only because you're using this adapter, not because the library needs them.

Start order: the repo outlives the instance

Your Ecto repo must be started before the Fief instance in your supervision tree, and — this is the part that is easy to get wrong in a multi-application release — it must not be stopped before the instance has finished stopping either:

children = [
  MyApp.Repo,               # starts first, stops last
  {Fief, name: MyApp.Fief, authority: {Fief.Authority.Postgres, repo: MyApp.Repo}, ...}
]

Two independent reasons make this a hard requirement, not just a sane default:

  • At startup, join-time config validation needs a live database — Fief.Authority.Postgres's init/1 ensures the namespace's fief_meta row exists as an idempotent bootstrap write, and an unreachable database there raises and fails the instance's start loudly (join-time validation is fatal by design regardless of the reason).
  • At shutdown, graceful leave on teardown writes status and observes settles through the authority as the last thing that happens before the instance's supervision tree falls. If the repo has already stopped by then, the drain cannot write leaving, cannot observe transfers settling, and degrades to the ordinary (safe, but cold-start-costing) node-failure path instead of a clean handoff.

Within one supervision tree, listing the repo before {Fief, opts} already gives you both — Elixir supervisors start children in order and stop them in reverse order, so the repo above Fief in the list starts first and stops last for free. The constraint only needs stating explicitly when the repo and the Fief instance live in different OTP applications: make sure the application boot order and :extra_applications/dependency graph keep the repo's application running until after the Fief instance's application has stopped.

PgBouncer / transaction mode is the native grain

Every operation here is one statement, never a multi-statement transaction — this adapter runs cleanly behind PgBouncer in transaction pooling mode, with no advisory locks, no session state, and no long-lived transactions to worry about. Compare-and-swap (cas_assign/cas_settle) is a single statement whose data-modifying CTE on fief_meta checks and bumps the namespace epoch and ratchets the leader term atomically with the table write — a failed guard writes nothing, in one round trip. Lease and leadership expiry are judged against the database's own clock in the same statement (COALESCE($n::timestamptz, now())), so node clocks never appear in SQL and every node computes expiry identically no matter its own clock skew.

What runs where

The supervised child Fief.Authority.Postgres starts under the instance's authority name, but it does almost nothing itself: it is a small config holder — it owns %{repo, ns, partitions, ...} in a protected ETS table and nothing else. Every read and write runs in the calling process (the node's gen_statem, the planner, a key agent) as a direct repo.query/2 call — pooling and concurrency are your repo's business, not a bottleneck Fief introduces. {:error, :unreachable} is exactly the connection-level failure class (DBConnection connection errors); anything else the database rejects — a constraint violation, a SQL error — is raised, never disguised as a protocol result, because that is corruption or a bug, not a transient condition to route around.

Verified by

  • test/fief/authority/postgres_test.exs — the full Fief.StateStoreContract suite against this adapter (verbatim — the contract suite is the spec), plus describe "Postgres specifics" (vnode-bound rejection, atom node ids, set_unreachable, connection-error mapping), describe "namespace isolation..." (independent coordination domains sharing one database), describe "leadership primitives...", and describe "migrations" (up idempotent over existing tables, down/up round-trips, the five table names this page lists).
  • Fief.Authority.Postgres and Fief.Authority.Postgres.Migrations moduledocs — authoritative for the SQL and the table shapes.
  • Configuration — the authority: option's repo/bootstrap sub-options.