Auto-indexing service and GraphQL API for AT Protocol Records
at main 234 lines 7.6 kB view raw view rendered
1# Tutorial: Build Statusphere with Quickslice 2 3Let's build Statusphere, an app where users share their current status as an emoji. This is the same app from the [AT Protocol docs](https://atproto.com/guides/applications), but using Quickslice as the AppView. 4 5Along the way, we'll show what you'd write manually versus what Quickslice handles automatically. 6 7**Try it live:** A working example is running at [StackBlitz](https://stackblitz.com/edit/stackblitz-starters-g3uwhweu?file=index.html), connected to a slice at [xyzstatusphere.slices.network](https://xyzstatusphere.slices.network) with the `xyz.statusphere.status` lexicon. 8 9NOTE: For the StackBliz example, OAuth will only work if you open the preview in a new tab and login from there 🫠. 10 11## What We're Building 12 13Statusphere lets users: 14- Log in with their AT Protocol identity 15- Set their status as an emoji 16- See a feed of everyone's statuses with profile information 17 18By the end of this tutorial, you'll understand how Quickslice eliminates the boilerplate of building an AppView. 19 20## Step 1: Project Setup and Importing Lexicons 21 22Every AT Protocol app starts with Lexicons. Here's the Lexicon for a status record: 23 24```json 25{ 26 "lexicon": 1, 27 "id": "xyz.statusphere.status", 28 "defs": { 29 "main": { 30 "type": "record", 31 "key": "tid", 32 "record": { 33 "type": "object", 34 "required": ["status", "createdAt"], 35 "properties": { 36 "status": { 37 "type": "string", 38 "minLength": 1, 39 "maxGraphemes": 1, 40 "maxLength": 32 41 }, 42 "createdAt": { "type": "string", "format": "datetime" } 43 } 44 } 45 } 46 } 47} 48``` 49 50Importing this Lexicon into Quickslice triggers three automatic steps: 51 521. **Jetstream registration**: Quickslice tracks `xyz.statusphere.status` records from the network 532. **Database schema**: Quickslice creates a normalized table with proper columns and indexes 543. **GraphQL types**: Quickslice generates query, mutation, and subscription types 55 56| Without Quickslice | With Quickslice | 57|---|---| 58| Write Jetstream connection code | Import your Lexicon | 59| Filter events for your collection | `xyz.statusphere.status` | 60| Validate incoming records | | 61| Design database schema | Quickslice handles the rest. | 62| Write ingestion logic | | 63 64## Step 2: Querying Status Records 65 66Query indexed records with GraphQL. Quickslice generates a query for each Lexicon type using Relay-style connections: 67 68```graphql 69query GetStatuses { 70 xyzStatusphereStatus( 71 first: 20 72 sortBy: [{ field: createdAt, direction: DESC }] 73 ) { 74 edges { 75 node { 76 uri 77 did 78 status 79 createdAt 80 } 81 } 82 } 83} 84``` 85 86The `edges` and `nodes` pattern comes from [Relay](https://relay.dev/graphql/connections.htm), a GraphQL pagination specification. Each `edge` contains a `node` (the record) and a `cursor` for pagination. 87 88You can filter with `where` clauses: 89 90```graphql 91query RecentStatuses { 92 xyzStatusphereStatus( 93 first: 10 94 where: { status: { eq: "👍" } } 95 ) { 96 edges { 97 node { 98 did 99 status 100 } 101 } 102 } 103} 104``` 105 106| Without Quickslice | With Quickslice | 107|---|---| 108| Design query API | Query is auto-generated: | 109| Write database queries | | 110| Handle pagination logic | `xyzStatusphereStatus { edges { node { status } } }` | 111| Build filtering and sorting | | 112 113## Step 3: Joining Profile Data 114 115Here Quickslice shines. Every status record has a `did` field identifying its author. In Bluesky, profile information lives in `app.bsky.actor.profile` records. Join directly from a status to its author's profile: 116 117```graphql 118query StatusesWithProfiles { 119 xyzStatusphereStatus(first: 20) { 120 edges { 121 node { 122 status 123 createdAt 124 appBskyActorProfileByDid { 125 displayName 126 avatar { url } 127 } 128 } 129 } 130 } 131} 132``` 133 134The `appBskyActorProfileByDid` field is a **DID join**. It follows the `did` on the status record to find the profile authored by that identity. 135 136Quickslice: 137- Collects DIDs from the status records 138- Batches them into a single database query (DataLoader pattern) 139- Joins profile data efficiently 140 141| Without Quickslice | With Quickslice | 142|---|---| 143| Collect DIDs from status records | Add join to your query: | 144| Batch resolve DIDs to profiles | | 145| Handle N+1 query problem | `appBskyActorProfileByDid { displayName }` | 146| Write batching logic | | 147| Join data in API response | | 148 149### Other Join Types 150 151Quickslice also supports: 152 153- **Forward joins**: Follow a URI or strong ref to another record 154- **Reverse joins**: Find all records that reference a given record 155 156See the [Joins Guide](guides/joins.md) for complete documentation. 157 158## Step 4: Writing a Status (Mutations) 159 160To set a user's status, call a mutation: 161 162```graphql 163mutation CreateStatus($status: String!, $createdAt: DateTime!) { 164 createXyzStatusphereStatus( 165 input: { status: $status, createdAt: $createdAt } 166 ) { 167 uri 168 status 169 createdAt 170 } 171} 172``` 173 174Quickslice: 175 1761. **Writes to the user's PDS**: Creates the record in their personal data repository 1772. **Indexes optimistically**: The record appears in queries immediately, before Jetstream confirmation 1783. **Handles OAuth**: Uses the authenticated session to sign the write 179 180| Without Quickslice | With Quickslice | 181|---|---| 182| Get OAuth session/agent | Call the mutation: | 183| Construct record with $type | | 184| Call putRecord XRPC on the PDS | `createXyzStatusphereStatus(input: { status: "👍" })` | 185| Optimistically update local DB | | 186| Handle errors | | 187 188## Step 5: Authentication 189 190Quickslice bridges AT Protocol OAuth. Your frontend initiates login; Quickslice manages the authorization flow: 191 1921. User enters their handle (e.g., `alice.bsky.social`) 1932. Your app redirects to Quickslice's OAuth endpoint 1943. Quickslice redirects to the user's PDS for authorization 1954. User approves the app 1965. PDS redirects back to Quickslice with an auth code 1976. Quickslice exchanges the code for tokens and establishes a session 198 199For authenticated queries and mutations, include auth headers. The exact headers depend on your OAuth flow (DPoP or Bearer token). See the [Authentication Guide](guides/authentication.md) for details. 200 201## Step 6: Deploying to Railway 202 203Deploy quickly with Railway: 204 2051. Click the deploy button in the [Quickstart Guide](guides/deployment.md) 2062. Generate an OAuth signing key with `goat key generate -t p256` 2073. Paste the key into the `OAUTH_SIGNING_KEY` environment variable 2084. Generate a domain and redeploy 2095. Create your admin account by logging in 2106. Upload your Lexicons 211 212See [Deployment Guide](guides/deployment.md) for detailed instructions. 213 214## What Quickslice Handled 215 216Quickslice handled: 217 218- **Jetstream connection**: firehose connection, event filtering, reconnection 219- **Record validation**: schema checking against Lexicons 220- **Database schema**: tables, migrations, indexes 221- **Query API**: filtering, sorting, pagination endpoints 222- **Batching**: efficient related-record resolution 223- **Optimistic updates**: indexing before Jetstream confirmation 224- **OAuth flow**: token exchange, session management, DPoP proofs 225 226Focus on your application logic; Quickslice handles infrastructure. 227 228## Next Steps 229 230- [Queries Guide](guides/queries.md): Filtering, sorting, and pagination 231- [Joins Guide](guides/joins.md): Forward, reverse, and DID joins 232- [Mutations Guide](guides/mutations.md): Creating, updating, and deleting records 233- [Authentication Guide](guides/authentication.md): Setting up OAuth 234- [Deployment Guide](guides/deployment.md): Production configuration