forked from
slices.network/quickslice
Auto-indexing service and GraphQL API for AT Protocol Records
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