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.
| Table | Holds | Primary key |
|---|---|---|
fief_leases | Per-node lease expiry — the failover clock | (ns, node) |
fief_members | Membership status (joining / active / leaving) | (ns, node) |
fief_table | Vnode ownership: owner, prev_owner (open transfers), row epoch | (ns, vnode) |
fief_meta | The namespace epoch/term domain, plus the immutable first-writer-wins config (partitions, impl_fingerprint, and the full config term) | ns |
fief_leader | The planner's leadership seat: holder, term, expires_at | ns |
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()
endFief.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'sinit/1ensures the namespace'sfief_metarow 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 fullFief.StateStoreContractsuite against this adapter (verbatim — the contract suite is the spec), plusdescribe "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...", anddescribe "migrations"(upidempotent over existing tables,down/upround-trips, the five table names this page lists).Fief.Authority.PostgresandFief.Authority.Postgres.Migrationsmoduledocs — authoritative for the SQL and the table shapes.- Configuration — the
authority:option'srepo/bootstrapsub-options.