forked from
slices.network/quickslice
Auto-indexing service and GraphQL API for AT Protocol Records
1# Joins
2
3AT 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.
4
5## Join Types
6
7Quickslice generates three join types automatically:
8
9| Type | What it does | Field naming |
10|------|--------------|--------------|
11| **Forward** | Follows a URI or strong ref to another record | `{fieldName}Resolved` |
12| **Reverse** | Finds all records that reference a given record | `{SourceType}Via{FieldName}` |
13| **DID** | Finds records by the same author | `{CollectionName}ByDid` |
14
15## Forward Joins
16
17Forward 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.
18
19### Example: Resolving a Favorite's Subject
20
21A favorite record has a `subject` field containing an AT-URI. The `subjectResolved` field fetches the actual record:
22
23```graphql
24query {
25 socialGrainFavorite(first: 5) {
26 edges {
27 node {
28 subject
29 createdAt
30 subjectResolved {
31 ... on SocialGrainGallery {
32 uri
33 title
34 }
35 }
36 }
37 }
38 }
39}
40```
41
42Forward joins return a `Record` union type because the referenced record could be any type. Use inline fragments (`... on TypeName`) for type-specific fields.
43
44## Reverse Joins
45
46Reverse joins work oppositely: given a record, find all records that reference it. Quickslice analyzes your Lexicons and generates reverse join fields automatically.
47
48Reverse joins return paginated connections supporting filtering, sorting, and cursors.
49
50### Example: Comments on a Photo
51
52Find all comments that reference a specific photo:
53
54```graphql
55query {
56 socialGrainPhoto(first: 5) {
57 edges {
58 node {
59 uri
60 alt
61 socialGrainCommentViaSubject(first: 10) {
62 totalCount
63 edges {
64 node {
65 text
66 createdAt
67 }
68 }
69 pageInfo {
70 hasNextPage
71 endCursor
72 }
73 }
74 }
75 }
76 }
77}
78```
79
80### Sorting and Filtering Reverse Joins
81
82Reverse joins support the same sorting and filtering as top-level queries:
83
84```graphql
85query {
86 socialGrainGallery(first: 3) {
87 edges {
88 node {
89 title
90 socialGrainGalleryItemViaGallery(
91 first: 10
92 sortBy: [{ field: position, direction: ASC }]
93 where: { createdAt: { gt: "2025-01-01T00:00:00Z" } }
94 ) {
95 edges {
96 node {
97 position
98 }
99 }
100 }
101 }
102 }
103 }
104}
105```
106
107## DID Joins
108
109DID 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.
110
111### Example: Author Profile from a Status
112
113Get the author's profile alongside their status:
114
115```graphql
116query {
117 xyzStatusphereStatus(first: 10) {
118 edges {
119 node {
120 status
121 createdAt
122 appBskyActorProfileByDid {
123 displayName
124 avatar { url }
125 }
126 }
127 }
128 }
129}
130```
131
132### Unique vs Non-Unique DID Joins
133
134Some collections have one record per DID (like profiles with a `literal:self` key). These return a single object:
135
136```graphql
137appBskyActorProfileByDid {
138 displayName
139}
140```
141
142Other collections can have multiple records per DID. These return paginated connections:
143
144```graphql
145socialGrainPhotoByDid(first: 10, sortBy: [{ field: createdAt, direction: DESC }]) {
146 totalCount
147 edges {
148 node {
149 alt
150 }
151 }
152}
153```
154
155### Cross-Lexicon DID Joins
156
157DID joins work across different Lexicon families. Get a user's Bluesky profile alongside their app-specific data:
158
159```graphql
160query {
161 socialGrainPhoto(first: 5) {
162 edges {
163 node {
164 alt
165 appBskyActorProfileByDid {
166 displayName
167 avatar { url }
168 }
169 socialGrainActorProfileByDid {
170 description
171 }
172 }
173 }
174 }
175}
176```
177
178## Common Patterns
179
180### Profile Lookups
181
182The most common pattern: joining author profiles to any record type.
183
184```graphql
185query {
186 myAppPost(first: 20) {
187 edges {
188 node {
189 content
190 appBskyActorProfileByDid {
191 displayName
192 avatar { url }
193 }
194 }
195 }
196 }
197}
198```
199
200### Engagement Counts
201
202Use reverse joins to count likes, comments, or other engagement:
203
204```graphql
205query {
206 socialGrainPhoto(first: 10) {
207 edges {
208 node {
209 uri
210 socialGrainFavoriteViaSubject {
211 totalCount
212 }
213 socialGrainCommentViaSubject {
214 totalCount
215 }
216 }
217 }
218 }
219}
220```
221
222### User Activity
223
224Get all records by a user across multiple collections:
225
226```graphql
227query {
228 socialGrainActorProfile(first: 1, where: { actorHandle: { eq: "alice.bsky.social" } }) {
229 edges {
230 node {
231 displayName
232 socialGrainPhotoByDid(first: 5) {
233 totalCount
234 edges { node { alt } }
235 }
236 socialGrainGalleryByDid(first: 5) {
237 totalCount
238 edges { node { title } }
239 }
240 }
241 }
242 }
243}
244```
245
246## How Batching Works
247
248Quickslice batches join resolution to avoid the N+1 query problem. When querying 100 photos with author profiles:
249
2501. Fetches 100 photos in one query
2512. Collects all unique DIDs from those photos
2523. Fetches all profiles in a single query: `WHERE did IN (...)`
2534. Maps profiles back to their photos
254
255All join types batch automatically.