At some point every backend team decides they need events, and someone opens the Kafka pricing page. For most products under a few hundred events per second, the boring answer has been sitting in your stack all along.
The four-table schema
An events table (append-only, JSONB payload, indexed by aggregate and timestamp), a subscriptions table naming each consumer, a cursors table tracking each consumer’s position, and a dead_letters table for the poison messages you will inevitably meet. Producers insert in the same transaction as their business write — which quietly deletes the entire outbox-pattern problem.
The 200 lines that matter
Consumers poll with FOR UPDATE SKIP LOCKED, batch by cursor, and advance transactionally, giving you exactly-once processing per consumer with retries and back-pressure — in TypeScript you can read in one sitting. On real hardware this comfortably clears 40-million-event days, which is more than most products will ever see.
When you outgrow it, your events are already well-shaped and moving to a broker is a plumbing change. Until then, it is one fewer distributed system between you and Friday evening.