Skip to content
All articles
Articlepostgreseventsarchitecture

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.

12 min read

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.

notify-on-write.sqlsql
-- 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):

ts
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).

outbox.sqlsql
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;
dispatch.tsts
// 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