A PostgreSQL extension for the transactional outbox pattern: messages committed with your data, delivered by background workers to HTTP, Kafka, MQTT, Redis, AMQP and NATS.
ulak is a PostgreSQL extension for the transactional outbox pattern. You call ulak.send() inside the same transaction as your business write, the message lands in ulak.queue with the rest of the data, and background workers inside PostgreSQL deliver it afterwards. If the transaction rolls back, the message never existed. That's the whole idea, and it's the dual-write problem gone without a CDC stack.
I built it because I kept writing the same outbox relay in every service, with the same retry loop, the same backoff, the same dead-letter table, and it was never quite the same. ulak means "messenger" in Turkish. The queue write is atomic with your data, and retries, backoff, circuit breaking, the DLQ, archive and redrive live in the extension instead of in application code.
Workers poll ulak.queue with FOR UPDATE SKIP LOCKED, so many of them can drain the same queue without stepping on each other, and they dispatch straight from the database. HTTP is built in. Kafka, MQTT, Redis Streams, AMQP and NATS are optional adapters behind the same lifecycle, compiled in with ENABLE_KAFKA=1, ENABLE_MQTT=1, ENABLE_REDIS=1, ENABLE_AMQP=1 or ENABLE_NATS=1, each one needing its client library at build time. You get exactly-once writes to the local queue and at-least-once delivery to whatever is on the other end.
Use it when PostgreSQL is your source of truth and you want outbox semantics next to the data, or when you're tired of rebuilding webhook retries and DLQ handling per service. Don't use it if you already run Debezium and are happy with it, if your problem is large-scale event streaming rather than outbox delivery, or if you don't want worker activity and delivery policy living inside your database. Those are real trade-offs, not fine print.
It's a background worker extension, so PostgreSQL has to load it at startup:
shared_preload_libraries = 'ulak'
Then, in the database:
CREATE EXTENSION ulak;SELECT ulak.
The row commits with your transaction, a worker picks it up, delivers it, retries with backoff if the endpoint is down, and parks it in the dead-letter queue if it never comes back. The other adapters, the retry policy and the operations views are in the README. Supported PostgreSQL versions are 14 through 18.