Guide

Transactions

Everything the body writes lands together, or none of it does — without threading a session through every call.

Setup

Transactions are powered by a database adapter. Pass it to Parse Server instead of a plain URI:

import {
  createVersionedMongoAdapter, CloudFunction, Transactional,
  withTransaction,
} from 'parse-server-kit';

// parse-server refuses to boot with an explicit database adapter unless a
// files adapter is named too. GridFS is what a Mongo install used anyway.
const {GridFSBucketAdapter} =
  require('parse-server/lib/Adapters/Files/GridFSBucketAdapter');

// Built as an object and cast — see the note below.
const options = {
  databaseAdapter: createVersionedMongoAdapter({
    uri: process.env.DATABASE_URI,
    collectionPrefix: '',
    mongoOptions: {},
  }),
  filesAdapter: new GridFSBucketAdapter(process.env.DATABASE_URI),
  directAccess: true,   // REQUIRED - see below
  // ...the rest of your options
};

const parseServer = ParseServer(options as any);
Two upstream quirks you will hit here Neither is something this library can fix, but both stop a transaction setup from starting, and neither message says what to do about it.

1. An explicit database adapter forces an explicit files adapter. parse-server 9 exits with "When using an explicit database adapter, you must also use an explicit filesAdapter." Naming GridFSBucketAdapter reproduces what you had before, since GridFS is the default for MongoDB.

2. The types and the runtime disagree about databaseURI. ParseServerOptions marks it required and does not special-case databaseAdapter, so leaving it out is a compile error. Passing both throws at runtime: "You cannot specify both a databaseAdapter and a databaseURI/databaseOptions/collectionPrefix." Nothing satisfies both, which is why the options object above is cast.
MongoDB onlyOn Postgres the feature is unavailable and says so at boot
Replica set requiredA standalone mongod refuses to open a transaction
directAccess: trueParse Server's default, but pin it explicitly

Using it

@CloudFunction({ methods: ['POST'] })
@Transactional()
static async placeOrder(req: Parse.Cloud.FunctionRequest) {
  await order.save(null, { useMasterKey: true });
  await inventory.save(null, { useMasterKey: true });
}  // both land, or neither

// Outside a cloud function, or for part of one:
await withTransaction(async () => { … });

Every save(), destroy() and query inside the body joins automatically. The session travels with the call chain through AsyncLocalStorage, so nothing has to be passed down.

Decorator order no longer matters @CloudFunction used to capture the method at the moment it was applied, so writing @Transactional() above it meant the registry kept the unwrapped method and the transaction silently never opened. Since 3.0 the registry re-reads the method when it registers it, so either order works. Below is still marginally better — the role check then runs outside the transaction, so an unauthorised request never opens one.

What it guarantees

Without directAccess there is no symptom A save() in cloud code becomes an internal HTTP request that arrives in a fresh async context with an empty store — so it writes outside the transaction. Every write succeeds and the rollback rolls nothing back. Nothing is logged, which is why the option is worth pinning rather than relying on its default.

Handling a conflict

try {
  await job.save(null, { useMasterKey: true });
} catch (error) {
  if (error instanceof Parse.Error && error.code === CONFLICT) {
    // Reload, re-apply, retry - or show CONFLICT_MESSAGE,
    // which is already written for an end user.
  }
  throw error;
}

A lost transaction race and a lost optimistic lock report the same code, because to the person looking at the screen they are the same event: somebody else got there first.