Skip to main content

Transactions

Transactions let you run multiple operations atomically — if any operation fails, all changes are rolled back.

Supported Adapters

AdapterTransactions
File❌ Not supported
Memory❌ Not supported
SQLite✅ Supported
D1✅ Supported

Basic Usage

try {
await db.transaction(async (tx) => {
const accounts = tx.collection('accounts', accountSchema)

// Withdraw from account A
await accounts.update({ id: 'a1' }, { balance: 50 })

// Deposit to account B
await accounts.update({ id: 'a2' }, { balance: 150 })
})
} catch (err) {
console.error('Transaction failed, all changes rolled back:', err)
}

How It Works

  1. Begin — The adapter starts a transaction (BEGIN IMMEDIATE for SQLite)
  2. Execute — All operations within the callback run against a transaction-scoped DB
  3. Commit — If the callback succeeds, all changes are committed atomically
  4. Rollback — If any operation throws, all changes are rolled back
// This is safe — if the second update fails, the first is rolled back
await db.transaction(async (tx) => {
const accounts = tx.collection('accounts', accountSchema)

await accounts.update({ id: 'a1' }, { balance: -100 })
// If this next line throws...
await accounts.update({ id: 'a2' }, { balance: 100 })
// ...both changes are undone
})

Transaction-Scoped DB

The callback receives a transaction-scoped DB instance. You must use this instance inside the transaction — it shares collections but uses the transaction adapter:

await db.transaction(async (tx) => {
// ✅ Use tx.collection()
const users = tx.collection('users', userSchema)
const posts = tx.collection('posts', postSchema)

await users.insert({ name: 'Alice' })
await posts.insert({ title: 'Hello', authorId: usersResult.id })
})

Complete Example: Bank Transfer

import { createDB, schema, t, sqliteAdapter } from '@sajjadbzn/jcell'

const accountSchema = schema({
id: t.id(),
name: t.string(),
balance: t.number(),
})

const db = createDB({
adapter: sqliteAdapter({ path: './bank.db' })
})

const accounts = db.collection('accounts', accountSchema)

async function transfer(fromId: string, toId: string, amount: number) {
await db.transaction(async (tx) => {
const txAccounts = tx.collection('accounts', accountSchema)

const from = await txAccounts.firstOrFail({ id: fromId })
if (from.balance < amount) {
throw new Error('Insufficient funds')
}

await txAccounts.update({ id: fromId }, { balance: from.balance - amount })
await txAccounts.update({ id: toId }, { balance: from.balance + amount })
})
}

// Safe transfer
await transfer('a1', 'a2', 50)

D1 Transactions

For Cloudflare Workers with D1, transactions use D1's batch API:

import { createDB, schema, t, d1Adapter } from '@sajjadbzn/jcell/d1'

const db = createDB({
adapter: d1Adapter({ binding: env.DB })
})

// Same API as SQLite
await db.transaction(async (tx) => {
const items = tx.collection('items', itemSchema)
await items.insert({ name: 'Widget', price: 9.99 })
await items.insert({ name: 'Gadget', price: 19.99 })
})