Transactions
Perform multiple mutations on one or more documents in an atomic manner for better performance at scale.
Why Use Transactions
You need to update multiple documents as a single atomic operation, where either all changes succeed or none do. Without transactions, a failure mid-way through a series of updates could leave your data in an inconsistent state. For example, transferring a balance between two accounts requires both the debit and credit to complete together.
Overview
To perform a transaction in Squid, use the runInTransaction method provided by the squid object. This method
takes a callback function as a parameter, which will be executed within the context of a transaction.
await squid.runInTransaction(async (transactionId: string) => {
const user1 = squid.collection<User>('users').doc('user_1_id');
const user2 = squid.collection<User>('users').doc('user_2_id');
await user1.update({ name: 'Alice' }, transactionId);
await user2.update({ name: 'Bob' }, transactionId);
});
When applying changes inside a transaction, these changes will not reflect immediately, but they will reflect
optimistically once the callback function completes.
This is in contrast to applying a mutation without a transaction, where changes are applied immediately and optimistically. Any mutation that is applied as part of a transaction will resolve immediately. In order to make sure the changes are applied on the server, you should wait for the promise returned from runInTransaction to resolve.
Core Concepts
Transactions and queries
Squid does not support queries from inside a transaction. For example, the following code will cause a deadlock in the transaction:
const user1 = squid.collection<User>('users').doc('user_1_id');
await squid.runInTransaction(async (transactionId: string) => {
// This query inside the transaction will cause a deadlock
const user1Data = await user1.snapshot();
await user1.update({ loginCount: user1Data.loginCount + 1 });
});
Instead, fetch any necessary data before the transaction begins:
const user1 = squid.collection<User>('users').doc('user_1_id');
const user1Data = await user1.snapshot();
await squid.runInTransaction(async (transactionId: string) => {
if (user1Data) {
await user1.update({ loginCount: user1Data.loginCount + 1 }, transactionId);
}
});
Cross-connector transactions
When applying a transaction on multiple connectors, each connector will be updated atomically. However, it is possible that one connector updates successfully while the other fails. There is no support for cross-connector atomic updates.
Cross-connector transactions provide per-connector atomicity, not global atomicity. Design your data model accordingly.
Error Handling
| Error | Cause | Solution |
|---|---|---|
| Deadlock | Performing a query inside the transaction callback | Fetch all required data before calling runInTransaction |
| Transaction timeout | The transaction callback took too long to complete | Keep transactions small and fast; move heavy computation outside the callback |
| Partial failure (cross-connector) | One connector succeeded but another failed | Design for per-connector atomicity; consider using a single connector for critical atomic operations |
| Security rule rejection | A mutation within the transaction was blocked by security rules | Verify the user has write permission for all collections involved |
Best Practices
-
Fetch data before the transaction to avoid deadlocks:
Client code// Fetch first
const accountA = await squid.collection<Account>('accounts').doc('a').snapshot();
const accountB = await squid.collection<Account>('accounts').doc('b').snapshot();
// Then transact
if (accountA && accountB) {
await squid.runInTransaction(async (txId: string) => {
await squid
.collection<Account>('accounts')
.doc('a')
.update({ balance: accountA.balance - 100 }, txId);
await squid
.collection<Account>('accounts')
.doc('b')
.update({ balance: accountB.balance + 100 }, txId);
});
} -
Keep transactions small by only including the mutations that must be atomic. Move computation and validation outside the callback.
-
Pass the
transactionIdto every mutation inside the callback. Forgetting to pass it causes the mutation to execute outside the transaction. -
Avoid cross-connector transactions when you need guaranteed all-or-nothing atomicity. Use a single connector for critical atomic operations.