obsidian/wiki/payloadcms/database-transactions.md
2026-05-15 15:13:56 +01:00

3.8 KiB

title aliases tags sources created updated
Database — Transactions
payload-transactions
payload-db-transactions
payloadcms
database
transactions
postgres
mongodb
sqlite
raw/database__transactions.md
2026-05-15 2026-05-15

Overview

Payload wraps all data-changing operations in database transactions by default (where supported). If any error is thrown during a request, all DB changes in that request are rolled back atomically.

  • PostgreSQL / SQLite (with config): full ACID transactions
  • MongoDB: requires a replicaset connection (standalone instance → no transactions)
  • SQLite: transactions disabled by default — pass transactionOptions: {} to enable

How It Works

Each request starts a transaction and stores its ID at req.transactionID. Payload hooks and Local API calls that receive the same req automatically join the transaction.

const afterChange: CollectionAfterChangeHook = async ({ req }) => {
  // Joins the parent transaction — rolled back if the request fails
  await req.payload.create({
    req,          // ← key: pass req to opt in
    collection: 'my-slug',
    data: { some: 'data' },
  })
}

Async Hooks — Gotcha

If you fire-and-forget (no await), do not pass req:

Pattern Behavior
await call({ req }) Safe — joined to transaction, rolled back on error
call({ req }) (no await) Dangerous — failure returns misleading OK, data not committed
call({}) (no req) Safe — runs independently, not rolled back with parent
// WRONG — unawaited + same req = misleading success response
const dangerouslyIgnoreAsync = req.payload.create({ req, collection: 'x', data: {} })

// CORRECT — unawaited + no req = independent, isolated
const safelyIgnoredAsync = req.payload.create({ collection: 'x', data: {} })

Direct Transaction Control

For custom scripts and endpoints outside normal request flow:

const transactionID = await payload.db.beginTransaction()

try {
  await payload.update({
    collection: 'posts',
    data: { some: 'data' },
    where: { slug: { equals: 'my-slug' } },
    req: { transactionID },   // ← pass as minimal req object
  })

  await payload.db.commitTransaction(transactionID)
} catch (error) {
  await payload.db.rollbackTransaction(transactionID)
}

API:

  • payload.db.beginTransaction() → returns transactionID
  • payload.db.commitTransaction(id) → finalizes changes
  • payload.db.rollbackTransaction(id) → discards changes

Disabling Transactions

Adapter-level (all operations):

// In your DB adapter config
postgresAdapter({ transactionOptions: false })

Per-call (single operation):

await payload.update({
  collection: 'posts',
  data: { some: 'data' },
  where: { slug: { equals: 'my-slug' } },
  disableTransaction: true,
})

Key Takeaways

  • Payload transactions are on by default for all mutations — no opt-in needed for standard hooks
  • Always pass req in hooks to join the parent transaction; omit it for fire-and-forget calls
  • MongoDB needs a replicaset — standalone dev MongoDB has no transaction support
  • SQLite transactions are opt-in: transactionOptions: {}
  • Use beginTransaction / commitTransaction / rollbackTransaction for standalone scripts
  • disableTransaction: true skips transaction on a single Local API call

Source: raw/database__transactions.md — https://payloadcms.com/docs/database/transactions