Docs

Transactions, batches and field transforms

Change many documents at once (all or nothing), read-then-write safely under concurrency, and update fields atomically on the server.

Updated Oct 17, 2026Open in console

Every document has a version (1 when created, +1 on each write) and an update_time. Writes go through one atomic commit: up to your plan's writes per commit, applied together or not at all, each checked by your collection rules and row policies.

Field transforms

Transforms run on the server inside the commit, on the document as it is then — two clients incrementing a counter never lose an update.

TransformEffect
increment(n)Adds n (sets n when the field isn't a number)
maximum(n) / minimum(n)Keeps the larger / smaller value
arrayUnion(...)Adds elements that aren't already in the array
arrayRemove(...)Removes every matching element
serverTimestamp()The commit time (ISO 8601, UTC)
delete()Removes the field (with update or a merging set)
import { FieldValue } from '@sovaria/sdk'

await sov.doc('posts/p1').update({
  likes: FieldValue.increment(1),
  tags: FieldValue.arrayUnion('featured'),
  'stats.lastLikedAt': FieldValue.serverTimestamp(),
})
await sov.doc('users/alice').set({ prefs: { theme: 'dark' } }, { merge: true })   // deep merge
await sov.doc('users/alice').update({ draft: FieldValue.delete() }, { version: 7 }) // only if still v7

Batched writes

const batch = sov.batch()
batch.set(sov.doc('orders/o1'), { status: 'paid' })
batch.update(sov.doc('stock/sku1'), { qty: FieldValue.increment(-1) })
batch.delete(sov.doc('carts/c1'))
await batch.commit()   // all three or none

Transactions

A transaction reads documents, then writes based on what it read. The commit sends the versions it read: if any of those documents changed in the meantime, nothing is written and the SDK runs your function again (up to 5 times). Read everything before the first write.

await sov.runTransaction(async (tx) => {
  const from = await tx.get(sov.doc('accounts/a'))
  const balance = from.data!.balance as number
  if (balance < 10) throw new Error('Insufficient funds')
  tx.update(sov.doc('accounts/a'), { balance: balance - 10 })
  tx.update(sov.doc('accounts/b'), { balance: FieldValue.increment(10) })
})

Errors

Status · codeMeaning
409 ABORTEDA document the transaction read changed meanwhile — retry (the SDKs do).
409 ALREADY_EXISTScreate on a document that exists.
404 NOT_FOUNDupdate (or exists: true) on a document that doesn't exist.
412 FAILED_PRECONDITIONThe document isn't at the version / update_time you gave.
403 PERMISSION_DENIEDA collection rule or row policy refused one of the writes; nothing was written.
403 PLAN_LIMIT_REACHEDMore writes than docs.max_transaction_writes (500 on every plan).
Something unclear or missing on this page?Tell us