For users and operators wiring a Fief instance into a supervision tree. Assumes you have followed getting started and want the full option reference: every instance option, its default, and whether it is fixed cluster-wide or tuned per node.
Fief has no application environment. Everything is instance options passed in
your own supervision tree — the child spec {Fief, opts} you place beside
your repo. Nothing runs under the :fief application itself, and where the
option values come from (a literal, runtime.exs, secrets) is entirely
yours.
{Fief,
name: MyApp.Fief,
authority: {Fief.Authority.Postgres, repo: MyApp.Repo},
vnode_impl: {Fief.Key.VnodeImpl, []},
partitions: 1024,
lease_ttl: 5_000}The vnode_impl above selects the Fief.Key surface; swapping in
{Fief.Cache.VnodeImpl, []} configures a Fief.Cache instance
instead, with the rest of these options unchanged.
Options fall into two classes, and the distinction is operational, not cosmetic:
- Immutable cluster-wide options are written to the namespace's authoritative config by the first node ever to start it, and every later joiner is checked against that record. A mismatch is fatal at startup, before the joining node touches any shared state — the instance refuses to start. You cannot change one without a full stop of every node in the namespace.
- Per-node tuning options are read only by the node that sets them. They may differ from node to node and change on a rolling restart. Getting one wrong costs latency or churn, never correctness.
A short third group — name, authority, vnode_impl, leadership —
selects what the instance is rather than tuning it; those come first.
Structural options
These define the instance's identity and its pluggable layers.
| Option | Type | Default | Purpose |
|---|---|---|---|
name | atom | (required) | The namespace: the identity of the coordination domain. Scopes every row in the Fief tables, so instances coexist on one database and in one BEAM. Must be identical on every node participating in this instance. |
authority | {module, opts} or module | {Fief.Authority.Local, []} | The authority adapter (StateStore + Leadership + Presence). {Fief.Authority.Postgres, repo: MyRepo} in production. |
vnode_impl | {module, opts} or module | nil | The Fief.Vnode implementation. Exactly two first-class values, mutually exclusive per instance: {Fief.Key.VnodeImpl, impl_opts} for the Fief.Key layer, {Fief.Cache.VnodeImpl, impl_opts} for Fief.Cache. When absent, the instance owns nothing and runs no vnode, planner, or leadership subtree. |
leadership | :from_authority or false | :from_authority | :from_authority elects a planner leader through the StateStore. false disables election and the planner. Only :from_authority and false are accepted; any other value is fatal at startup. |
name is a namespace key, not a validated field — it selects which
authoritative record applies rather than being compared against one. Two nodes
with different names simply belong to different namespaces.
authority is a bare {module, opts} tuple, so its own options nest inside
it. For the Postgres adapter:
repo— (required) your Ecto repo module.bootstrap— boolean, defaulttrue. Whether the adapter self-checks the coordination tables exist at startup.
Design notes: docs/implementation.md §9 (all-instance-opts, no app env), §1
(the instance model). The authority contracts themselves:
the authority concepts page.
leadership: :from_authority requires the StateStore adapter to implement the
Fief.Leadership.Store primitives (try_lead/3 and leader/1); an adapter
that does not is rejected at startup with a naming error. Fief.Authority.Local
and Fief.Authority.Postgres both qualify.
Immutable cluster-wide options
Fixed for the life of the namespace. The first node writes them; joiners are validated and refuse to start on mismatch.
| Option | Type | Default | Purpose |
|---|---|---|---|
partitions | positive integer | 1024 | Number of virtual nodes the keyspace is split into by the immutable hash. |
hasher (impl) | module | Fief.Hasher.Default | The Fief.Hasher module mapping a key to a vnode (rem(hasher.hash_key(key), partitions)). The default (:erlang.phash2/2) accepts binaries only; a custom module (never a fun — it is fingerprinted, so it must be a stable, cross-node-comparable term) hashes other terms. |
partitions is a top-level option. hasher lives inside the vnode_impl
tuple's options — it is the fact both first-class impls fold into their impl
fingerprint, Fief.Key.VnodeImpl and Fief.Cache.VnodeImpl alike (the cache
half is grounded by test/fief/cache_test.exs —
describe "Fief.Cache.VnodeImpl.config_fingerprint/1 ({:hasher, module})"),
and the fingerprint is what the join check compares. Everything else in the
impl tuple (sweep pacing, pend bounds, blob caps — Fief.Key.VnodeImpl
options) is deliberately excluded from the fingerprint and is per-node
tuning.
partitions is the one irreversible decision: changing it remaps every key.
1024 is the default and the right answer for most clusters. Read
tuning before choosing otherwise. The mismatch check
names the offending field in the error, and the joining node never registers
or acquires a lease when it fails.
Design notes: docs/implementation.md §4 (join phase one, the fatal-before-shared-state
gate); the immutable set in code is partitions, the wire protocol version,
and the impl fingerprint — the library version is recorded for observability
but deliberately not compared, so upgrades are never forbidden by it.
Per-node tuning options
Read only by the node that sets them; safe to differ across nodes and to change on a rolling restart.
Lease and fencing
| Option | Type | Default | Purpose |
|---|---|---|---|
lease_ttl | positive integer (ms) | 5_000 | Lease lifetime. The failover floor: a dead node takes at least this long to be replaced. |
lease_margin | :auto or positive integer (ms) | :auto | How far before nominal expiry a node self-fences. :auto = renew_rtt_budget + 2 × clock_drift_bound. |
renew_rtt_budget | non-negative integer (ms) | 100 | Renewal round-trip allowance feeding the :auto margin. |
clock_drift_bound | non-negative integer (ms) | 50 | Per-node clock-drift bound feeding the :auto margin (doubled). |
renew_interval | positive integer (ms) | lease_ttl ÷ 5 (1_000 at the default TTL) | How often a node renews its lease. |
poll_interval | positive integer (ms) | 5_000 | Routing-table cache refresh cadence. Hints accelerate this; they never replace it. |
handle_fence_deadline | non-negative integer (ms) | 50 | Advisory budget before the unconditional kill, forwarded as deadline_ms to the fence advisories (the vnode-level handle_fence/2, and Fief.Key's for on_fence: :callback modules). Applies only when a vnode_impl is configured. |
leave_drain_interval | positive integer (ms) | 100 | How often a leaving node re-checks that it is no longer owner or donor anywhere. |
Shutdown
| Option | Type | Default | Purpose |
|---|---|---|---|
leave_on_shutdown | pos_integer() (ms), :infinity, or false | 20_000 | Whether tearing the instance's supervision tree down — System.stop/0, SIGTERM, application shutdown, or the parent terminating {Fief, opts} — runs a graceful Fief.Node.leave/1 first and blocks the tree's fall until the node drains (to :stopped, or :fenced if the failure path takes over mid-drain) or this deadline expires. The value is the drain's shutdown deadline; there is no second timer. false disables the drain outright. |
The drain runs through a tail sentinel child (Fief.Node.ShutdownDrain)
appended after the instance's live machinery, so it is the first thing
terminated when the tree falls — against a still-fully-alive node, with
agents to hand off and an authority to write through. The sentinel exists
only when all hold: leave_on_shutdown is not false; a vnode_impl
is configured (with none, nothing is owned, so there is nothing to drain);
leadership is not false (with no planner, nothing would ever drain the
node, so a wait could only time out); and the instance is not sim-joined (a
simulation steps its own shutdown by hand). Outside that shape the option is
silently inert.
On timeout the supervisor kills the sentinel and the tree falls exactly as
it would on a crash: the cluster sees an ordinary node failure and recovers
through the already-verified lease-expiry path
(failure) — no new failure mode, no partial-drain
state. Default 20 s deliberately sits under Kubernetes's default 30 s
terminationGracePeriodSeconds; keep the grace period comfortably above
leave_on_shutdown if you tune either. leave_drain_interval above is the
poll cadence both the sentinel and any explicit leave/1 caller use while
waiting for a drain to finish.
Design notes: docs/leave-on-shutdown.md is the decision record.
Planner and leadership
Read only when a vnode_impl is configured and leadership: :from_authority.
| Option | Type | Default | Purpose |
|---|---|---|---|
on_arbiter_loss | :freeze or :serve_last_epoch | :freeze | Posture when the arbiter is unreachable. Any other value is fatal at startup. |
max_concurrent_transfers | positive integer | 8 | Cap on simultaneously open transfers (prev_owner rows), enforced by the planner. |
rebalance_interval | positive integer (ms) | 1_000 | Planner reassignment cadence. |
rebalance_jitter | non-negative integer (ms) | rebalance_interval ÷ 10 (100 at the default) | Random spread added to the rebalance cadence. |
leadership_ttl | positive integer (ms) | 5_000 | Leadership lease lifetime. Expiry costs a leaderless interval, never safety. |
campaign_interval | positive integer (ms) | leadership_ttl (5_000) | How often a follower campaigns for the empty seat. |
campaign_jitter | non-negative integer (ms) | leadership_ttl ÷ 10 (500) | Random spread on the campaign cadence. |
settle_retry_interval | positive integer (ms) | 200 | How often the settle relay re-forwards an unsettled transfer report. |
Routing
| Option | Type | Default | Purpose |
|---|---|---|---|
max_moved_hops | positive integer | 3 | How many {:moved, ...} redirects a caller follows before giving up. |
fallback_jitter | non-negative integer (ms) | 50 | Random spread before a caller re-reads authoritative routing after a miss. |
Vnode-impl tuning (Fief.Key.VnodeImpl)
These live inside the vnode_impl tuple's options and are excluded from the
fingerprint — per-node, freely varied.
| Option | Type | Default | Purpose |
|---|---|---|---|
sweep_rate | positive integer | 100 | Keys migrated per sweep tick. |
sweep_interval | positive integer (ms) | 1_000 | Sweep tick cadence. Together with sweep_rate this sets the drain speed on handover. |
pull_retry_interval | positive integer (ms) | 1_000 | Retry cadence for a stalled lazy pull. |
max_pending_per_key | positive integer | 128 | Cap on messages buffered per key while it is being pulled. |
max_blob_size | positive integer (bytes) | 65_536 | Cap on a single extracted-state blob (a freeze-time bundle counts against it). |
extract_deadline | positive integer (ms) | 5_000 | Donor-side budget for keys to extract at freeze. A key that hasn't extracted when it expires is killed and rebuilt from durable truth on the new owner — what bounds a transfer against a flooded or stuck key. See the key lifecycle. |
channel | module | Fief.Transfer.Channel.InBand | Transport for key-state transfer payloads. |
Fief.Cache.VnodeImpl has no per-node tuning options today — get/put/
delete only; see cache.md's roadmap for the TTL/eviction
knobs planned as fast-follows.
Fail-fast: mounting {Fief, opts}
Fief.Supervisor — the process this child spec starts — does not hide a
sick node behind an internal reboot. It is a thin outer supervisor over the
instance's live machinery (Fief.Kernel, mounted significant: true, restart: :temporary) and the shutdown-leave sentinel above. The outer never
retries a crashed kernel: auto_shutdown: :any_significant means a kernel
exit — its own rest_for_one restart budget already exhausted, local
recovery already tried and failed — brings the whole instance down, and
Fief.Supervisor itself exits with reason :shutdown.
That makes the child spec you write for {Fief, opts} the place recovery
policy actually lives, exactly as with any other :permanent / :transient
/ :temporary child:
:permanent— the default (Fief'schild_spec/1leaves:restartunset, soSupervisorapplies it) — your tree always restarts the instance, including after this fail-fast:shutdown.:transient— restarts only on an abnormal exit. Because a kernel exit is reported as:shutdown, a:transientinstance is not restarted by this path; choose it if a kernel that has exhausted its own retries should stay down until something else intervenes.:temporary— never restarted by your supervisor at all.
Override the default with
Supervisor.child_spec({Fief, opts}, restart: :transient) (or :temporary)
in your children list.
This is a deliberate contract, not a bug: the cluster's own failure path —
lease expiry, takeover by survivors — is the verified fallback for a node
that can no longer serve (failure), and a
persistently sick node should become visibly dead quickly rather than flap
through a nested restart loop generating lease and membership churn the rest
of the cluster can see. Design notes: docs/leave-on-shutdown.md §2.6,
§2.6.1.
Caveats on the load-bearing options
lease_ttl is your failover floor. A node that dies is not replaced until
its lease expires at the arbiter, so recovery of its keys takes at least
lease_ttl, and no hint channel or configuration can beat it. Shorter TTLs
recover faster but renew more often and tolerate less clock drift and Postgres
latency; 3–5 seconds is comfortable against a healthy arbiter. See
guarantee 3, downtime over inconsistency.
lease_margin must be smaller than lease_ttl. The margin is the window
in which a node self-fences before its lease could expire at the arbiter, so
a margin at or above the TTL would fence the node before its lease could ever
be live — this is validated and fatal at startup. The :auto value doubles
the clock-drift bound deliberately: drift can swing between extremes across a
measurement interval, and the 1× margin has a model-checked counterexample.
Design notes: docs/implementation.md §2; specs/FiefLease.tla.
handle_fence_deadline must fit strictly inside the margin. The advisory
handle_fence/2 callback runs before the unconditional kill, and the whole
kill must complete inside lease_ttl − lease_margin so it finishes before the
lease can expire at the arbiter. A deadline at or above the margin is
validated and fatal at startup. The advisory budget is advisory; the kill that
follows it is not — see fencing modes.
on_arbiter_loss is consulted only with leadership enabled. The
planner-side posture is always to freeze — an unreachable arbiter accepts no
CAS writes anyway. :serve_last_epoch names the node-side behavior, which is
already the de-facto default: nodes serve their last-observed routing table
until the fence clock decides otherwise. Both accepted values produce the same
freeze-writes / serve-until-fence behavior today; a stricter node-side freeze
would be a future refinement. Design notes: docs/implementation.md §9.
on_fence is not an instance option. The fence mode (:terminate /
:continue / :callback) is set per key module with use Fief.Key, not in
the instance opts — see fencing modes.
What is deliberately not configurable
The sources of nondeterminism — time, randomness, cross-node transport, and the simulation progress marks — are compile-time seams, not instance options. They exist so a test harness can take over a deterministic simulation; no user ever configures them, and they sit on hot paths where an indirection would cost.
Fief.Seam(time, randomness, and transport) is a single module binding resolved withApplication.compile_env/3(default:Fief.Seam.Real). There is no runtime knob and no way to ship a simulation module by accident.Fief.Seam.checkpoint/2is gated by a compile-time boolean; in production builds its checkpoints expand to:okand vanish.
A handful of instance options exist purely as test seams and are not part of
the supported surface: sim, node_id, fence_action, impl_fingerprint,
and authority: {:external, module, handle}. They let one BEAM impersonate a
cluster under simulation. Do not use them in production. Design notes:
docs/implementation.md §10 (the determinism seams), §11 (the testing shape).
Verified by
test/fief/node_test.exsdescribe "join"— first-writer-wins config; apartitionsmismatch and aprotocol_version/impl_fingerprintmismatch are each fatal before shared state, naming the field.test/fief/node_test.exsdescribe "margin arithmetic"— the:automargin isrenew_rtt_budget + 2 × clock_drift_bound;resolve_margindefaults and overrides; alease_margin ≥ lease_ttland ahandle_fence_deadline ≥ margineach refuse to start.test/fief/key/vnode_impl_test.exsdescribe "join validation"— ahashermismatch is fatal at join, before shared state.test/fief/key_test.exsdescribe "config_fingerprint/1 ({:hasher, module})"— the fingerprint is{:hasher, Fief.Hasher.Default}by default; a fun hasher and a hasher withouthash_key/1are each fatal.test/guides/configuration_test.exs— the option set this page documents and the defaults it states for the load-bearing options are asserted against the validator code, so this page cannot drift from it.test/fief/node/shutdown_drain_test.exs—leave_on_shutdowndefault20_000and validation;describe "shape gating"— absent underfalse, underleadership: false, with novnode_impl, and for a sim-joined instance;describe "auto_shutdown"— a kernel exit bringsFief.Supervisordown with reason:shutdown, the fail-fast contract above.