# outpost Transactional outbox for Go. Durable messages, pluggable delivery, no driver types in the core. ## Model Three seams: `Appender` (write side, bound to a unit of work the adapter owns), `Store` (relay side: `Claim`/`Complete`/`Fail`), and `Producer` (what the relay does with a claimed message). The contracts live in the package documentation; this file is the tour. Handlers are written once and registered by name; any producer can feed them. ```go // request path: same unit of work as the business write err := outpost.Emit(ctx, myStore.InTx(tx), "email.send", SendEmail{To: "ada@example.com", Template: "welcome"}, outpost.Key("ada")) // worker path d := outpost.NewDispatcher() outpost.Handle(d, "email.send", func(ctx context.Context, e SendEmail) error { info, _ := outpost.InfoFromContext(ctx) return mailer.Send(ctx, render(e), mailer.IdempotencyKey(info.ID)) }) relay, err := outpost.NewRelay(store, outpost.ProducerFunc(d.Dispatch)) if err != nil { return err } relay.Run(ctx) ``` ## Guarantees - At-least-once delivery. Consumers must be idempotent; `Message.ID` is stable across retries and is the natural idempotency key. - Retries with configurable backoff, plus `RetryAfter` for provider rate limits and `Permanent` for failures that should not be retried. Both delays are floors: the relay spreads each retry by up to 50% so independent workers do not stampede a shared provider. - Messages are abandoned, not retried forever, after `WithMaxAttempts`. - The lease duration is derived from batch size, worker count, and delivery timeout: `(ceil(batch/workers)+1) * deliveryTimeout`. A claimed batch cannot outlive its lease while a worker is alive, so two workers cannot deliver the same message concurrently. The remaining duplicate window is a crash or hang between the provider accepting and `Complete`; no lease scheme closes that, which is what the provider idempotency key is for. - Stale leases cannot corrupt bookkeeping: `Complete` and `Fail` are ignored unless the claim's token still matches. ## Non-goals - Exactly-once delivery. - Ordering: a single relay with `WithWorkers(1)` processes claims in store order; nothing else is promised. - Codecs other than JSON. - Deferred acknowledgements (a delivery that completes later, for example a websocket client ack). The lease and token in `Claim` leave room for it. ## Adapters Not written yet. An adapter provides: - something returning an `Appender` bound to its unit of work, for example `InTx(tx)`, `InBatch(batch)`, `InTxn(txn)`; - a `Store` implementation (`Claim`, `Complete`, `Fail`) honoring the contract on the interface; - optionally a `Producer` for its broker.