Event-Driven Architecture on Postgres in 2026
You don't need Kafka. Postgres ships LISTEN/NOTIFY, logical replication, and the outbox pattern. The 2026 guide to event-driven on the database you already have.
Event-driven architecture suffers from premature complexity. Teams reach for Kafka or Pulsar at scale they don't have, paying operational tax for years before they need it. Postgres has the primitives for most real event-driven needs; you ship faster and graduate later if you actually have to.
Why Postgres for events
Three things Postgres gives you:
- LISTEN/NOTIFY: pub/sub semantics over a connection. Real-time notifications when something happens.
- Logical replication: stream changes from a publication to subscribers. The basis of every Supabase Realtime dashboard and most CDC pipelines.
- The outbox pattern: events written in the same transaction as the business data, drained by a worker.
Together these cover ~90% of the "we need events" use cases.
LISTEN/NOTIFY
The simplest mechanism. A trigger fires NOTIFY on a channel; subscribers LISTEN.
-- Trigger: when an order is created, NOTIFY
CREATE OR REPLACE FUNCTION notify_order_created()
RETURNS trigger LANGUAGE plpgsql AS $$
BEGIN
PERFORM pg_notify('orders', json_build_object('id', NEW.id, 'tenant_id', NEW.tenant_id)::text);
RETURN NEW;
END $$;
CREATE TRIGGER orders_created_notify
AFTER INSERT ON orders
FOR EACH ROW EXECUTE FUNCTION notify_order_created();Application side (Node + postgres-js):
import postgres from "postgres";
const sql = postgres(process.env.DATABASE_URL!, { max: 1 });
const listener = await sql.listen("orders", (msg) => {
console.log("new order:", msg);
});The outbox pattern
For durable, replayable events. Every transaction that updates business data also inserts an event row into an outbox table. A worker drains the outbox and dispatches the events (HTTP webhook, queue, downstream service).
CREATE TABLE outbox (
id bigserial PRIMARY KEY,
occurred_at timestamptz NOT NULL DEFAULT now(),
topic text NOT NULL, -- 'order.created'
payload jsonb NOT NULL,
dispatched_at timestamptz
);
CREATE INDEX outbox_pending_idx
ON outbox (occurred_at)
WHERE dispatched_at IS NULL;// Run on a schedule (every second or so).
async function dispatch() {
const batch = await sql`
SELECT id, topic, payload
FROM outbox
WHERE dispatched_at IS NULL
ORDER BY occurred_at
LIMIT 100
FOR UPDATE SKIP LOCKED
`;
for (const ev of batch) {
await sendToDownstream(ev.topic, ev.payload);
await sql`UPDATE outbox SET dispatched_at = now() WHERE id = ${ev.id}`;
}
}FOR UPDATE SKIP LOCKED is the magic word; it lets multiple workers drain the same outbox concurrently without stepping on each other.
Logical replication
Built-in CDC. You create a publication on the source, a subscription on the target; Postgres streams INSERT/UPDATE/DELETE changes.
Common uses:
- Replicate to a read-only analytics database (so analytics queries don't affect the OLTP workload).
- Feed a search index.
pg_replication_slot+ a CDC consumer (Debezium, or a custom Go service) pipes changes to Elasticsearch / OpenSearch. - Power Supabase Realtime. The platform's realtime service reads logical replication and pushes WebSocket events.
When you actually need Kafka
Concrete signals:
- You need multiple consumers replaying the same event stream from arbitrary points. Postgres can do this with logical slots, but managing many slots gets operational.
- You're processing 50k+ events per second sustained. Above this point Postgres' WAL backpressure starts mattering and Kafka's log-as-disk design wins.
- You need cross-region replication of your event stream with strong ordering guarantees per partition.
If none of these apply, you don't need Kafka. You need the outbox pattern.
Suparbase is an admin workspace for Supabase. Encrypted credentials, server-side proxy, RLS debugger, SQL playground, AI assistant with diff-confirmed writes. Free tier for solo projects.
Related articles
- postgres · mvcc
MVCC in Postgres: When It Bites You
Postgres uses MVCC for concurrency. Most of the time you don't think about it. Then you do. Here are the four ways MVCC bites in production, and how to handle each.
Read article - postgres · observability
The Modern Postgres Observability Stack in 2026
The metrics that actually matter, the tools that work in 2026, and the alerts to set up before your database becomes someone else's problem.
Read article - postgres · multi-tenant
Building Multi-Tenant SaaS on Postgres: Schemas, RLS, and Pooling
Three battle-tested patterns for multi-tenancy on Postgres (and Supabase) in 2026: shared table with tenant_id, schema-per-tenant, and database-per-tenant. With migration, RLS, and pooling trade-offs.
Read article