Atomic operations
How the adapter implements Better Auth's consumeOne and incrementOne, the atomic Postgres SQL path, the atomicWrites policy and its limits.
Better Auth 1.7 requires every database adapter to implement two atomic primitives, and calls them on paths you are almost certainly using:
| Primitive | Used by |
|---|---|
consumeOne | Single-use credentials: email verification, password reset, magic links, email OTP, device-authorization codes |
incrementOne | Guarded counters: API-key quota and rate limits, two-factor backup codes, team member counts |
Postgres: one guarded SQL statement
On Postgres, eligible collections and supported predicates run as one guarded SQL statement over Payload's existing Drizzle connection:
consumeOneusesDELETE … RETURNING, returning the row only to the caller whose statement removed it.incrementOneusesUPDATE … SET counter = COALESCE(counter, 0) + delta … RETURNING, so concurrent increments accumulate without a read-then-write race.
Both statements select at most one identity and re-assert the caller's guard on the outer write, where Postgres rechecks it after waiting for a concurrent writer. A guard miss returns null. No migration is required.
Since 0.14.2. Earlier releases used the Local API path on every database.
The atomicWrites policy
payloadAdapter({
payloadClient: payload,
adapterConfig: { atomicWrites: 'auto' }, // default
})| Mode | Path for incrementOne and consumeOne |
|---|---|
'auto' (default) | Atomic Postgres when the sanitized collection has no trash, no versions/drafts, and no collection or field hooks other than field beforeDuplicate. Nested fields and read hooks are included in the check; empty hook arrays are allowed. Each refusal warns once per collection and reason, then uses the Local API. |
'always' | Atomic Postgres even with hooks, after you have reviewed them. Throws if an otherwise eligible collection or request cannot run atomically. Trash and versions/drafts still use the Local API, with a warning once per collection and reason. |
'never' | Always the Local API. |
The atomic path does not run Payload hooks
This includes validation, change, delete, operation and read hooks. For a hook-only refusal, review the hooks before choosing 'always' to bypass them.
- Payload's default
beforeDuplicatehooks on unique fields don't disqualify a collection; they never run on update or delete. - On the Payload 4 line, generated collections set
authorship: falseto keep injectedcreatedBy/updatedByhooks from disabling atomic writes. Re-enabling authorship throughcustomizeCollectionrestores those hooks and the default policy's Local API fallback. See the Payload 4 upgrade note. - Admin
baseFilter/baseListFilterdon't apply to these Local API operations and don't disqualify atomic writes. - Virtual,
joinanduifields have no stored columns and are ignored in the column checks. - The policy inspects the sanitized collection config once per collection and caches the decision for the adapter instance, so configure collections before using the adapter.
Logging
- Policy refusals (in
'auto'and'always') alwaysconsole.warnonce per collection and distinct reason. - SQL compatibility failures (missing connection, table or column metadata; unsupported stored fields or writes; untranslatable predicates) always
console.erroronce per collection and distinct reason.'auto'then uses the Local API;'always'throws. - With
enableDebugLogs: true, the adapter also logs the first atomic execution and first fallback per collection.
Under 'always', a predicate shape introduced by a future Better Auth release that the adapter can't translate will throw until the adapter is patched. That is why 'auto' is the default.
Supported shapes
Supported predicates are scalar eq, ne, lt, lte, gt, gte and scalar-array in, with the usual AND/OR grouping and explicit NULL semantics. Nested paths, JSON predicates, insensitive mode and other operators are not supported. Unsupported stored shapes (such as localized or has-many fields) are not supported either. Both log an error and fall back in 'auto', or throw in 'always'.
Malformed IDs fail closed. Invalid UUIDs and non-numeric serial primary or relationship IDs never match, including for ne, so an invalid exclusion can't broaden a credential write to every non-null row. This is stricter than Payload's query builder, which validates primary-key UUIDs but passes relationship UUIDs through. Explicit null keeps its existing semantics.
Local API fallback and its limit
Other databases (MongoDB, SQLite), and any collection or request that falls back, use the Local API:
consumeOnereads the row, then deletes it by id. The row is returned only if this call's delete removed it; if a concurrent caller got there first, Payload raises a 404 and the adapter returnsnull.incrementOnereads the row, then writes with a guard that re-asserts Better Auth's precondition (for exampleremaining > 0) and the counter values the write was computed from. A racing writer's update matches no row, so the loser re-reads and retries (up to five attempts) instead of overwriting the winner.
The fallback narrows the race; it does not remove it
Payload resolves a where by finding rows and then mutating them, so a window remains between its internal read and write. Under heavy concurrency on the same row, a counter update can be lost or a token consumed twice. If you need strict cross-process guarantees for API-key quota on this path, enforce them in the database with a CHECK constraint or a unique index.
Dependency
drizzle-orm is a regular dependency pinned to 0.45.2, matching Payload 3.92.1, and installs with the package. Static imports keep the atomic SQL path in production Next.js, Vercel and standalone bundles without runtime adapter discovery. A test compares the pin with the installed dev Payload, and a daily canary compares it with the latest published @payloadcms/drizzle requirement and opens an issue on drift.
CI runs a real Postgres 17 integration suite for both UUID and serial IDs: concurrent increments, single-use consumption, OAuth refresh-token guards, malformed identifiers, row-shape parity, and Better Auth's ten-failure TOTP lockout.
payloadAdapter
The Better Auth database adapter that stores auth data in Payload collections. Options, ID and database detection, and custom model names.
betterAuthCollections
The Payload plugin that generates auth collections from the installed Better Auth schema. Options, first-user admin, secret fields and customization.

