forked from
slices.network/slices
Highly ambitious ATProtocol AppView service and sdks
1# GraphQL API
2
3Slices provides a powerful GraphQL API for querying indexed AT Protocol data.
4The API automatically generates schema from your lexicons and provides efficient
5querying with relationship traversal.
6
7## Accessing the API
8
9GraphQL endpoints are available per-slice:
10
11```
12POST /graphql?slice=<slice-uri>
13```
14
15### GraphQL Playground
16
17Access the interactive GraphQL Playground in your browser:
18
19```
20https://api.slices.network/graphql?slice=<slice-uri>
21```
22
23## Schema Generation
24
25The GraphQL schema is automatically generated from your slice's lexicons:
26
27- **Types**: One GraphQL type per collection (e.g., `social.grain.gallery` →
28 `SocialGrainGallery`)
29- **Queries**: Collection queries with filtering, sorting, and pagination
30- **Mutations**: Create, update, delete operations per collection
31- **Subscriptions**: Real-time updates for record changes
32
33## Querying Data
34
35### Basic Query
36
37```graphql
38query {
39 socialGrainGalleries {
40 edges {
41 node {
42 uri
43 title
44 description
45 createdAt
46 }
47 }
48 }
49}
50```
51
52### Filtering
53
54Use `where` clauses with typed filter conditions. Each collection has its own
55`{Collection}WhereInput` type with appropriate filters for each field.
56
57```graphql
58query {
59 socialGrainGalleries(where: {
60 title: { contains: "Aerial" }
61 }) {
62 edges {
63 node {
64 uri
65 title
66 description
67 }
68 }
69 }
70}
71```
72
73#### Filter Types
74
75The API provides three filter types based on field data types:
76
77**StringFilter** - For string fields:
78
79- `eq`: Exact match
80- `in`: Match any value in array
81- `contains`: Substring match (case-insensitive)
82- `fuzzy`: Fuzzy/similarity match (typo-tolerant)
83- `gt`: Greater than (lexicographic)
84- `gte`: Greater than or equal to
85- `lt`: Less than
86- `lte`: Less than or equal to
87
88**IntFilter** - For integer fields:
89
90- `eq`: Exact match
91- `in`: Match any value in array
92- `gt`: Greater than
93- `gte`: Greater than or equal to
94- `lt`: Less than
95- `lte`: Less than or equal to
96
97**DateTimeFilter** - For datetime fields:
98
99- `eq`: Exact match
100- `gt`: After datetime
101- `gte`: At or after datetime
102- `lt`: Before datetime
103- `lte`: At or before datetime
104
105#### Fuzzy Matching Example
106
107The `fuzzy` filter uses PostgreSQL's trigram similarity for typo-tolerant
108search:
109
110```graphql
111query FuzzySearch {
112 fmTealAlphaFeedPlays(
113 where: {
114 trackName: { fuzzy: "love" }
115 }
116 ) {
117 edges {
118 node {
119 trackName
120 artists
121 }
122 }
123 }
124}
125```
126
127This will match track names like:
128
129- "Love" (exact)
130- "Love Song"
131- "Lovely"
132- "I Love You"
133- "Lover"
134- "Loveless"
135
136The fuzzy filter is great for:
137
138- Handling typos and misspellings
139- Finding similar variations of text
140- Flexible search without exact matching
141
142**Note**: Fuzzy matching works on the similarity between strings (using
143trigrams), so it's more flexible than `contains` but may return unexpected
144matches if the similarity threshold is met.
145
146#### Date Range Example
147
148```graphql
149query RecentGalleries {
150 socialGrainGalleries(
151 where: {
152 createdAt: {
153 gte: "2025-01-01T00:00:00Z"
154 lt: "2025-12-31T23:59:59Z"
155 }
156 }
157 ) {
158 edges {
159 node {
160 uri
161 title
162 createdAt
163 }
164 }
165 }
166}
167```
168
169#### Multiple Conditions
170
171Combine multiple filters - they are AND'ed together:
172
173```graphql
174query {
175 socialGrainGalleries(
176 where: {
177 title: { contains: "Aerial" }
178 createdAt: { gte: "2025-01-01T00:00:00Z" }
179 }
180 ) {
181 edges {
182 node {
183 uri
184 title
185 description
186 createdAt
187 }
188 }
189 }
190}
191```
192
193#### Nested AND/OR Queries
194
195Build complex filter logic with arbitrarily nestable `and` and `or` arrays:
196
197**Simple OR - Match any condition:**
198
199```graphql
200query {
201 networkSlicesSlices(
202 where: {
203 or: [
204 { name: { contains: "grain" } }
205 { name: { contains: "teal" } }
206 ]
207 }
208 ) {
209 edges {
210 node {
211 name
212 }
213 }
214 }
215}
216```
217
218**Simple AND - Match all conditions:**
219
220```graphql
221query {
222 networkSlicesSlices(
223 where: {
224 and: [
225 { name: { contains: "grain" } }
226 { name: { contains: "teal" } }
227 ]
228 }
229 ) {
230 edges {
231 node {
232 name
233 }
234 }
235 }
236}
237```
238
239**Complex Nested Logic:**
240
241```graphql
242query {
243 appBskyFeedPost(
244 where: {
245 and: [
246 {
247 or: [
248 { text: { contains: "music" } }
249 { text: { contains: "song" } }
250 ]
251 }
252 {
253 and: [
254 { uri: { contains: "app.bsky" } }
255 { uri: { contains: "post" } }
256 ]
257 }
258 { createdAt: { gte: "2025-01-01T00:00:00Z" } }
259 ]
260 }
261 ) {
262 edges {
263 node {
264 uri
265 text
266 createdAt
267 }
268 }
269 }
270}
271```
272
273This example finds posts where:
274
275- (text contains "music" OR text contains "song") AND
276- (uri contains "app.bsky" AND uri contains "post") AND
277- createdAt is after 2025-01-01
278
279**Key Features:**
280
281- Unlimited nesting depth - `and`/`or` can be nested arbitrarily
282- Mix with field filters - combine nested logic with regular field conditions
283- Type-safe - Each collection's `WhereInput` supports `and` and `or` arrays
284- Available in queries and aggregations
285
286### Pagination
287
288Relay-style cursor pagination:
289
290```graphql
291query {
292 socialGrainGalleries(first: 10, after: "cursor") {
293 edges {
294 cursor
295 node {
296 uri
297 title
298 }
299 }
300 pageInfo {
301 hasNextPage
302 endCursor
303 }
304 }
305}
306```
307
308### Sorting
309
310Each collection has its own typed `{Collection}SortFieldInput` for type-safe
311sorting:
312
313```graphql
314query {
315 socialGrainGalleries(
316 sortBy: [
317 { field: createdAt, direction: desc }
318 ]
319 ) {
320 edges {
321 node {
322 uri
323 title
324 createdAt
325 }
326 }
327 }
328}
329```
330
331**Multi-field sorting:**
332
333```graphql
334query {
335 socialGrainGalleries(
336 sortBy: [
337 { field: actorHandle, direction: asc }
338 { field: createdAt, direction: desc }
339 ]
340 ) {
341 edges {
342 node {
343 uri
344 title
345 actorHandle
346 createdAt
347 }
348 }
349 }
350}
351```
352
353The `field` enum values are collection-specific (e.g.,
354`SocialGrainGallerySortFieldInput`). Use GraphQL introspection or the playground
355to see available fields for each collection.
356
357## Aggregations
358
359Aggregation queries allow you to group records and perform calculations. Each
360collection has a corresponding `{Collection}Aggregated` query.
361
362### Basic Aggregation
363
364Group records by one or more fields and get counts:
365
366```graphql
367query TopTracks {
368 fmTealAlphaFeedPlaysAggregated(
369 groupBy: [{ field: trackName }]
370 orderBy: { count: desc }
371 limit: 10
372 ) {
373 trackName
374 count
375 }
376}
377```
378
379### Multi-Field Grouping
380
381Group by multiple fields using the typed `{Collection}GroupByField` enum:
382
383```graphql
384query TopTracksByArtist {
385 fmTealAlphaFeedPlaysAggregated(
386 groupBy: [{ field: trackName }, { field: artists }]
387 orderBy: { count: desc }
388 limit: 20
389 ) {
390 trackName
391 artists
392 count
393 }
394}
395```
396
397### Filtering Aggregations
398
399Combine typed filters with aggregations for time-based analysis:
400
401```graphql
402query TopTracksThisWeek {
403 fmTealAlphaFeedPlaysAggregated(
404 groupBy: [{ field: trackName }, { field: artists }]
405 where: {
406 indexedAt: {
407 gte: "2025-01-01T00:00:00Z"
408 lt: "2025-01-08T00:00:00Z"
409 }
410 trackName: { contains: "Love" }
411 }
412 orderBy: { count: desc }
413 limit: 10
414 ) {
415 trackName
416 artists
417 count
418 }
419}
420```
421
422### Aggregation Features
423
424- **Typed GroupBy**: Each collection has a `{Collection}GroupByField` enum for
425 type-safe field selection
426- **Typed Filters**: Use the same `{Collection}WhereInput` as regular queries
427- **Sorting**: Order by `count` (ascending or descending) or any grouped field
428- **Pagination**: Use `limit` to control result count
429- **Multiple Fields**: Group by any combination of fields from your lexicon
430- **Date Truncation**: Group by time intervals (second, minute, hour, day, week,
431 month, quarter, year)
432
433### Date Truncation
434
435Group records by time intervals using the `interval` parameter in `groupBy`:
436
437```graphql
438query DailyPlays {
439 fmTealAlphaFeedPlaysAggregated(
440 groupBy: [
441 { field: playedTime, interval: day }
442 ]
443 orderBy: { count: desc }
444 limit: 30
445 ) {
446 playedTime
447 count
448 }
449}
450```
451
452**Supported Intervals:**
453
454- `second` - Group by second
455- `minute` - Group by minute
456- `hour` - Group by hour
457- `day` - Group by day (common for daily reports)
458- `week` - Group by week (Monday-Sunday)
459- `month` - Group by month
460- `quarter` - Group by quarter (Q1-Q4)
461- `year` - Group by year
462
463**Combining with Regular Fields:**
464
465```graphql
466query TrackPlaysByDay {
467 fmTealAlphaFeedPlaysAggregated(
468 groupBy: [
469 { field: trackName },
470 { field: playedTime, interval: day }
471 ]
472 orderBy: { count: desc }
473 limit: 100
474 ) {
475 trackName
476 playedTime
477 count
478 }
479}
480```
481
482**How it Works:**
483
484- Uses PostgreSQL's `date_trunc()` function for efficient time bucketing
485- Automatically handles timestamp casting for JSON fields
486- Returns truncated timestamps (e.g., `2025-01-15 00:00:00` for day interval)
487- Works with both system fields (`indexedAt`) and lexicon datetime fields
488
489### Use Cases
490
491**Daily/Weekly/Monthly Reports**:
492
493```graphql
494query WeeklyPlays {
495 fmTealAlphaFeedPlaysAggregated(
496 groupBy: [{ field: trackName }]
497 where: {
498 playedTime: {
499 gte: "2025-01-01T00:00:00Z"
500 lt: "2025-01-08T00:00:00Z"
501 }
502 }
503 orderBy: { count: desc }
504 limit: 50
505 ) {
506 trackName
507 count
508 }
509}
510```
511
512**Trend Analysis**:
513
514```graphql
515query TrendingArtists {
516 fmTealAlphaFeedPlaysAggregated(
517 groupBy: [{ field: artists }]
518 where: {
519 playedTime: { gte: "2025-01-01T00:00:00Z" }
520 }
521 orderBy: { count: desc }
522 limit: 20
523 ) {
524 artists
525 count
526 }
527}
528```
529
530## Relationships
531
532The GraphQL API automatically generates relationship fields based on your
533lexicon's `at-uri` fields.
534
535### Forward Joins (References)
536
537When a record has an `at-uri` field, you get a **singular** field that resolves
538to the referenced record.
539
540**Lexicon Schema (social.grain.gallery.item):**
541
542```json
543{
544 "lexicon": 1,
545 "id": "social.grain.gallery.item",
546 "defs": {
547 "main": {
548 "type": "record",
549 "key": "tid",
550 "record": {
551 "type": "object",
552 "required": ["gallery", "item", "position", "createdAt"],
553 "properties": {
554 "gallery": {
555 "type": "string",
556 "format": "at-uri"
557 },
558 "item": {
559 "type": "string",
560 "format": "at-uri"
561 },
562 "position": { "type": "integer" },
563 "createdAt": { "type": "string", "format": "datetime" }
564 }
565 }
566 }
567 }
568}
569```
570
571**Generated GraphQL Type:**
572
573```graphql
574type SocialGrainGalleryItem {
575 uri: String!
576 gallery: String! # at-uri field from lexicon
577 item: String! # at-uri field from lexicon
578 position: Int!
579 createdAt: String!
580
581 # Auto-generated forward joins (singular)
582 socialGrainGallery: SocialGrainGallery
583 socialGrainPhoto: SocialGrainPhoto
584}
585```
586
587**Example Query:**
588
589```graphql
590query {
591 socialGrainGalleryItems(limit: 5) {
592 position
593 # Follow the reference to get the photo
594 socialGrainPhoto {
595 uri
596 alt
597 aspectRatio
598 }
599 # Follow the reference to get the gallery
600 socialGrainGallery {
601 title
602 description
603 }
604 }
605}
606```
607
608### Reverse Joins (Backlinks)
609
610When other records reference this record via `at-uri` fields, you get **plural**
611fields that find all records pointing here.
612
613**Lexicon Schema (social.grain.favorite):**
614
615```json
616{
617 "lexicon": 1,
618 "id": "social.grain.favorite",
619 "defs": {
620 "main": {
621 "type": "record",
622 "key": "tid",
623 "record": {
624 "type": "object",
625 "required": ["subject", "createdAt"],
626 "properties": {
627 "subject": {
628 "type": "string",
629 "format": "at-uri"
630 },
631 "createdAt": { "type": "string", "format": "datetime" }
632 }
633 }
634 }
635 }
636}
637```
638
639**Generated GraphQL Types:**
640
641```graphql
642type SocialGrainFavorite {
643 uri: String!
644 subject: String! # at-uri pointing to gallery
645 createdAt: String!
646
647 # Forward join (follows the subject field)
648 socialGrainGallery: SocialGrainGallery
649}
650
651type SocialGrainGallery {
652 uri: String!
653 title: String
654
655 # Auto-generated reverse joins (plural)
656 # These find all records whose at-uri fields point here
657 socialGrainFavorites: [SocialGrainFavorite!]!
658 socialGrainComments: [SocialGrainComment!]!
659 socialGrainGalleryItems: [SocialGrainGalleryItem!]!
660}
661```
662
663**Example Query:**
664
665```graphql
666query {
667 socialGrainGalleries(where: {
668 actorHandle: { eq: "chadtmiller.com" }
669 }) {
670 edges {
671 node {
672 uri
673 title
674 # Get all favorites for this gallery
675 socialGrainFavorites {
676 uri
677 createdAt
678 actorHandle
679 }
680 # Get all comments for this gallery
681 socialGrainComments {
682 uri
683 text
684 actorHandle
685 }
686 }
687 }
688 }
689}
690```
691
692### Count Fields
693
694For efficient counting without loading all data, use `*Count` fields:
695
696```graphql
697query {
698 socialGrainGalleries {
699 edges {
700 node {
701 uri
702 title
703 # Efficient count queries (no data loading)
704 socialGrainFavoritesCount
705 socialGrainCommentsCount
706 socialGrainPhotosCount
707 }
708 }
709 }
710}
711```
712
713### Combining Counts and Data
714
715Best practice: Get counts separately from limited data:
716
717```graphql
718query {
719 socialGrainGalleries {
720 edges {
721 node {
722 uri
723 title
724 # Total count
725 socialGrainFavoritesCount
726 socialGrainCommentsCount
727
728 # Show preview (first 3)
729 socialGrainFavorites(limit: 3) {
730 uri
731 actorHandle
732 }
733 socialGrainComments(limit: 3) {
734 uri
735 text
736 }
737 }
738 }
739 }
740}
741```
742
743## DataLoader & Performance
744
745The GraphQL API uses DataLoader for efficient batching:
746
747### CollectionDidLoader
748
749- Batches queries by `(slice_uri, collection, did)`
750- Used for forward joins where the DID is known
751- Eliminates N+1 queries when following references
752
753### CollectionUriLoader
754
755- Batches queries by `(slice_uri, collection, parent_uri, reference_field)`
756- Used for reverse joins based on at-uri fields
757- Efficiently loads all records that reference a parent URI
758- Supports multiple at-uri fields (tries each until match found)
759
760Example: Loading 100 galleries with favorites
761
762- **Without DataLoader**: 1 + 100 queries (N+1 problem)
763- **With DataLoader**: 1 + 1 query (batched)
764
765## Complex Queries
766
767### Nested Relationships
768
769```graphql
770query {
771 socialGrainGalleries {
772 edges {
773 node {
774 title
775 socialGrainGalleryItems {
776 position
777 socialGrainPhoto {
778 uri
779 alt
780 photo {
781 url(preset: "feed_fullsize")
782 }
783 socialGrainPhotoExifs {
784 fNumber
785 iSO
786 make
787 model
788 }
789 }
790 }
791 }
792 }
793 }
794}
795```
796
797### Full Example
798
799```graphql
800query MyGrainGalleries {
801 socialGrainGalleries(
802 where: { actorHandle: { eq: "chadtmiller.com" } }
803 sortBy: [{ field: createdAt, direction: desc }]
804 ) {
805 edges {
806 node {
807 uri
808 title
809 description
810 createdAt
811
812 # Counts
813 socialGrainFavoritesCount
814 socialGrainCommentsCount
815
816 # Preview data
817 socialGrainFavorites(limit: 5) {
818 uri
819 createdAt
820 actorHandle
821 }
822
823 socialGrainComments(limit: 3) {
824 uri
825 text
826 createdAt
827 actorHandle
828 }
829
830 # Gallery items with nested photos
831 socialGrainGalleryItems {
832 position
833 socialGrainPhoto {
834 uri
835 alt
836 photo {
837 url(preset: "avatar")
838 }
839 aspectRatio
840 createdAt
841 socialGrainPhotoExifs {
842 fNumber
843 iSO
844 make
845 model
846 }
847 }
848 }
849 }
850 }
851 pageInfo {
852 hasNextPage
853 endCursor
854 }
855 }
856}
857```
858
859## Mutations
860
861### Upload Blob
862
863Upload a blob (image, video, or other file) to your AT Protocol repository. The blob will be stored in your PDS and can be referenced in records.
864
865```graphql
866mutation UploadBlob($data: String!, $mimeType: String!) {
867 uploadBlob(data: $data, mimeType: $mimeType) {
868 blob {
869 ref
870 mimeType
871 size
872 }
873 }
874}
875```
876
877**Parameters:**
878
879- `data` (String, required): Base64-encoded file data
880- `mimeType` (String, required): MIME type of the file (e.g., "image/jpeg", "image/png", "video/mp4")
881
882**Returns:**
883
884- `blob`: A Blob object containing:
885 - `ref` (String): The CID (content identifier) reference for the blob
886 - `mimeType` (String): The MIME type of the uploaded blob
887 - `size` (Int): The size of the blob in bytes
888 - `url` (String): CDN URL for the blob (supports presets)
889
890**Example with Variables:**
891
892```json
893{
894 "data": "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==",
895 "mimeType": "image/png"
896}
897```
898
899**Usage in Records:**
900
901After uploading a blob, use the returned blob object in your record mutations. You can provide the blob as a complete object with `ref` as a String:
902
903```graphql
904mutation UpdateProfile($avatar: JSON) {
905 updateAppBskyActorProfile(
906 rkey: "self"
907 input: {
908 displayName: "My Name"
909 avatar: $avatar # Blob object with ref as String (CID)
910 }
911 ) {
912 uri
913 displayName
914 avatar {
915 ref # Returns as String (CID)
916 mimeType
917 size
918 url(preset: "avatar")
919 }
920 }
921}
922```
923
924**Example blob object for mutations:**
925
926```json
927{
928 "ref": "bafyreigbtj4x7ip5legnfznufuopl4sg4knzc2cof6duas4b3q2fy6swua",
929 "mimeType": "image/jpeg",
930 "size": 245678
931}
932```
933
934**Note:** The GraphQL API automatically handles the conversion between the GraphQL format (where `ref` is a String containing the CID) and the AT Protocol format (where `ref` is an object `{$link: "cid"}`). You always work with `ref` as a simple String in GraphQL queries and mutations.
935
936### Create Records
937
938Create new records in your AT Protocol repository. Each collection has a typed `create{Collection}` mutation with a corresponding `{Collection}Input` type.
939
940```graphql
941mutation CreateFollow {
942 createAppBskyGraphFollow(
943 input: {
944 subject: "did:plc:z72i7hdynmk6r22z27h6tvur"
945 createdAt: "2025-01-15T12:00:00Z"
946 }
947 ) {
948 uri
949 cid
950 subject
951 createdAt
952 }
953}
954```
955
956**Parameters:**
957
958- `input` (required): Typed input object with the fields defined in your lexicon
959- `rkey` (optional): Record key for the new record. If not provided, a TID (timestamp identifier) is automatically generated
960
961**Returns:** The complete created record with all fields, including generated fields like `uri`, `cid`, `did`, and `indexedAt`.
962
963**Example with custom rkey:**
964
965```graphql
966mutation CreateFollowWithRkey {
967 createAppBskyGraphFollow(
968 input: {
969 subject: "did:plc:z72i7hdynmk6r22z27h6tvur"
970 createdAt: "2025-01-15T12:00:00Z"
971 }
972 rkey: "my-custom-key"
973 ) {
974 uri
975 subject
976 createdAt
977 }
978}
979```
980
981### Update Records
982
983Update existing records by their rkey. Each collection has an `update{Collection}` mutation.
984
985**Important:** Updates replace the entire record. You must provide all required fields, not just the fields you want to change.
986
987```graphql
988mutation UpdateProfile {
989 updateAppBskyActorProfile(
990 rkey: "self"
991 input: {
992 displayName: "New Display Name"
993 description: "Updated bio"
994 avatar: $avatarBlob
995 banner: $bannerBlob
996 }
997 ) {
998 uri
999 displayName
1000 description
1001 avatar {
1002 url(preset: "avatar")
1003 }
1004 }
1005}
1006```
1007
1008**Parameters:**
1009
1010- `rkey` (String, required): The record key of the record to update
1011- `input` (required): Complete record data (all required fields must be provided)
1012
1013**Returns:** The complete updated record with all fields.
1014
1015**Notes:**
1016- Updates perform a full record replacement via AT Protocol's `putRecord`
1017- All required fields from your lexicon must be included in `input`
1018- To partially update, first fetch the existing record, merge your changes, then update with complete data
1019- The rkey is the last segment of the record's URI (e.g., `at://did:plc:abc/app.bsky.actor.profile/self` → rkey is `self`)
1020
1021### Delete Records
1022
1023Delete records by their rkey. Each collection has a `delete{Collection}` mutation.
1024
1025```graphql
1026mutation DeleteFollow {
1027 deleteAppBskyGraphFollow(rkey: "3kjvbfz5nw42a") {
1028 uri
1029 subject
1030 }
1031}
1032```
1033
1034**Parameters:**
1035
1036- `rkey` (String, required): The record key of the record to delete
1037
1038**Returns:** The deleted record with its data (before deletion).
1039
1040**Example - Delete a profile:**
1041
1042```graphql
1043mutation DeleteProfile {
1044 deleteAppBskyActorProfile(rkey: "self") {
1045 uri
1046 displayName
1047 }
1048}
1049```
1050
1051### Naming Convention
1052
1053All mutations follow a consistent naming pattern based on the lexicon collection name:
1054
1055| Collection | Create | Update | Delete |
1056|------------|--------|--------|--------|
1057| `app.bsky.actor.profile` | `createAppBskyActorProfile` | `updateAppBskyActorProfile` | `deleteAppBskyActorProfile` |
1058| `app.bsky.graph.follow` | `createAppBskyGraphFollow` | `updateAppBskyGraphFollow` | `deleteAppBskyGraphFollow` |
1059| `social.grain.gallery` | `createSocialGrainGallery` | `updateSocialGrainGallery` | `deleteSocialGrainGallery` |
1060| `social.grain.photo` | `createSocialGrainPhoto` | `updateSocialGrainPhoto` | `deleteSocialGrainPhoto` |
1061
1062The pattern is: `{action}{PascalCaseCollection}` where dots in the collection name are removed and each segment is capitalized.
1063
1064## Subscriptions
1065
1066Real-time updates for record changes. Each collection has three subscription
1067fields:
1068
1069### Created Records
1070
1071Subscribe to newly created records:
1072
1073```graphql
1074subscription {
1075 socialGrainGalleryCreated {
1076 uri
1077 title
1078 description
1079 createdAt
1080 }
1081}
1082```
1083
1084### Updated Records
1085
1086Subscribe to record updates:
1087
1088```graphql
1089subscription {
1090 socialGrainGalleryUpdated {
1091 uri
1092 title
1093 description
1094 updatedAt
1095 }
1096}
1097```
1098
1099### Deleted Records
1100
1101Subscribe to record deletions (returns just the URI):
1102
1103```graphql
1104subscription {
1105 socialGrainGalleryDeleted
1106}
1107```
1108
1109## Limits & Performance
1110
1111- **Depth Limit**: 50 (supports introspection with circular relationships)
1112- **Complexity Limit**: 5000 (prevents expensive queries)
1113- **Default Limit**: 50 records per query
1114- **DataLoader**: Automatic batching eliminates N+1 queries
1115
1116## Best Practices
1117
11181. **Use count fields** when you only need totals
11192. **Limit nested data** with `limit` parameter
11203. **Request only needed fields** (no over-fetching)
11214. **Use cursors** for pagination, not offset
11225. **Batch related queries** with DataLoader (automatic)
11236. **Combine counts + limited data** for previews
1124
1125## Error Handling
1126
1127GraphQL errors include:
1128
1129- `"Query is nested too deep"` - Exceeds depth limit (50)
1130- `"Query is too complex"` - Exceeds complexity limit (5000)
1131- `"Schema error"` - Invalid slice or missing lexicons