slack status without the slack status.zzstoatzz.io/
quickslice
at main 235 lines 9.8 kB view raw view rendered
1# migrating to quickslice: a status app rewrite 2 3## what we built 4 5a bluesky status app that lets users set emoji statuses (like slack status) stored in their AT protocol repository. the app has two parts: 6 7- **backend**: [quickslice](https://github.com/bigmoves/quickslice) on fly.io - handles OAuth, GraphQL API, and jetstream ingestion 8- **frontend**: vanilla JS SPA on cloudflare pages 9 10live at https://status.zzstoatzz.io 11 12## why quickslice 13 14the original implementation was a custom rust backend using atrium-rs. it worked, but maintaining OAuth, jetstream ingestion, and all the AT protocol plumbing was a lot. quickslice handles all of that out of the box: 15 16- OAuth 2.0 with PKCE + DPoP (the hard part of AT protocol) 17- GraphQL API auto-generated from your lexicons 18- jetstream consumer for real-time firehose data 19- admin UI for managing OAuth clients 20 21## the migration 22 23### 1. lexicon design 24 25quickslice ingests data based on lexicons you define. we have two: 26 27**io.zzstoatzz.status.record** - the actual status 28```json 29{ 30 "emoji": "🔥", 31 "text": "shipping code", 32 "createdAt": "2025-12-13T12:00:00Z" 33} 34``` 35 36**io.zzstoatzz.status.preferences** - user display preferences 37```json 38{ 39 "accentColor": "#4a9eff", 40 "theme": "dark" 41} 42``` 43 44### 2. frontend architecture 45 46since quickslice serves its own admin UI at the root path, we couldn't bundle our frontend into the same container. this led to a clean separation: 47 48- quickslice backend on fly.io (`zzstoatzz-quickslice-status.fly.dev`) 49- static frontend on cloudflare pages (`status.zzstoatzz.io`) 50 51the frontend uses the `quickslice-client-js` library for OAuth: 52```html 53<script src="https://cdn.jsdelivr.net/gh/bigmoves/quickslice@v0.17.3/quickslice-client-js/dist/quickslice-client.min.js"></script> 54``` 55 56### 3. the UI 57 58since quickslice serves its own admin UI at the root path, we host our frontend separately on cloudflare pages. the frontend is vanilla JS - no framework, just a single `app.js` file. 59 60**OAuth with quickslice-client-js** 61 62the `quickslice-client-js` library handles the OAuth flow in the browser: 63 64```javascript 65const client = await QuicksliceClient.create({ 66 server: 'https://your-app.fly.dev', // your quickslice instance 67 clientId: 'client_xxx', // from quickslice admin UI 68 redirectUri: window.location.origin + '/', // where OAuth redirects back 69}); 70 71// start login 72await client.signIn(handle); 73 74// after redirect, client.agent is authenticated 75const { data } = await client.agent.getProfile({ actor: client.agent.session.did }); 76``` 77 78the `clientId` comes from registering an OAuth client in the quickslice admin UI. the redirect URI should match what you registered. 79 80**GraphQL queries** 81 82quickslice auto-generates a GraphQL API from your lexicons. querying status records looks like: 83 84```javascript 85const response = await fetch(`https://your-app.fly.dev/api/graphql`, { 86 method: 'POST', 87 headers: { 'Content-Type': 'application/json' }, 88 body: JSON.stringify({ 89 query: ` 90 query GetStatuses($did: String!) { 91 ioZzstoatzzStatusRecords( 92 where: { did: { eq: $did } } 93 orderBy: { createdAt: DESC } 94 first: 50 95 ) { 96 nodes { uri did emoji text createdAt } 97 } 98 } 99 `, 100 variables: { did } 101 }) 102}); 103``` 104 105the query name is auto-generated from your lexicon ID - dots become camelCase (e.g., `io.zzstoatzz.status.record``ioZzstoatzzStatusRecords`). 106 107no need to write resolvers or schema - it's all generated from the lexicon definitions. 108 109## problems we hit 110 111### the `sub` claim fix 112 113the biggest issue: after OAuth login, the app would redirect loop infinitely. the AT protocol SDK needs a `sub` claim in the OAuth token response to identify the user, but quickslice v0.17.2 didn't include it. 114 115the fix was in v0.17.3 (commit `0b2d54a`), but `ghcr.io/bigmoves/quickslice:latest` still pointed to v0.17.2. we had to build from source: 116 117```dockerfile 118# Clone quickslice at the v0.17.3 tag (includes sub claim fix) 119RUN git clone --depth 1 --branch v0.17.3 https://github.com/bigmoves/quickslice.git /build 120``` 121 122### secrets configuration 123 124quickslice needs two secrets for OAuth to work: 125 126```bash 127fly secrets set SECRET_KEY_BASE="$(openssl rand -base64 64 | tr -d '\n')" 128fly secrets set OAUTH_SIGNING_KEY="$(goat key generate -t p256 | tail -1)" 129``` 130 131the `OAUTH_SIGNING_KEY` must be just the multibase key (starts with `z`), not the full output from goat. 132 133### EXTERNAL_BASE_URL 134 135without this, quickslice uses `0.0.0.0:8080` in its OAuth client metadata, which breaks the flow. set it to your public URL: 136 137```toml 138[env] 139 EXTERNAL_BASE_URL = 'https://your-app.fly.dev' 140``` 141 142### PDS caching 143 144when debugging OAuth issues, be aware that your PDS caches OAuth client metadata. if you fix something on the server, the PDS might still have the old metadata cached. this caused some confusion during debugging. 145 146## deployment architecture 147 148``` 149┌─────────────────────────────────────────────────────────┐ 150│ cloudflare pages │ 151│ your-frontend.com │ 152│ │ 153│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ 154│ │ index.html │ │ app.js │ │ styles.css │ │ 155│ └─────────────┘ └─────────────┘ └─────────────┘ │ 156└─────────────────────────────────────────────────────────┘ 157158 │ GraphQL + OAuth 159160┌─────────────────────────────────────────────────────────┐ 161│ fly.io │ 162│ your-app.fly.dev │ 163│ │ 164│ ┌─────────────────────────────────────────────────┐ │ 165│ │ quickslice │ │ 166│ │ • OAuth server (PKCE + DPoP) │ │ 167│ │ • GraphQL API (auto-generated from lexicons) │ │ 168│ │ • Jetstream consumer │ │ 169│ │ • SQLite database │ │ 170│ └─────────────────────────────────────────────────┘ │ 171└─────────────────────────────────────────────────────────┘ 172173 │ Jetstream 174175┌─────────────────────────────────────────────────────────┐ 176│ AT Protocol │ 177│ (bluesky PDS, jetstream firehose) │ 178└─────────────────────────────────────────────────────────┘ 179``` 180 181## what quickslice eliminated 182 183the rust backend was ~2000 lines of code handling: 184 185- OAuth server implementation (PKCE + DPoP) 186- jetstream consumer for firehose ingestion 187- custom API endpoints for reading/writing statuses 188- session management 189- database queries 190 191with quickslice, all of that is replaced by: 192 193- a Dockerfile that builds quickslice from source 194- a fly.toml with env vars 195- two secrets 196 197the frontend is still custom (~1200 lines), but the backend complexity is gone. 198 199## deployment checklist 200 201when deploying quickslice: 202 203```bash 204# 1. set required secrets 205fly secrets set SECRET_KEY_BASE="$(openssl rand -base64 64 | tr -d '\n')" 206fly secrets set OAUTH_SIGNING_KEY="$(goat key generate -t p256 | tail -1)" 207 208# 2. deploy (builds from source, takes ~3 min) 209fly deploy 210 211# 3. in quickslice admin UI: 212# - set domain authority (e.g., io.zzstoatzz) 213# - add supported lexicons 214# - register OAuth client with redirect URI 215``` 216 217## key takeaways 218 2191. **quickslice eliminates the hard parts** - OAuth and jetstream are notoriously tricky. quickslice handles them so you can focus on your app logic. 220 2212. **separate frontend and backend** - quickslice serves its own admin UI, so host your frontend elsewhere. cloudflare pages is free and fast. 222 2233. **pin your dependencies** - we got bit by `:latest` not being latest. pin to specific versions/tags. 224 2254. **check the image version** - `fly image show` tells you exactly what's deployed. don't assume. 226 2275. **GraphQL is your API** - quickslice auto-generates a GraphQL API from your lexicons. no need to write endpoints. 228 2296. **the sub claim matters** - AT protocol OAuth needs the `sub` claim in token responses. this was the root cause of our redirect loop. 230 231## resources 232 233- [quickslice](https://github.com/bigmoves/quickslice) - the framework 234- [AT protocol OAuth](https://atproto.com/specs/oauth) - the spec 235- [quickslice-client-js](https://github.com/bigmoves/quickslice/tree/main/quickslice-client-js) - frontend OAuth helper