npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

prisma-atomic-lock

v1.0.0

Published

Framework-native pessimistic row-level locking (SELECT ... FOR UPDATE) for Prisma. Eliminates database race conditions such as double-spending and over-purchasing, with first-class helpers for NestJS and Next.js.

Readme

prisma-atomic-lock

Pessimistic row-level locking (SELECT ... FOR UPDATE) for Prisma, wired natively into NestJS and Next.js.

Race conditions in "check-then-write" flows — decrementing stock, debiting a wallet balance, redeeming a coupon — happen because two concurrent requests both read the same row before either writes back. prisma-atomic-lock closes that window by taking a real row-level lock inside an interactive Prisma transaction before your business logic runs, so concurrent requests for the same row are serialized by the database itself instead of racing in application code.

  • prisma-atomic-lock/nest — a @AtomicLock() method decorator for NestJS services.
  • prisma-atomic-lock/next — a withAtomicLock() higher-order function for Next.js Server Actions and Route Handlers.
  • Dual package: ships both ESM and CommonJS builds with full type declarations, so it works with import or require either way.
  • Zero dependencies. @nestjs/common and @prisma/client are peer dependencies only.

Requires a database that supports SELECT ... FOR UPDATE — PostgreSQL, MySQL/MariaDB, or CockroachDB. SQLite and MongoDB do not support row-level locking and are not compatible with this library.

Install

npm install prisma-atomic-lock

@prisma/client is always required. @nestjs/common is only required if you use the /nest subpath.

How it works

Both entry points do the same thing under the hood:

  1. Open an interactive prisma.$transaction(async (tx) => { ... }).
  2. Inside it, run SELECT 1 FROM "<table>" WHERE "<idColumn>" = $1 FOR UPDATE using the row id you provide.
  3. Postgres/MySQL blocks any other transaction trying to lock the same row until this one commits or rolls back — so a second concurrent call for the same id simply waits its turn instead of reading stale data.
  4. Your business logic then runs inside that same transaction, using the transaction client (tx), so its reads and writes are part of the locked, atomic unit of work.

Different ids are not blocked by each other — only concurrent operations on the same row serialize.


NestJS: @AtomicLock()

import { Injectable } from '@nestjs/common';
import { PrismaService } from './prisma.service';
import { AtomicLock } from 'prisma-atomic-lock/nest';

@Injectable()
export class WalletService {
  constructor(private readonly prisma: PrismaService) {}

  @AtomicLock({ table: 'Account', keyParam: 'accountId' })
  async debit(accountId: string, amount: number) {
    const account = await this.prisma.account.findUniqueOrThrow({ where: { id: accountId } });

    if (account.balance < amount) {
      throw new Error('Insufficient balance');
    }

    return this.prisma.account.update({
      where: { id: accountId },
      data: { balance: { decrement: amount } },
    });
  }
}

That's it — no other code changes. Calling walletService.debit(accountId, amount) now:

  1. Locks the Account row matching accountId via FOR UPDATE.
  2. Makes this.prisma resolve to the active transaction client for the duration of the call, so every this.prisma.* call inside debit() participates in the same locked transaction.
  3. Falls back to the original client automatically once the call finishes — even if debit() throws.

Requirements

  • The host class must have a property literally named prisma holding your injected PrismaClient (or a PrismaService wrapping it). If it's missing, AtomicLock throws InternalServerErrorException when the method is called.
  • The decorated method must declare a parameter with the exact name given in keyParam. AtomicLock inspects the method's source at class-definition time to resolve keyParam to a positional argument — if no parameter matches, it throws InternalServerErrorException immediately (at decoration time, not at call time).

Options

interface AtomicLockOptions {
  table: string;        // table name used verbatim in the raw SQL query
  keyParam: string;     // name of the method parameter holding the row id to lock
  idColumn?: string;    // column matched against — defaults to "id"
  transactionOptions?: Record<string, unknown>; // forwarded to prisma.$transaction(), e.g. { timeout, maxWait }
}

Under heavy contention, many callers can end up queuing for the same row lock — Prisma's default interactive-transaction timeout is 5 seconds, which can be too short for high-contention rows (e.g. a flash sale). Raise it via transactionOptions:

@AtomicLock({ table: 'Item', keyParam: 'itemId', transactionOptions: { timeout: 15000, maxWait: 15000 } })
async purchase(itemId: string, quantity: number) { /* ... */ }

Concurrency safety

AtomicLock is safe on the default singleton Nest provider scope under concurrent load — no Scope.REQUEST needed. Internally it does not mutate this.prisma as a plain instance field (which would race across concurrent calls on the same singleton instance); it uses AsyncLocalStorage to scope the active transaction client to each call's own async continuation, so concurrent calls — on the same row or different rows, through the same or different methods — never observe each other's transaction. This was verified under real concurrent load (30 simultaneous requests against 10 units of stock, exactly 10 succeeded, 0 oversold) — see examples/nestjs-demo.


Next.js: withAtomicLock()

// app/actions/purchase.ts
'use server';

import { prisma } from '@/lib/prisma';
import { withAtomicLock } from 'prisma-atomic-lock/next';

export const purchaseItem = withAtomicLock(
  { prisma, table: 'Item' },
  async (tx, itemId: string, quantity: number) => {
    const item = await tx.item.findUniqueOrThrow({ where: { id: itemId } });

    if (item.stock < quantity) {
      throw new Error('Not enough stock');
    }

    return tx.item.update({
      where: { id: itemId },
      data: { stock: { decrement: quantity } },
    });
  },
);
// app/checkout/actions-consumer.tsx
import { purchaseItem } from '@/app/actions/purchase';

await purchaseItem(itemId, quantity);

withAtomicLock returns a function whose signature matches your original action exactly (id first, then whatever else you defined) — the only difference is that your action callback receives the locked transaction client tx as its new first parameter, with id and the rest of the arguments shifted after it.

Works identically in Route Handlers:

// app/api/purchase/route.ts
import { NextResponse } from 'next/server';
import { prisma } from '@/lib/prisma';
import { withAtomicLock } from 'prisma-atomic-lock/next';

const purchaseItem = withAtomicLock({ prisma, table: 'Item' }, async (tx, itemId: string, quantity: number) => {
  /* ... */
});

export async function POST(req: Request) {
  const { itemId, quantity } = await req.json();
  const result = await purchaseItem(itemId, quantity);
  return NextResponse.json(result);
}

Options

interface AtomicLockOptions<TClient> {
  prisma: TClient;      // your PrismaClient instance (any client exposing $transaction)
  table: string;        // table name used verbatim in the raw SQL query
  idColumn?: string;    // column matched against — defaults to "id"
  transactionOptions?: Record<string, unknown>; // forwarded to prisma.$transaction(), e.g. { timeout, maxWait }
}

TClient is inferred from whatever you pass as prisma — this works with the default @prisma/client output as well as Prisma 6/7 custom-output generated clients, since the library doesn't import a concrete PrismaClient type internally.

As with /nest, raise transactionOptions.timeout for rows that see heavy concurrent contention — this was verified under real concurrent load (30 simultaneous requests against 10 units of stock, exactly 10 succeeded, 0 oversold) — see examples/nextjs-demo.


Why not just use SELECT ... FOR UPDATE inline everywhere?

You can — this library just removes the boilerplate and the easy-to-miss mistakes: forgetting to run the lock query before the read, forgetting to run everything through the same tx, or forgetting to restore state afterward. prisma-atomic-lock gives you one line to opt a method or action into "this row's operations are strictly serialized," while leaving everything else in your codebase untouched.

Examples

Full, runnable apps that exercise @AtomicLock/withAtomicLock against a real PostgreSQL database (Prisma 7 + driver adapters):

Both were stress-tested by firing 30 concurrent HTTP requests at a row with only 10 units/dollars available: exactly 10 succeeded and 20 were correctly rejected, with the database left in a consistent final state, in both frameworks.

Compatibility

| Peer dependency | Supported versions | | --- | --- | | @prisma/client | ^4.0.0 \|\| ^5.0.0 \|\| ^6.0.0 \|\| ^7.0.0 | | @nestjs/common | ^8.0.0 \|\| ^9.0.0 \|\| ^10.0.0 \|\| ^11.0.0 (only if using prisma-atomic-lock/nest) |

Database: PostgreSQL, MySQL/MariaDB, or CockroachDB (anything supporting SELECT ... FOR UPDATE).

License

MIT