> ## Documentation Index
> Fetch the complete documentation index at: https://alphaconsultings.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Runtime Transactions

> Native transaction and locking helpers for SQL and Mongo runtime flows.

# Runtime Transactions

Use runtime transactions when a service must keep several writes consistent.

## Public helpers

The root package now exposes:

* `transaction(connectionName, work, options?)`
* `lockedTransaction(connectionName, lockKey, work, options?)`
* `model.withTransaction(work, options?)`

## SQL transaction example

```ts theme={null}
import { transaction } from "@alpha.consultings/eloquent-orm.js";

await transaction("pg", async (tx) => {
  await tx.execute(
    "UPDATE appointments SET status = $1 WHERE id = $2",
    ["confirmed", 42],
  );

  await tx.execute(
    "INSERT INTO audit_logs (actor_id, event_code) VALUES ($1, $2)",
    [7, "appointment.confirmed"],
  );
});
```

## Transaction-bound model helpers

Use `Model.useTransaction(tx)` when you want ORM reads and writes to stay inside the active transaction.

```ts theme={null}
import { transaction } from "@alpha.consultings/eloquent-orm.js";

await transaction("pg", async (tx) => {
  const appointment = await Appointment.useTransaction(tx)
    .where("practitioner_id", 18)
    .where("appointment_date", "2026-04-11")
    .forUpdate()
    .skipLocked()
    .first();

  if (!appointment) {
    await Appointment.useTransaction(tx).create({
      practitioner_id: 18,
      appointment_date: "2026-04-11",
      appointment_time: "09:00",
    });
  }
});
```

Lock-helper rules:

* `forUpdate()` and `forShare()` are available only on transaction-bound SQL finders
* `skipLocked()` requires `forUpdate()` or `forShare()` first
* PostgreSQL: `FOR UPDATE`, `FOR SHARE`, `SKIP LOCKED`
* MySQL: `FOR UPDATE`, `FOR SHARE`, `SKIP LOCKED`
* SQLite: row-lock helpers are rejected
* MongoDB: row-lock helpers are rejected
* raw `tx.execute(...)` remains available for advanced SQL paths

Nested-helper rules:

* a model already bound to `tx` may call `.useTransaction(tx)` again as a no-op
* rebinding that model to a different transaction on the same connection fails fast
* hydrated finder results keep the active transaction/session for later `save()` / `delete()` calls

## SQL locking example

Use `lockedTransaction(...)` when one critical section must serialize concurrent writers.

```ts theme={null}
import { lockedTransaction } from "@alpha.consultings/eloquent-orm.js";

await lockedTransaction("pg", "appointments:slot:2026-04-11:09:00", async (tx) => {
  const existing = await tx.queryOne(
    "SELECT id FROM appointments WHERE practitioner_id = $1 AND appointment_date = $2 AND appointment_time = $3",
    [18, "2026-04-11", "09:00"],
  );

  if (existing) {
    throw new Error("slot already booked");
  }

  await tx.execute(
    "INSERT INTO appointments (practitioner_id, appointment_date, appointment_time) VALUES ($1, $2, $3)",
    [18, "2026-04-11", "09:00"],
  );
});
```

Current native lock support:

* PostgreSQL: advisory transaction lock
* MySQL: named lock + SQL transaction
* SQLite: no `lockedTransaction(...)` helper
* MongoDB: no `lockedTransaction(...)` helper

## Mongo transaction example

```ts theme={null}
import { transaction } from "@alpha.consultings/eloquent-orm.js";

await transaction(
  "mongo",
  async (tx) => {
    await tx.collection("wallets").updateOne(
      { user_id: "u1" },
      { $inc: { balance_minor: -500 } },
      { session: tx.session },
    );

    await tx.collection("ledger_entries").insertOne(
      {
        user_id: "u1",
        amount_minor: 500,
        direction: "debit",
      },
      { session: tx.session },
    );
  },
  {
    mongo: { maxCommitTimeMS: 5000 },
  },
);
```

Mongo note:

* the helper exposes `db`, `session`, and `collection(name)`
* each write must pass `{ session: tx.session }`
* multi-document transactions still require a transaction-capable Mongo deployment

## Model-owned convenience helper

```ts theme={null}
const user = new User();

await user.withTransaction(async (tx) => {
  if (tx.driver === "mongo") {
    throw new Error("expected SQL context");
  }

  await tx.execute("DELETE FROM sessions WHERE user_id = $1", [42]);
});
```

`withTransaction(...)` is a convenience wrapper over the model's configured `connectionName`.

## Practical rule

* keep transaction orchestration in services
* use `transaction(...)` for grouped writes
* use `lockedTransaction(...)` only for real race-sensitive flows
* do not put transaction control in controllers

## Related pages

* [Runtime Services](./services)
* [Runtime Controllers](./controllers)
* [Multi-Connection Strategy](../orm/multi-connection-strategy)
* [NoSQL (Mongo) Usage](../orm/nosql)
