Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

FerroFED’s own store

[store] connects FerroFED to one PostgreSQL database of its own. The store holds the state that must both survive a restart and be one on every replica. No specification governs the store: it is FerroFED’s own design.

When you need it

You need [store] in two cases:

A configuration in either case that sets no [store] refuses to start, and config check refuses it too, naming the key that needs the store and [store]:

federation.contact_point[0] needs FerroFED's own PostgreSQL store, and the configuration sets no [store]; add [store] with url or url_file

In every other case FerroFED runs with no PostgreSQL database. The embedded redb file of the default stored-query backend and the read-only files backend are unchanged.

The database and its role

Give FerroFED a database and a login role of its own. Never point [store] at a member CDR’s database or role, and never at a schema inside a FerroEHR cluster’s database, so a gateway credential never reaches clinical data. The tables live in the schema ferrofed. The role needs CREATE on the database to make the schema, or a ferrofed schema made for it on which it holds CREATE and USAGE. The store is tested against PostgreSQL 18.6.

What it holds

The store holds only state that must outlive the process and be shared by every replica:

TableWhat a row holdsWhen it goes
ferrofed.stored_query_definitiona stored-query definition of the postgres backend: name, version, the instant it was stored, its parameterised AQL (§12.7, N44)never: a stored version is immutable
ferrofed.emergency_accessthe emergency accesses granted to one health professional a national contact point relays, keyed by a SHA-256 digest of the professional’s identity: the instants granted inside the window, and the end of that window (The emergency access limit)at expires_at, the end of the window of the last access granted, by the sweep
ferrofed.dpop_proofone DPoP proof the gateway accepted, by the SHA-256 of its jti alone, never the value, so a replay to any replica is refused (RFC 9449 §11.1; DPoP)at expires_at, the end of the window the proof is accepted in, by the sweep
ferrofed.contact_point_dispensationwho dispensed one dispensation a national contact point wrote, keyed by the SHA-256 of the gateway’s id of it: the identity assertion’s organization-id and the facility identifier the dispensation names on performer.actor, so its discard is held to that dispenser on every replica (Dispensations and their discard)at expires_at, 24 hours after the dispensation, by the sweep; no discard is admitted after it
ferrofed.store_migrationone migration applied, and whennever

These stay out of the store: caches (revocation and OCSP answers, issuer keys), audit records (the audit spool), configuration (the revisions of [config] keep their own store) and clinical data. A table that a later release adds meets the same rule.

The keys

[store]
url_file = "/run/secrets/ferrofed/store-url"   # or url = "...": the connection string, a secret
ca_file = "/etc/ferrofed/store-ca.pem"         # optional: the CA the database's certificate is verified by
max_connections = 4                            # 1 to 64
timeout = "10s"                             # one connection, or one statement
sweep_interval = "1min"                          # how often expired rows are deleted
KeyDefaultWhat it sets
url, url_filenone, one is requiredthe connection string, a URL (postgres://ferrofed@db.example.org:5432/ferrofed?sslmode=require) or libpq key/value pairs; a secret, read at boot and trimmed when it comes from the file, never both
ca_filethe platform’s trusted rootsa PEM file of the CA certificates the database’s certificate is verified by, in place of the platform’s roots
max_connections4how many connections the gateway opens, 1 to 64
timeout_ms10000how long one connection, or one statement, may take; zero is refused
sweep_interval1minhow often the rows past their window are deleted; zero is refused

Give the credentials with url_file: write the whole connection string, password included, to a secret file only the gateway’s user reads, and keep url for a connection string that carries no password.

A key the table does not have refuses the configuration, naming it. A connection string that does not parse is refused naming its key, never quoting the string. The startup banner, the log and ferrofed report never show the string. Changing [store] needs a restart.

TLS is required

The store is reached over TLS under every profile, the development profile included. The connection string must set sslmode=require; a string that leaves sslmode unset or sets disable or prefer is refused by config check, the start and a reload, naming store.url or store.url_file:

store.url does not require TLS: FerroFED's store is reached over TLS under every profile; set sslmode=require

TLS uses rustls and always verifies the server’s certificate, against ca_file when it is set and the platform’s roots otherwise. A database that listens on a Unix socket alone cannot serve TLS, so give it a TCP listener.

Migrations

The migrations are part of the binary. At start, and each time a connection opens, the gateway takes a PostgreSQL advisory lock in one transaction, creates the schema ferrofed when it is absent, and applies each migration ferrofed.store_migration does not record, in order. Two replicas that boot together over one empty database apply each migration once: the second waits for the lock and then finds every migration recorded. A database that records a migration this release does not have was migrated by a newer FerroFED: the start is refused, naming both numbers, and the database is left as it was (Rollback). Migrations only move forward, and a released migration never changes.

Readiness and failure

The store has its own readiness indicator, store, which is down while the database does not answer a statement within the readiness check’s two seconds (Health probes). A down indicator takes the replica out of rotation.

Every probe asks the database, but the log tells you only what changed. The first probe that finds the store down logs the warning the store does not answer with the cause. While it stays down, the store still does not answer repeats at most once every five minutes, with down_for_s. The first probe that finds it answering again logs the store answers again, with how long it was down. The five minutes are FerroFED’s own choice.

A database that cannot be reached at start, or within timeout_ms, refuses the start. Once running, a request that needs the store while the database does not answer is refused, and is never answered from memory: a stored query read or PUT answers 500, names nothing in the body, and the log names the failure.

Expired rows

A table whose rows expire carries the end of each row’s window in a column expires_at. Every sweep_interval, the gateway deletes each row past its window from every such table of the schema ferrofed, so the store keeps a row no longer than its rule needs. The stored-query definitions have no window and are never swept.

Moving from [stored_queries] url

The postgres backend of the stored-query registry used to read its own connection string, [stored_queries] url or url_file. Those keys are gone since 0.0.17, and a configuration that still sets either refuses to start with an unknown key. Move the connection string to [store], point it at the database the backend already uses, and set sslmode=require:

[stored_queries]
backend = "postgres"

[store]
url_file = "/run/secrets/ferrofed/store-url"
ca_file = "/etc/ferrofed/store-ca.pem"

The table of definitions is the store’s first migration, so the gateway takes over the existing table with every definition it holds, and a version it held is refused a second PUT as before. Upgrading lists the step.