Joins
AT Protocol data lives in collections. A user's status records (xyz.statusphere.status) occupy one collection, their profile (app.bsky.actor.profile) another. Quickslice generates joins that query across collections—fetch a status and its author's profile in one request.
#Join Types
Quickslice generates three join types automatically:
| Type | What it does | Field naming |
|---|---|---|
| Forward | Follows a URI or strong ref to another record | {fieldName}Resolved |
| Reverse | Finds all records that reference a given record | {SourceType}Via{FieldName} |
| DID | Finds records by the same author | {CollectionName}ByDid |
#Forward Joins
Forward joins follow references from one record to another. When a record has a field containing an AT-URI or strong ref, Quickslice generates a {fieldName}Resolved field that fetches the referenced record.
#Example: Resolving a Favorite's Subject
A favorite record has a subject field containing an AT-URI. The subjectResolved field fetches the actual record:
query {
socialGrainFavorite(first: 5) {
edges {
node {
subject
createdAt
subjectResolved {
... on SocialGrainGallery {
uri
title
}
}
}
}
}
}
Forward joins return a Record union type because the referenced record could be any type. Use inline fragments (... on TypeName) for type-specific fields.
#Reverse Joins
Reverse joins work oppositely: given a record, find all records that reference it. Quickslice analyzes your Lexicons and generates reverse join fields automatically.
Reverse joins return paginated connections supporting filtering, sorting, and cursors.
#Example: Comments on a Photo
Find all comments that reference a specific photo:
query {
socialGrainPhoto(first: 5) {
edges {
node {
uri
alt
socialGrainCommentViaSubject(first: 10) {
totalCount
edges {
node {
text
createdAt
}
}
pageInfo {
hasNextPage
endCursor
}
}
}
}
}
}
#Sorting and Filtering Reverse Joins
Reverse joins support the same sorting and filtering as top-level queries:
query {
socialGrainGallery(first: 3) {
edges {
node {
title
socialGrainGalleryItemViaGallery(
first: 10
sortBy: [{ field: position, direction: ASC }]
where: { createdAt: { gt: "2025-01-01T00:00:00Z" } }
) {
edges {
node {
position
}
}
}
}
}
}
}
#DID Joins
DID joins connect records by author identity. Every record has a did field identifying its creator. Quickslice generates {CollectionName}ByDid fields to find related records by the same author.
#Example: Author Profile from a Status
Get the author's profile alongside their status:
query {
xyzStatusphereStatus(first: 10) {
edges {
node {
status
createdAt
appBskyActorProfileByDid {
displayName
avatar { url }
}
}
}
}
}
#Unique vs Non-Unique DID Joins
Some collections have one record per DID (like profiles with a literal:self key). These return a single object:
appBskyActorProfileByDid {
displayName
}
Other collections can have multiple records per DID. These return paginated connections:
socialGrainPhotoByDid(first: 10, sortBy: [{ field: createdAt, direction: DESC }]) {
totalCount
edges {
node {
alt
}
}
}
#Cross-Lexicon DID Joins
DID joins work across different Lexicon families. Get a user's Bluesky profile alongside their app-specific data:
query {
socialGrainPhoto(first: 5) {
edges {
node {
alt
appBskyActorProfileByDid {
displayName
avatar { url }
}
socialGrainActorProfileByDid {
description
}
}
}
}
}
#Common Patterns
#Profile Lookups
The most common pattern: joining author profiles to any record type.
query {
myAppPost(first: 20) {
edges {
node {
content
appBskyActorProfileByDid {
displayName
avatar { url }
}
}
}
}
}
#Engagement Counts
Use reverse joins to count likes, comments, or other engagement:
query {
socialGrainPhoto(first: 10) {
edges {
node {
uri
socialGrainFavoriteViaSubject {
totalCount
}
socialGrainCommentViaSubject {
totalCount
}
}
}
}
}
#User Activity
Get all records by a user across multiple collections:
query {
socialGrainActorProfile(first: 1, where: { actorHandle: { eq: "alice.bsky.social" } }) {
edges {
node {
displayName
socialGrainPhotoByDid(first: 5) {
totalCount
edges { node { alt } }
}
socialGrainGalleryByDid(first: 5) {
totalCount
edges { node { title } }
}
}
}
}
}
#How Batching Works
Quickslice batches join resolution to avoid the N+1 query problem. When querying 100 photos with author profiles:
- Fetches 100 photos in one query
- Collects all unique DIDs from those photos
- Fetches all profiles in a single query:
WHERE did IN (...) - Maps profiles back to their photos
All join types batch automatically.