Highly ambitious ATProtocol AppView service and sdks

GraphQL API#

Slices provides a powerful GraphQL API for querying indexed AT Protocol data. The API automatically generates schema from your lexicons and provides efficient querying with relationship traversal.

Accessing the API#

GraphQL endpoints are available per-slice:

POST /graphql?slice=<slice-uri>

GraphQL Playground#

Access the interactive GraphQL Playground in your browser:

https://api.slices.network/graphql?slice=<slice-uri>

Schema Generation#

The GraphQL schema is automatically generated from your slice's lexicons:

  • Types: One GraphQL type per collection (e.g., social.grain.gallerySocialGrainGallery)
  • Queries: Collection queries with filtering, sorting, and pagination
  • Mutations: Create, update, delete operations per collection
  • Subscriptions: Real-time updates for record changes

Querying Data#

Basic Query#

query {
  socialGrainGalleries {
    edges {
      node {
        uri
        title
        description
        createdAt
      }
    }
  }
}

Filtering#

Use where clauses with typed filter conditions. Each collection has its own {Collection}WhereInput type with appropriate filters for each field.

query {
  socialGrainGalleries(where: {
    title: { contains: "Aerial" }
  }) {
    edges {
      node {
        uri
        title
        description
      }
    }
  }
}

Filter Types#

The API provides three filter types based on field data types:

StringFilter - For string fields:

  • eq: Exact match
  • in: Match any value in array
  • contains: Substring match (case-insensitive)
  • fuzzy: Fuzzy/similarity match (typo-tolerant)
  • gt: Greater than (lexicographic)
  • gte: Greater than or equal to
  • lt: Less than
  • lte: Less than or equal to

IntFilter - For integer fields:

  • eq: Exact match
  • in: Match any value in array
  • gt: Greater than
  • gte: Greater than or equal to
  • lt: Less than
  • lte: Less than or equal to

DateTimeFilter - For datetime fields:

  • eq: Exact match
  • gt: After datetime
  • gte: At or after datetime
  • lt: Before datetime
  • lte: At or before datetime

Fuzzy Matching Example#

The fuzzy filter uses PostgreSQL's trigram similarity for typo-tolerant search:

query FuzzySearch {
  fmTealAlphaFeedPlays(
    where: {
      trackName: { fuzzy: "love" }
    }
  ) {
    edges {
      node {
        trackName
        artists
      }
    }
  }
}

This will match track names like:

  • "Love" (exact)
  • "Love Song"
  • "Lovely"
  • "I Love You"
  • "Lover"
  • "Loveless"

The fuzzy filter is great for:

  • Handling typos and misspellings
  • Finding similar variations of text
  • Flexible search without exact matching

Note: Fuzzy matching works on the similarity between strings (using trigrams), so it's more flexible than contains but may return unexpected matches if the similarity threshold is met.

Date Range Example#

query RecentGalleries {
  socialGrainGalleries(
    where: {
      createdAt: {
        gte: "2025-01-01T00:00:00Z"
        lt: "2025-12-31T23:59:59Z"
      }
    }
  ) {
    edges {
      node {
        uri
        title
        createdAt
      }
    }
  }
}

Multiple Conditions#

Combine multiple filters - they are AND'ed together:

query {
  socialGrainGalleries(
    where: {
      title: { contains: "Aerial" }
      createdAt: { gte: "2025-01-01T00:00:00Z" }
    }
  ) {
    edges {
      node {
        uri
        title
        description
        createdAt
      }
    }
  }
}

Nested AND/OR Queries#

Build complex filter logic with arbitrarily nestable and and or arrays:

Simple OR - Match any condition:

query {
  networkSlicesSlices(
    where: {
      or: [
        { name: { contains: "grain" } }
        { name: { contains: "teal" } }
      ]
    }
  ) {
    edges {
      node {
        name
      }
    }
  }
}

Simple AND - Match all conditions:

query {
  networkSlicesSlices(
    where: {
      and: [
        { name: { contains: "grain" } }
        { name: { contains: "teal" } }
      ]
    }
  ) {
    edges {
      node {
        name
      }
    }
  }
}

Complex Nested Logic:

query {
  appBskyFeedPost(
    where: {
      and: [
        {
          or: [
            { text: { contains: "music" } }
            { text: { contains: "song" } }
          ]
        }
        {
          and: [
            { uri: { contains: "app.bsky" } }
            { uri: { contains: "post" } }
          ]
        }
        { createdAt: { gte: "2025-01-01T00:00:00Z" } }
      ]
    }
  ) {
    edges {
      node {
        uri
        text
        createdAt
      }
    }
  }
}

This example finds posts where:

  • (text contains "music" OR text contains "song") AND
  • (uri contains "app.bsky" AND uri contains "post") AND
  • createdAt is after 2025-01-01

Key Features:

  • Unlimited nesting depth - and/or can be nested arbitrarily
  • Mix with field filters - combine nested logic with regular field conditions
  • Type-safe - Each collection's WhereInput supports and and or arrays
  • Available in queries and aggregations

Pagination#

Relay-style cursor pagination:

query {
  socialGrainGalleries(first: 10, after: "cursor") {
    edges {
      cursor
      node {
        uri
        title
      }
    }
    pageInfo {
      hasNextPage
      endCursor
    }
  }
}

Sorting#

Each collection has its own typed {Collection}SortFieldInput for type-safe sorting:

query {
  socialGrainGalleries(
    sortBy: [
      { field: createdAt, direction: desc }
    ]
  ) {
    edges {
      node {
        uri
        title
        createdAt
      }
    }
  }
}

Multi-field sorting:

query {
  socialGrainGalleries(
    sortBy: [
      { field: actorHandle, direction: asc }
      { field: createdAt, direction: desc }
    ]
  ) {
    edges {
      node {
        uri
        title
        actorHandle
        createdAt
      }
    }
  }
}

The field enum values are collection-specific (e.g., SocialGrainGallerySortFieldInput). Use GraphQL introspection or the playground to see available fields for each collection.

Aggregations#

Aggregation queries allow you to group records and perform calculations. Each collection has a corresponding {Collection}Aggregated query.

Basic Aggregation#

Group records by one or more fields and get counts:

query TopTracks {
  fmTealAlphaFeedPlaysAggregated(
    groupBy: [{ field: trackName }]
    orderBy: { count: desc }
    limit: 10
  ) {
    trackName
    count
  }
}

Multi-Field Grouping#

Group by multiple fields using the typed {Collection}GroupByField enum:

query TopTracksByArtist {
  fmTealAlphaFeedPlaysAggregated(
    groupBy: [{ field: trackName }, { field: artists }]
    orderBy: { count: desc }
    limit: 20
  ) {
    trackName
    artists
    count
  }
}

Filtering Aggregations#

Combine typed filters with aggregations for time-based analysis:

query TopTracksThisWeek {
  fmTealAlphaFeedPlaysAggregated(
    groupBy: [{ field: trackName }, { field: artists }]
    where: {
      indexedAt: {
        gte: "2025-01-01T00:00:00Z"
        lt: "2025-01-08T00:00:00Z"
      }
      trackName: { contains: "Love" }
    }
    orderBy: { count: desc }
    limit: 10
  ) {
    trackName
    artists
    count
  }
}

Aggregation Features#

  • Typed GroupBy: Each collection has a {Collection}GroupByField enum for type-safe field selection
  • Typed Filters: Use the same {Collection}WhereInput as regular queries
  • Sorting: Order by count (ascending or descending) or any grouped field
  • Pagination: Use limit to control result count
  • Multiple Fields: Group by any combination of fields from your lexicon
  • Date Truncation: Group by time intervals (second, minute, hour, day, week, month, quarter, year)

Date Truncation#

Group records by time intervals using the interval parameter in groupBy:

query DailyPlays {
  fmTealAlphaFeedPlaysAggregated(
    groupBy: [
      { field: playedTime, interval: day }
    ]
    orderBy: { count: desc }
    limit: 30
  ) {
    playedTime
    count
  }
}

Supported Intervals:

  • second - Group by second
  • minute - Group by minute
  • hour - Group by hour
  • day - Group by day (common for daily reports)
  • week - Group by week (Monday-Sunday)
  • month - Group by month
  • quarter - Group by quarter (Q1-Q4)
  • year - Group by year

Combining with Regular Fields:

query TrackPlaysByDay {
  fmTealAlphaFeedPlaysAggregated(
    groupBy: [
      { field: trackName },
      { field: playedTime, interval: day }
    ]
    orderBy: { count: desc }
    limit: 100
  ) {
    trackName
    playedTime
    count
  }
}

How it Works:

  • Uses PostgreSQL's date_trunc() function for efficient time bucketing
  • Automatically handles timestamp casting for JSON fields
  • Returns truncated timestamps (e.g., 2025-01-15 00:00:00 for day interval)
  • Works with both system fields (indexedAt) and lexicon datetime fields

Use Cases#

Daily/Weekly/Monthly Reports:

query WeeklyPlays {
  fmTealAlphaFeedPlaysAggregated(
    groupBy: [{ field: trackName }]
    where: {
      playedTime: {
        gte: "2025-01-01T00:00:00Z"
        lt: "2025-01-08T00:00:00Z"
      }
    }
    orderBy: { count: desc }
    limit: 50
  ) {
    trackName
    count
  }
}

Trend Analysis:

query TrendingArtists {
  fmTealAlphaFeedPlaysAggregated(
    groupBy: [{ field: artists }]
    where: {
      playedTime: { gte: "2025-01-01T00:00:00Z" }
    }
    orderBy: { count: desc }
    limit: 20
  ) {
    artists
    count
  }
}

Relationships#

The GraphQL API automatically generates relationship fields based on your lexicon's at-uri fields.

Forward Joins (References)#

When a record has an at-uri field, you get a singular field that resolves to the referenced record.

Lexicon Schema (social.grain.gallery.item):

{
  "lexicon": 1,
  "id": "social.grain.gallery.item",
  "defs": {
    "main": {
      "type": "record",
      "key": "tid",
      "record": {
        "type": "object",
        "required": ["gallery", "item", "position", "createdAt"],
        "properties": {
          "gallery": {
            "type": "string",
            "format": "at-uri"
          },
          "item": {
            "type": "string",
            "format": "at-uri"
          },
          "position": { "type": "integer" },
          "createdAt": { "type": "string", "format": "datetime" }
        }
      }
    }
  }
}

Generated GraphQL Type:

type SocialGrainGalleryItem {
  uri: String!
  gallery: String!  # at-uri field from lexicon
  item: String!     # at-uri field from lexicon
  position: Int!
  createdAt: String!

  # Auto-generated forward joins (singular)
  socialGrainGallery: SocialGrainGallery
  socialGrainPhoto: SocialGrainPhoto
}

Example Query:

query {
  socialGrainGalleryItems(limit: 5) {
    position
    # Follow the reference to get the photo
    socialGrainPhoto {
      uri
      alt
      aspectRatio
    }
    # Follow the reference to get the gallery
    socialGrainGallery {
      title
      description
    }
  }
}

When other records reference this record via at-uri fields, you get plural fields that find all records pointing here.

Lexicon Schema (social.grain.favorite):

{
  "lexicon": 1,
  "id": "social.grain.favorite",
  "defs": {
    "main": {
      "type": "record",
      "key": "tid",
      "record": {
        "type": "object",
        "required": ["subject", "createdAt"],
        "properties": {
          "subject": {
            "type": "string",
            "format": "at-uri"
          },
          "createdAt": { "type": "string", "format": "datetime" }
        }
      }
    }
  }
}

Generated GraphQL Types:

type SocialGrainFavorite {
  uri: String!
  subject: String!  # at-uri pointing to gallery
  createdAt: String!

  # Forward join (follows the subject field)
  socialGrainGallery: SocialGrainGallery
}

type SocialGrainGallery {
  uri: String!
  title: String

  # Auto-generated reverse joins (plural)
  # These find all records whose at-uri fields point here
  socialGrainFavorites: [SocialGrainFavorite!]!
  socialGrainComments: [SocialGrainComment!]!
  socialGrainGalleryItems: [SocialGrainGalleryItem!]!
}

Example Query:

query {
  socialGrainGalleries(where: {
    actorHandle: { eq: "chadtmiller.com" }
  }) {
    edges {
      node {
        uri
        title
        # Get all favorites for this gallery
        socialGrainFavorites {
          uri
          createdAt
          actorHandle
        }
        # Get all comments for this gallery
        socialGrainComments {
          uri
          text
          actorHandle
        }
      }
    }
  }
}

Count Fields#

For efficient counting without loading all data, use *Count fields:

query {
  socialGrainGalleries {
    edges {
      node {
        uri
        title
        # Efficient count queries (no data loading)
        socialGrainFavoritesCount
        socialGrainCommentsCount
        socialGrainPhotosCount
      }
    }
  }
}

Combining Counts and Data#

Best practice: Get counts separately from limited data:

query {
  socialGrainGalleries {
    edges {
      node {
        uri
        title
        # Total count
        socialGrainFavoritesCount
        socialGrainCommentsCount

        # Show preview (first 3)
        socialGrainFavorites(limit: 3) {
          uri
          actorHandle
        }
        socialGrainComments(limit: 3) {
          uri
          text
        }
      }
    }
  }
}

DataLoader & Performance#

The GraphQL API uses DataLoader for efficient batching:

CollectionDidLoader#

  • Batches queries by (slice_uri, collection, did)
  • Used for forward joins where the DID is known
  • Eliminates N+1 queries when following references

CollectionUriLoader#

  • Batches queries by (slice_uri, collection, parent_uri, reference_field)
  • Used for reverse joins based on at-uri fields
  • Efficiently loads all records that reference a parent URI
  • Supports multiple at-uri fields (tries each until match found)

Example: Loading 100 galleries with favorites

  • Without DataLoader: 1 + 100 queries (N+1 problem)
  • With DataLoader: 1 + 1 query (batched)

Complex Queries#

Nested Relationships#

query {
  socialGrainGalleries {
    edges {
      node {
        title
        socialGrainGalleryItems {
          position
          socialGrainPhoto {
            uri
            alt
            photo {
              url(preset: "feed_fullsize")
            }
            socialGrainPhotoExifs {
              fNumber
              iSO
              make
              model
            }
          }
        }
      }
    }
  }
}

Full Example#

query MyGrainGalleries {
  socialGrainGalleries(
    where: { actorHandle: { eq: "chadtmiller.com" } }
    sortBy: [{ field: createdAt, direction: desc }]
  ) {
    edges {
      node {
        uri
        title
        description
        createdAt

        # Counts
        socialGrainFavoritesCount
        socialGrainCommentsCount

        # Preview data
        socialGrainFavorites(limit: 5) {
          uri
          createdAt
          actorHandle
        }

        socialGrainComments(limit: 3) {
          uri
          text
          createdAt
          actorHandle
        }

        # Gallery items with nested photos
        socialGrainGalleryItems {
          position
          socialGrainPhoto {
            uri
            alt
            photo {
              url(preset: "avatar")
            }
            aspectRatio
            createdAt
            socialGrainPhotoExifs {
              fNumber
              iSO
              make
              model
            }
          }
        }
      }
    }
    pageInfo {
      hasNextPage
      endCursor
    }
  }
}

Mutations#

Upload Blob#

Upload 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.

mutation UploadBlob($data: String!, $mimeType: String!) {
  uploadBlob(data: $data, mimeType: $mimeType) {
    blob {
      ref
      mimeType
      size
    }
  }
}

Parameters:

  • data (String, required): Base64-encoded file data
  • mimeType (String, required): MIME type of the file (e.g., "image/jpeg", "image/png", "video/mp4")

Returns:

  • blob: A JSON blob object containing:
    • ref: The CID (content identifier) reference for the blob
    • mimeType: The MIME type of the uploaded blob
    • size: The size of the blob in bytes

Example with Variables:

{
  "data": "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==",
  "mimeType": "image/png"
}

Usage in Records:

After uploading a blob, use the returned blob object in your record mutations:

mutation UpdateProfile($avatar: JSON) {
  updateAppBskyActorProfile(
    rkey: "self"
    input: {
      displayName: "My Name"
      avatar: $avatar  # Use the blob object from uploadBlob
    }
  ) {
    uri
    displayName
    avatar {
      ref
      mimeType
      size
      url(preset: "avatar")
    }
  }
}

Create Records#

Create new records in your AT Protocol repository. Each collection has a typed create{Collection} mutation with a corresponding {Collection}Input type.

mutation CreateFollow {
  createAppBskyGraphFollow(
    input: {
      subject: "did:plc:z72i7hdynmk6r22z27h6tvur"
      createdAt: "2025-01-15T12:00:00Z"
    }
  ) {
    uri
    cid
    subject
    createdAt
  }
}

Parameters:

  • input (required): Typed input object with the fields defined in your lexicon
  • rkey (optional): Record key for the new record. If not provided, a TID (timestamp identifier) is automatically generated

Returns: The complete created record with all fields, including generated fields like uri, cid, did, and indexedAt.

Example with custom rkey:

mutation CreateFollowWithRkey {
  createAppBskyGraphFollow(
    input: {
      subject: "did:plc:z72i7hdynmk6r22z27h6tvur"
      createdAt: "2025-01-15T12:00:00Z"
    }
    rkey: "my-custom-key"
  ) {
    uri
    subject
    createdAt
  }
}

Update Records#

Update existing records by their rkey. Each collection has an update{Collection} mutation.

Important: Updates replace the entire record. You must provide all required fields, not just the fields you want to change.

mutation UpdateProfile {
  updateAppBskyActorProfile(
    rkey: "self"
    input: {
      displayName: "New Display Name"
      description: "Updated bio"
      avatar: $avatarBlob
      banner: $bannerBlob
    }
  ) {
    uri
    displayName
    description
    avatar {
      url(preset: "avatar")
    }
  }
}

Parameters:

  • rkey (String, required): The record key of the record to update
  • input (required): Complete record data (all required fields must be provided)

Returns: The complete updated record with all fields.

Notes:

  • Updates perform a full record replacement via AT Protocol's putRecord
  • All required fields from your lexicon must be included in input
  • To partially update, first fetch the existing record, merge your changes, then update with complete data
  • 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)

Delete Records#

Delete records by their rkey. Each collection has a delete{Collection} mutation.

mutation DeleteFollow {
  deleteAppBskyGraphFollow(rkey: "3kjvbfz5nw42a") {
    uri
    subject
  }
}

Parameters:

  • rkey (String, required): The record key of the record to delete

Returns: The deleted record with its data (before deletion).

Example - Delete a profile:

mutation DeleteProfile {
  deleteAppBskyActorProfile(rkey: "self") {
    uri
    displayName
  }
}

Naming Convention#

All mutations follow a consistent naming pattern based on the lexicon collection name:

Collection Create Update Delete
app.bsky.actor.profile createAppBskyActorProfile updateAppBskyActorProfile deleteAppBskyActorProfile
app.bsky.graph.follow createAppBskyGraphFollow updateAppBskyGraphFollow deleteAppBskyGraphFollow
social.grain.gallery createSocialGrainGallery updateSocialGrainGallery deleteSocialGrainGallery
social.grain.photo createSocialGrainPhoto updateSocialGrainPhoto deleteSocialGrainPhoto

The pattern is: {action}{PascalCaseCollection} where dots in the collection name are removed and each segment is capitalized.

Subscriptions#

Real-time updates for record changes. Each collection has three subscription fields:

Created Records#

Subscribe to newly created records:

subscription {
  socialGrainGalleryCreated {
    uri
    title
    description
    createdAt
  }
}

Updated Records#

Subscribe to record updates:

subscription {
  socialGrainGalleryUpdated {
    uri
    title
    description
    updatedAt
  }
}

Deleted Records#

Subscribe to record deletions (returns just the URI):

subscription {
  socialGrainGalleryDeleted
}

Limits & Performance#

  • Depth Limit: 50 (supports introspection with circular relationships)
  • Complexity Limit: 5000 (prevents expensive queries)
  • Default Limit: 50 records per query
  • DataLoader: Automatic batching eliminates N+1 queries

Best Practices#

  1. Use count fields when you only need totals
  2. Limit nested data with limit parameter
  3. Request only needed fields (no over-fetching)
  4. Use cursors for pagination, not offset
  5. Batch related queries with DataLoader (automatic)
  6. Combine counts + limited data for previews

Error Handling#

GraphQL errors include:

  • "Query is nested too deep" - Exceeds depth limit (50)
  • "Query is too complex" - Exceeds complexity limit (5000)
  • "Schema error" - Invalid slice or missing lexicons