This guide is for integrators who run the broker with PostgreSQL instead of Redis or SQLite: how to build the crate, create clients, share one database between processes, and run the Python integration harness.
Also read: backends.md (backend choice), routing-and-store-layout.md (tables — same layout as SQLite), operations-redis-sqlite.md (PostgreSQL backup and clear_*), errors-and-logging.md (NULL / None, stderr), bindings.md (C / Python over include/liner.h).
- Shared catalog — all cooperating processes use the same connection URL (like Redis, unlike per-process SQLite files).
- Durable SQL storage — routing, offline queues, and ack cursors live in relational tables (
topic_addr,conn_messages, …). - Same messaging API as other backends:
run,send_to,subscribe, TCP between peers.
Not a fit when you want zero server process and a single portable file — use SQLite. When you already standardize on Redis and only need an in-memory-style shared store, Redis remains the default build (no extra feature).
There is no receivers_json on new_postgres / lnr_new_client_postgres. Peers discover each other after run registers topic_addr, or via refresh_address_topic on senders (same model as Redis).
PostgreSQL support is optional in Cargo.toml:
cargo build --release --features postgresThe implementation uses the Rust postgres crate with NoTls (plain TCP to the server). TLS requires a different connector setup and is not wired in the stock build — see security-defaults.md.
C / Python: the shared library must be built with --features postgres or symbols such as lnr_new_client_postgres will be missing at link/load time.
Pass a libpq connection string, for example:
postgresql://user:password@127.0.0.1:5432/liner
Rules match other backends: non-empty unique_name, topic, and localhost (bind address). If the server is down or the URL is wrong, Client::new_postgres returns None / C NULL (usually without a detailed message on the creation path — check stderr and connectivity).
On first connect, the store runs CREATE TABLE IF NOT EXISTS … for the liner schema and sets lock_timeout to 5s for contended rows.
| Topic | PostgreSQL behavior |
|---|---|
| Catalog | Rows in topic_addr (topic → bind address → unique_name) are visible to all clients on the same URL. |
| Starting a mesh | Typical order: peer A run (registers its topic); peer B run; B refresh_address_topic(A's topic) then send_to. |
at_least_once_delivery |
With one shared URL, listener acks in conn_mess_number are visible to the sender on the same DB — same as Redis / shared SQLite file. |
| Multi-peer | One-to-many / many-to-many on a single URL is supported without JSON seed files (contrast with isolated SQLite files in using-sqlite.md). |
Internal seed_receivers exists on the Store trait for parity with SQLite (used in unit tests and store-level APIs). The public client constructors do not take catalog JSON for PostgreSQL.
#[cfg(feature = "postgres")]
use liner_broker::Client;
let mut c = Client::new_postgres(
"my_unique_name",
"my_source_topic",
"127.0.0.1:0",
"postgresql://user:pass@127.0.0.1/liner",
)
.expect("open postgres store");
c.clear_stored_messages();
c.clear_addresses_of_topic();
assert!(c.run(|_to, _from, _data| { /* … */ }));liner_broker::Liner::new_postgres wraps the C constructor (panics on null handle — prefer Client for Option).
Requires a build with feature postgres:
lnr_hClient c = lnr_new_client_postgres(
"my_unique_name",
"my_source_topic",
"127.0.0.1:0",
"postgresql://user:pass@127.0.0.1/liner"
);
if (!c) { /* invalid args or store open failure */ }
BOOL ok = lnr_run(c, my_receive_cb, my_udata);liner.loadLib("target/release/libliner_broker.so")
c = liner.Client.new_postgres(
"my_unique_name",
"my_source_topic",
"127.0.0.1:0",
"postgresql://user:pass@127.0.0.1/liner",
)If the .so was built without postgres, new_postgres raises that the symbol is missing.
After run, use bound_listen_addr() and unique_name() when configuring peers (catalog rows are written by registration, not JSON).
PostgreSQL uses the same table names as SQLite (see routing-and-store-layout.md). Examples:
SELECT * FROM topic_addr WHERE topic = 'my_topic';
SELECT COUNT(*) FROM conn_messages WHERE connection_key = 1;
SELECT v FROM conn_mess_number WHERE connection_key = 1;Integration tests that inspect the DB use LINER_TEST_POSTGRES_URL and psycopg2 — see debug-and-tests.md.
- Choose one database and URL; create role/database in PostgreSQL.
- Build:
cargo build --release --features postgres. - Process A:
new_postgres(...),runon topictopic_a(port0if you want an ephemeral port). - Process B:
new_postgres(...)with the same URL,runontopic_b. - B:
refresh_address_topic("topic_a"), thensend_to("topic_a", payload, true)(orfalseper your delivery policy). - A receives on its callback; reply with
send_toon B’s topic afterrefresh_address_topic("topic_b")if needed.
Reference: integration test shared_postgres_two_clients_send_to in src/client.rs; Python mirror under test/postgres/.
From the repository root:
export LINER_TEST_POSTGRES_URL='postgresql://user:pass@127.0.0.1/liner_test'
cargo build --release --features postgres
pip install psycopg2-binary
python3 test/postgres/run_integration.pyRust unit tests under store::postgres use the same environment variable.
- backends.md — compare Redis, SQLite, PostgreSQL.
- store-startup-failure-semantics.md — listener/sender store open at
run. - offline-delivery-and-message-numbers.md — queues and
connection_key. - debug-and-tests.md —
test/postgres/harness.