Joins
quickslice automatically generates forward joins, reverse joins, and DID joins based on AT Protocol lexicon schemas, allowing you to traverse relationships between records.
Overview
-
Forward Joins: Follow references from one record to another (e.g., post → parent post)
- Returns: Single object or
Recordunion - Naming:
{fieldName}Resolved
- Returns: Single object or
-
Reverse Joins: Discover records that reference a given record (e.g., post → all likes on that post)
- Returns: Paginated Connection with sorting, filtering, and pagination
- Naming:
{SourceType}Via{FieldName}
-
DID Joins: Find records that share the same author (DID)
- Returns: Single object (unique DID) or Paginated Connection (non-unique DID)
- Naming:
{CollectionName}ByDid
-
Union Types: Forward joins return a
Recordunion, allowing type-specific field access via inline fragments
Forward Joins
Forward joins are generated for fields that reference other records via:
at-uriformat stringsstrongRefobjects
Basic Forward Join
When a field references another record, quickslice creates a *Resolved field:
query {
appBskyFeedPost {
edges {
node {
uri
text
replyTo # The at-uri string
replyToResolved { # The resolved record
uri
}
}
}
}
}
Union Types & Inline Fragments
Forward join fields return a Record union type because the referenced record could be any type. Use inline fragments to access type-specific fields:
query {
appBskyFeedPost {
edges {
node {
uri
text
replyToResolved {
# Access fields based on the actual type
... on AppBskyFeedPost {
uri
text
createdAt
}
... on AppBskyFeedLike {
uri
subject
createdAt
}
}
}
}
}
}
StrongRef Forward Joins
StrongRef fields (containing uri and cid) are resolved automatically:
query {
appBskyActorProfile {
edges {
node {
displayName
pinnedPost {
uri # Original strongRef uri
cid # Original strongRef cid
}
pinnedPostResolved {
... on AppBskyFeedPost {
uri
text
likeCount
}
}
}
}
}
}
Reverse Joins
Reverse joins are automatically discovered by analyzing all lexicons. They allow you to find all records that reference a given record. Reverse joins return paginated connections with support for sorting, filtering, and cursor-based pagination.
Basic Reverse Join
Reverse join fields are named: {SourceType}Via{FieldName} and return a Connection type:
query {
appBskyFeedPost {
edges {
node {
uri
text
# Find all likes that reference this post via their 'subject' field
appBskyFeedLikeViaSubject(first: 20) {
totalCount # Total number of likes
edges {
node {
uri
createdAt
}
cursor
}
pageInfo {
hasNextPage
hasPreviousPage
startCursor
endCursor
}
}
}
}
}
}
Multiple Reverse Joins
A record type can have multiple reverse join fields. You can request different page sizes for each:
query {
appBskyFeedPost {
edges {
node {
uri
text
# Get first 10 replies
appBskyFeedPostViaReplyTo(first: 10) {
totalCount
edges {
node {
uri
text
}
}
}
# Get first 20 likes
appBskyFeedLikeViaSubject(first: 20) {
totalCount
edges {
node {
uri
createdAt
}
}
}
# Get first 20 reposts
appBskyFeedRepostViaSubject(first: 20) {
totalCount
edges {
node {
uri
createdAt
}
}
}
}
}
}
}
Reverse Joins with StrongRef
Reverse joins work with strongRef fields too. You can also use sorting and filtering:
query {
appBskyFeedPost {
edges {
node {
uri
text
# Find all profiles that pinned this post
appBskyActorProfileViaPinnedPost(
sortBy: [{field: "indexedAt", direction: DESC}]
) {
totalCount
edges {
node {
uri
displayName
}
}
}
}
}
}
}
Sorting Reverse Joins
You can sort reverse join results by any field in the joined collection:
query {
appBskyFeedPost {
edges {
node {
uri
# Get most recent likes first
appBskyFeedLikeViaSubject(
first: 10
sortBy: [{field: "createdAt", direction: DESC}]
) {
edges {
node {
uri
createdAt
}
}
}
}
}
}
}
Filtering Reverse Joins
Use where filters to narrow down nested join results:
query {
appBskyFeedPost {
edges {
node {
uri
text
# Only get likes from a specific user
appBskyFeedLikeViaSubject(
where: { did: { eq: "did:plc:abc123" } }
) {
totalCount # Likes from this specific user
edges {
node {
uri
createdAt
}
}
}
}
}
}
}
DID Joins
DID joins allow you to traverse relationships between records that share the same author (DID). These are automatically generated for all collection pairs and are named: {CollectionName}ByDid
Two Types of DID Joins
1. Unique DID Joins (literal:self key)
Collections with a literal:self key (like profiles) have only one record per DID. These return a single nullable object (no pagination needed):
query {
appBskyFeedPost {
edges {
node {
uri
text
# Get the author's profile (single object, not paginated)
appBskyActorProfileByDid {
uri
displayName
bio
}
}
}
}
}
2. Non-Unique DID Joins
Most collections can have multiple records per DID. These return paginated connections with full support for sorting, filtering, and pagination:
query {
appBskyActorProfile {
edges {
node {
displayName
# Get all posts by this user (paginated)
appBskyFeedPostByDid(
first: 10
sortBy: [{field: "indexedAt", direction: DESC}]
) {
totalCount # Total posts by this user
edges {
node {
uri
text
indexedAt
}
}
pageInfo {
hasNextPage
endCursor
}
}
}
}
}
}
DID Join with Filtering
Combine DID joins with filters to find specific records:
query {
appBskyActorProfile(where: { did: { eq: "did:plc:abc123" } }) {
edges {
node {
displayName
# Get only posts containing "gleam"
appBskyFeedPostByDid(
where: { text: { contains: "gleam" } }
sortBy: [{field: "indexedAt", direction: DESC}]
) {
totalCount # Posts mentioning "gleam"
edges {
node {
text
indexedAt
}
}
}
}
}
}
}
Cross-Collection DID Queries
DID joins work across all collection pairs, enabling powerful cross-collection queries:
query {
appBskyActorProfile {
edges {
node {
displayName
# All their posts
appBskyFeedPostByDid(first: 10) {
totalCount
edges {
node {
text
}
}
}
# All their likes
appBskyFeedLikeByDid(first: 10) {
totalCount
edges {
node {
subject
}
}
}
# All their reposts
appBskyFeedRepostByDid(first: 10) {
totalCount
edges {
node {
subject
}
}
}
}
}
}
}
DID Join Arguments
Non-unique DID joins support all standard connection arguments:
| Argument | Type | Description |
|---|---|---|
first |
Int |
Number of records to return (forward pagination) |
after |
String |
Cursor for forward pagination |
last |
Int |
Number of records to return (backward pagination) |
before |
String |
Cursor for backward pagination |
sortBy |
[SortFieldInput!] |
Sort by any field in the collection |
where |
WhereInput |
Filter nested records |
Complete Example
Combining forward joins, reverse joins, and DID joins to build a rich thread view:
query GetThread($postUri: String!) {
appBskyFeedPost(where: { uri: { eq: $postUri } }) {
edges {
node {
uri
text
createdAt
# DID join: Get the author's profile
appBskyActorProfileByDid {
displayName
bio
}
# Forward join: Get the parent post
replyToResolved {
... on AppBskyFeedPost {
uri
text
createdAt
}
}
# Reverse join: Get first 10 replies
appBskyFeedPostViaReplyTo(
first: 10
sortBy: [{field: "createdAt", direction: ASC}]
) {
totalCount # Total replies
edges {
node {
uri
text
createdAt
}
}
pageInfo {
hasNextPage
}
}
# Reverse join: Get first 20 likes
appBskyFeedLikeViaSubject(first: 20) {
totalCount # Like count
edges {
node {
uri
createdAt
}
}
}
# Reverse join: Get reposts
appBskyFeedRepostViaSubject(first: 20) {
totalCount # Repost count
edges {
node {
uri
createdAt
}
}
}
}
}
}
}
DataLoader Batching
All joins use DataLoader for efficient batching:
# This query will batch all replyToResolved lookups into a single database query
query {
appBskyFeedPost(first: 100) {
edges {
node {
uri
text
replyToResolved {
... on AppBskyFeedPost {
uri
text
}
}
}
}
}
}
How it works:
- Fetches 100 posts
- Collects all unique
replyToURIs - Batches them into a single SQL query:
WHERE uri IN (...) - Returns resolved records efficiently
Performance Tips
1. Only Request What You Need
# Good: Only request specific fields
query {
appBskyFeedPost {
edges {
node {
uri
text
appBskyFeedLikeViaSubject(first: 20) {
totalCount # Get count without fetching all records
edges {
node {
uri # Only need the URI
}
}
}
}
}
}
}
2. Use totalCount for Metrics
Get engagement counts efficiently without fetching all records:
query {
appBskyFeedPost {
edges {
node {
uri
text
# Just get counts, no records
likes: appBskyFeedLikeViaSubject(first: 0) {
totalCount # Like count
}
reposts: appBskyFeedRepostViaSubject(first: 0) {
totalCount # Repost count
}
replies: appBskyFeedPostViaReplyTo(first: 0) {
totalCount # Reply count
}
}
}
}
}
3. Use Pagination on Nested Joins
Nested joins are paginated by default. Always specify first or last for optimal performance:
query {
appBskyFeedPost(first: 10) {
edges {
node {
uri
text
# Limit nested join results
appBskyFeedLikeViaSubject(first: 20) {
totalCount # Total likes
edges {
node {
uri
}
}
pageInfo {
hasNextPage # Know if there are more
}
}
}
}
}
}
4. Avoid Deep Nesting
# Avoid: Deeply nested joins can be expensive
query {
appBskyFeedPost {
edges {
node {
replyToResolved {
... on AppBskyFeedPost {
replyToResolved {
... on AppBskyFeedPost {
replyToResolved {
# Too deep!
}
}
}
}
}
}
}
}
}
Type Resolution
The Record union uses a type resolver that examines the collection field:
| Collection | GraphQL Type |
|---|---|
app.bsky.feed.post |
AppBskyFeedPost |
app.bsky.feed.like |
AppBskyFeedLike |
app.bsky.actor.profile |
AppBskyActorProfile |
This allows inline fragments to work correctly:
{
appBskyFeedPost {
edges {
node {
replyToResolved {
# Runtime type is determined by the collection field
... on AppBskyFeedPost { text }
... on AppBskyFeedLike { subject }
}
}
}
}
}
Schema Introspection
Discover available joins using introspection:
query {
__type(name: "AppBskyFeedPost") {
fields {
name
type {
name
kind
}
}
}
}
Look for fields ending in:
Resolved(forward joins)Via*(reverse joins)ByDid(DID joins)
Common Patterns
Thread Navigation
# Get a post and its parent
query {
appBskyFeedPost(where: { uri: { eq: $uri } }) {
edges {
node {
uri
text
replyToResolved {
... on AppBskyFeedPost {
uri
text
}
}
}
}
}
}
Engagement Metrics
Use totalCount to get efficient engagement counts without fetching all records:
# Get counts efficiently
query {
appBskyFeedPost {
edges {
node {
uri
text
# Get like count
likes: appBskyFeedLikeViaSubject(first: 0) {
totalCount
}
# Get repost count
reposts: appBskyFeedRepostViaSubject(first: 0) {
totalCount
}
# Get reply count
replies: appBskyFeedPostViaReplyTo(first: 0) {
totalCount
}
}
}
}
}
Or fetch recent engagement with pagination:
query {
appBskyFeedPost {
edges {
node {
uri
text
# Get 10 most recent likes
likes: appBskyFeedLikeViaSubject(
first: 10
sortBy: [{field: "createdAt", direction: DESC}]
) {
totalCount # Total like count
edges {
node {
did # Who liked it
createdAt
}
}
}
}
}
}
}
User's Pinned Content
query {
appBskyActorProfile(where: { did: { eq: $did } }) {
edges {
node {
displayName
pinnedPostResolved {
... on AppBskyFeedPost {
uri
text
createdAt
}
}
}
}
}
}