Skip to content

Group-repo putRecord: add swapRecord / optimistic-concurrency support (currently silent last-writer-wins) #24

Description

@hb-agent

Note — AI-generated analysis. This issue was produced by an automated AI code analysis (Claude Code) reading this repository at commit d4de5a1. Treat the claims and code pointers below as a starting point and review them carefully against the current source before acting — they may be incomplete or wrong.

Summary

Group-repo putRecord does not support a swapRecord (optimistic-concurrency) precondition, so concurrent writes to the same record — e.g. two admins editing an org profile at once — silently last-writer-wins instead of returning a conflict.

Current behavior

There is no swapRecord, swapCommit, or InvalidSwap handling anywhere in the service (empty across src/, lexicons/, tests/, docs/). Specifically:

  • The app.certified.group.repo.putRecord input lexicon declares only repo, collection, rkey, record — no swapRecord/swapCommit (lexicons/app/certified/group/repo/putRecord.json).
  • The handler casts the body to exactly { repo, collection, rkey, record } and forwards that; it neither declares nor explicitly forwards a swap precondition (src/api/repo/putRecord.ts).

So a caller cannot express "only write if the current record is still CID X," and a CID mismatch is never detected.

Impact

Concurrent multi-writer edits to a group repo silently overwrite each other (last-writer-wins). This is most visible for the org/group app.bsky.actor.profile record, where two admins editing settings concurrently lose one set of changes with no error.

What's already in place (so the fix is small)

The error-relay half already works: proxyToPds forwards PDS 4xx errors to the caller preserving the error discriminator (throw new XRPCError(err.status, err.message, err.error) in src/api/util.ts). So if a swapRecord precondition were forwarded to the group's PDS and it returned InvalidSwap (400), the service would already relay it as a 400 carrying InvalidSwap.

Proposed change

  1. Add swapRecord (and ideally swapCommit) to the app.certified.group.repo.putRecord input lexicon (mirroring com.atproto.repo.putRecord).
  2. Forward the precondition explicitly from the handler to the PDS agent in src/api/repo/putRecord.ts.

No change needed to error handling — the InvalidSwap 400 relay already exists via proxyToPds. (Consider the same for repo.deleteRecord if delete-with-swap is desired.)

Context

Surfaced while answering a downstream contract question from certified-app (hypercerts-org/certified-app#112, tracker judgment-006). The app side already handles the atproto InvalidSwap discriminator and forwards swapRecord on the group profile path, but it's a no-op until the service honors it. Until this lands, group-repo putRecord should be treated as last-writer-wins.

Pointers: lexicons/app/certified/group/repo/putRecord.json, src/api/repo/putRecord.ts, src/api/util.ts (proxyToPds). Analysis only — no changes made.

Metadata

Metadata

Assignees

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions