Repository Operations (Advanced)
Copy MarkdownThis guide covers low-level repository operations for advanced use cases. Most applications should use the high-level Bsky helpers instead.
Understanding Repositories
In AT Protocol, a repository is a collection of records stored in a user's Personal Data Server (PDS). Each record has:
- Collection: Record type (e.g., "app.bsky.feed.post")
- Rkey: Record key (unique identifier within the collection)
- Record: The actual data conforming to a lexicon schema
Repository Functions
All repository functions are in ProtoRune.Atproto.Repo.
Create Record
Create a new record in a collection:
alias ProtoRune.Atproto.Repo
{:ok, result} = Repo.create_record(session, %{
repo: session.did,
collection: "app.bsky.feed.post",
record: %{
"$type" => "app.bsky.feed.post",
"text" => "Hello from low-level API",
"createdAt" => DateTime.utc_now() |> DateTime.to_iso8601(),
"langs" => ["en"]
}
})
# Result contains:
# %{uri: "at://did/collection/rkey", cid: "bafy..."}Get Record
Fetch a specific record:
{:ok, record} = Repo.get_record(session, %{
repo: "did:plc:abc123",
collection: "app.bsky.feed.post",
rkey: "3kxyz..."
})Parameters:
:repo- DID of the repository owner:collection- Collection name:rkey- Record key:cid- (optional) Specific version by CID
Update Record
Update an existing record:
{:ok, result} = Repo.put_record(session, %{
repo: session.did,
collection: "app.bsky.feed.post",
rkey: "3kxyz...",
record: %{
"$type" => "app.bsky.feed.post",
"text" => "Updated text",
"createdAt" => DateTime.utc_now() |> DateTime.to_iso8601(),
"langs" => ["en"]
}
})Optional parameters:
:validate- Validate record against lexicon (default: true):swap_record- CID for optimistic concurrency control:swap_commit- Commit CID for atomic operations
Delete Record
Remove a record from a collection:
{:ok, result} = Repo.delete_record(session, %{
repo: session.did,
collection: "app.bsky.feed.post",
rkey: "3kxyz..."
})Optional parameters:
:swap_record- CID for optimistic concurrency control:swap_commit- Commit CID for atomic operations
List Records
List all records in a collection:
{:ok, result} = Repo.list_records(session, %{
repo: session.did,
collection: "app.bsky.feed.post",
limit: 50
})
# Result contains:
# %{records: [...], cursor: "..."}Optional parameters:
:limit- Max records to return (1-100):cursor- Pagination cursor:reverse- Reverse chronological order
AT-URI Format
AT-URIs identify records in the format:
at://[did]/[collection]/[rkey]Example:
at://did:plc:abc123xyz/app.bsky.feed.post/3kxyz789Components:
did:plc:abc123xyz- Repository owner's DIDapp.bsky.feed.post- Collection name3kxyz789- Record key
Working with Collections
Standard Collections
Common Bluesky collections:
# Posts
"app.bsky.feed.post"
# Likes
"app.bsky.feed.like"
# Reposts
"app.bsky.feed.repost"
# Follows
"app.bsky.graph.follow"
# Blocks
"app.bsky.graph.block"
# Profile
"app.bsky.actor.profile"Custom Collections
You can create custom collections if you've defined lexicons:
{:ok, result} = Repo.create_record(session, %{
repo: session.did,
collection: "com.example.custom.record",
record: %{
"$type" => "com.example.custom.record",
"data" => "custom value"
}
})Record Keys
Record keys (rkeys) are base32-encoded TIDs (timestamp identifiers) by default:
# Example rkey
"3kxyz789abc"You can optionally specify custom rkeys:
{:ok, result} = Repo.create_record(session, %{
repo: session.did,
collection: "app.bsky.feed.post",
rkey: "custom-key-123", # Custom rkey
record: record_data
})However, using TID-based rkeys is recommended for proper ordering and collision avoidance.
Content Identifiers (CIDs)
CIDs are cryptographic hashes of record content:
# Example CID
"bafyreigp5..."CIDs enable:
- Versioning: Track record changes
- Verification: Ensure content hasn't been tampered with
- Deduplication: Identify identical content
Using CIDs for Concurrency Control
Prevent conflicting updates with swap operations:
# Get current record
{:ok, current} = Repo.get_record(session, %{
repo: session.did,
collection: "app.bsky.feed.post",
rkey: "3kxyz..."
})
# Update only if CID matches (no one else modified it)
case Repo.put_record(session, %{
repo: session.did,
collection: "app.bsky.feed.post",
rkey: "3kxyz...",
record: updated_record,
swap_record: current.cid # Only succeed if CID still matches
}) do
{:ok, result} ->
# Update succeeded
IO.puts("Record updated")
{:error, %{error: "InvalidSwap"}} ->
# Someone else modified the record
IO.puts("Record was modified by another client")
{:error, reason} ->
# Other error
IO.puts("Update failed: #{inspect(reason)}")
endPagination
List operations support cursor-based pagination:
defmodule RecordFetcher do
alias ProtoRune.Atproto.Repo
def fetch_all_posts(session, repo_did) do
fetch_posts(session, repo_did, nil, [])
end
defp fetch_posts(session, repo_did, cursor, acc) do
params = %{
repo: repo_did,
collection: "app.bsky.feed.post",
limit: 100
}
params = if cursor, do: Map.put(params, :cursor, cursor), else: params
case Repo.list_records(session, params) do
{:ok, %{records: records, cursor: next_cursor}} ->
all_records = acc ++ records
if next_cursor do
fetch_posts(session, repo_did, next_cursor, all_records)
else
{:ok, all_records}
end
{:error, reason} ->
{:error, reason}
end
end
end
# Usage
{:ok, all_posts} = RecordFetcher.fetch_all_posts(session, session.did)Error Handling
Repository operations can fail for various reasons:
case Repo.create_record(session, params) do
{:ok, result} ->
result
{:error, %{error: "InvalidRecord"}} ->
# Record doesn't conform to lexicon
IO.puts("Invalid record structure")
{:error, %{error: "InvalidSwap"}} ->
# CID mismatch in swap operation
IO.puts("Concurrent modification detected")
{:error, %{error: "RecordNotFound"}} ->
# Record doesn't exist
IO.puts("Record not found")
{:error, reason} ->
# Network or other errors
IO.puts("Operation failed: #{inspect(reason)}")
endBest Practices
Use High-Level Helpers When Possible
For common operations, use ProtoRune.Bsky helpers:
# Good: Use helper
ProtoRune.Bsky.post(session, "Hello")
# Only when needed: Use low-level
Repo.create_record(session, %{
repo: session.did,
collection: "app.bsky.feed.post",
record: post_record
})Validate Records
Always ensure records conform to their lexicon schemas:
# Validation happens automatically by default
{:ok, result} = Repo.create_record(session, %{
repo: session.did,
collection: "app.bsky.feed.post",
record: record,
validate: true # Default
})Handle Concurrent Modifications
Use swap parameters for atomic operations:
{:ok, current} = Repo.get_record(session, params)
# Modify record
updated = Map.put(current.value, :field, new_value)
# Update with CID check
Repo.put_record(session, %{
repo: session.did,
collection: collection,
rkey: rkey,
record: updated,
swap_record: current.cid
})Paginate Large Collections
Don't fetch all records at once:
# Bad: Unbounded
Repo.list_records(session, %{repo: did, collection: collection})
# Good: Paginated
Repo.list_records(session, %{
repo: did,
collection: collection,
limit: 50
})When to Use Repository Operations
Use low-level repository operations when you need to:
- Create custom record types
- Implement optimistic concurrency control
- Batch process records efficiently
- Work with non-standard collections
- Build custom protocols on AT Protocol
For standard Bluesky operations (posts, likes, follows), use the high-level ProtoRune.Bsky API instead.