Highly ambitious ATProtocol AppView service and sdks
at main 1131 lines 23 kB view raw view rendered
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