Two users (or one user's two browser tabs) editing the same row is normal. Without care, one silently overwrites the other — a lost update.
Tab A reads task (title="A") Tab B reads task (title="A")
Tab A sets title="Alpha" ──▶ save
Tab B sets title="Beta" ──▶ save
Result: "Beta". Tab A's change is gone, silently.
Add a version (or reuse updatedAt). The client sends the version it last saw; the update only succeeds if it still matches.
1model Task {
2 // ...
3 version Int @default(0)
4}1const result = await prisma.task.updateMany({
2 where: { id, version: clientVersion },
3 data: { ...changes, version: { increment: 1 } },
4});
5if (result.count === 0) {
6 return conflict("Task was modified by someone else"); // 409
7}If someone changed the row first, version no longer matches, count is 0, and you return 409 Conflict instead of clobbering their work.
POST isn't idempotent, so a retried request can create duplicates. Let clients send an Idempotency-Key header:
POST /tasks
Idempotency-Key: 7f3c-...-a91b
Store the key with the created resource. If the same key arrives again, return the original result instead of creating a second row. This makes Create safe to retry over flaky networks.
The HTTP-native version of optimistic concurrency:
GET /tasks/42 → ETag: "v7"
PUT /tasks/42 → If-Match: "v7" (412 Precondition Failed if stale)
Same idea, expressed in standard headers — useful for public APIs.