Skip to content

PostgreSQL transport #266

Closed
Closed

Description

@adenhertog

A transport that needs only Postgres, a database-as-queue approach similar to pg-boss in Node. This lowers the barrier to entry: users can start with the database they already have, get the outbox in the same transaction for free, and move to a broker later.

  • Queue tables with SELECT ... FOR UPDATE SKIP LOCKED, and LISTEN/NOTIFY for low latency.
  • Pub/sub via a subscription table.
  • Native delayed delivery.
  • Pass the @node-ts/bus-test conformance suite.

Activity

  1. adenhertog commented on Oct 7, 2026

    @adenhertog
    ContributorAuthor

    Accepted design (maintainer, 2026-10-07)

    • Package: PostgresTransport, exported from @node-ts/bus-postgres.
    • Tables: shared, in the configured schema: transport_messages (keyed by queue), transport_queues, transport_subscriptions, transport_dead_letters.
    • Pub/sub: provision() writes the queue row and a subscription row per handled message (and custom-handler topic). It never deletes any. send and publish both fan out with one insert per subscriber, in one statement. sendToAddress inserts into the target queue.
    • Receiving: a lease. A short for update skip locked claim sets visible_at = now() + visibilityTimeout (default 30 s, configurable, no heartbeat) and a new lease token. A crashed process's message comes back when its lease ends. failedAttempts is deliveries − 1, like SQS.
    • Latency: LISTEN/NOTIFY on a dedicated connection, with a 1 s poll as the fallback. listen: false for PgBouncer in transaction mode.
    • Retries and dead letters: returnMessage sets visible_at. fail moves the row to transport_dead_letters with the bus-failure header. Redrive and purge are documented as SQL.
    • Delayed delivery: uses the existing outgoing-messages dispatcher. No bus-core change.
    • Exactly-once (enlisting the transport in the withOutbox() transaction) is a follow-up: postgres transport: exactly-once by enlisting the transport in the outbox transaction #343.
    • Provisioning: provision infrastructure at deploy time, not on application start #339's model: provision() creates, initialize() only checks, and the plan has SQL grants for the runtime role.
    • Cleanup: rows are deleted when acked. Dead letters are kept until removed.
    • Out of scope: per-queue tables, lease heartbeat, batched claims, poison-message backstop, redrive API, archive, strict ordering.
    • Tests: transportTests, outboxTests and inboxTests run against it.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requestroadmapPart of the project roadmap

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions