db.transaction, and the same typed DbService API works inside it.
Standardised patterns. Zero guesswork.
db.transaction
db.transaction(async (tx) => ...) runs a function against a transactional copy of the whole data layer. The tx you receive has the same shape as db — every table service, fully typed — but every call runs inside one Postgres transaction. Return a value and it resolves; throw and the entire transaction rolls back.
Use
tx, not db, for every call inside the callback. A call on the outer db runs in its own connection and won’t be part of the transaction — so it won’t roll back with the rest.Where workflows live
A workflow that’s used in one place can live right in the endpoint handler. Once it’s reused — or just large enough to deserve a name — move it into the resource’smethods/ folder, where shared business logic lives:
db (or db.transaction) — no base class, no registration:
Inside a transaction, the full API still works
tx is the same typed surface as db, so reads, filters, soft deletes and updates all behave identically — they’re just scoped to the transaction. A typical accept-invite workflow reads a token, creates a user, and marks the token used, all atomically:
ORPCError from @orpc/server — rolls the whole thing back. The user is only ever created together with their account and a consumed token.
Mutation events and transactions
Each write inside a transaction still emits its typedMutationEvent through the event bus. Handlers run after the surrounding write returns, so keep transaction callbacks focused on the writes themselves — push notifications, analytics and socket pushes belong in <resource>/handlers/*.ts, reacting to the committed change rather than running inside the transaction.
