···11+# migrating to quickslice: a status app rewrite
22+33+## what we built
44+55+a bluesky status app that lets users set emoji statuses (like slack status) stored in their AT protocol repository. the app has two parts:
66+77+- **backend**: [quickslice](https://github.com/bigmoves/quickslice) on fly.io - handles OAuth, GraphQL API, and jetstream ingestion
88+- **frontend**: vanilla JS SPA on cloudflare pages
99+1010+live at https://status.zzstoatzz.io
1111+1212+## why quickslice
1313+1414+the 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:
1515+1616+- OAuth 2.0 with PKCE + DPoP (the hard part of AT protocol)
1717+- GraphQL API auto-generated from your lexicons
1818+- jetstream consumer for real-time firehose data
1919+- admin UI for managing OAuth clients
2020+2121+## the migration
2222+2323+### 1. lexicon design
2424+2525+quickslice ingests data based on lexicons you define. we have two:
2626+2727+**io.zzstoatzz.status.record** - the actual status
2828+```json
2929+{
3030+ "emoji": "🔥",
3131+ "text": "shipping code",
3232+ "createdAt": "2025-12-13T12:00:00Z"
3333+}
3434+```
3535+3636+**io.zzstoatzz.status.preferences** - user display preferences
3737+```json
3838+{
3939+ "accentColor": "#4a9eff",
4040+ "theme": "dark"
4141+}
4242+```
4343+4444+### 2. frontend architecture
4545+4646+since 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:
4747+4848+- quickslice backend on fly.io (`zzstoatzz-quickslice-status.fly.dev`)
4949+- static frontend on cloudflare pages (`status.zzstoatzz.io`)
5050+5151+the frontend uses the `quickslice-client-js` library for OAuth:
5252+```html
5353+<script src="https://cdn.jsdelivr.net/gh/bigmoves/quickslice@v0.17.3/quickslice-client-js/dist/quickslice-client.min.js"></script>
5454+```
5555+5656+### 3. OAuth flow
5757+5858+quickslice handles the OAuth server side. the frontend just needs to:
5959+6060+1. create a client with `QuicksliceClient.create()`
6161+2. call `client.signIn()` to start the flow
6262+3. handle the callback (quickslice redirects back with auth tokens)
6363+4. use `client.agent` for authenticated AT protocol operations
6464+6565+the redirect URI is just the root of your site (e.g., `https://status.zzstoatzz.io/`).
6666+6767+## problems we hit
6868+6969+### the `sub` claim fix
7070+7171+the 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.
7272+7373+the 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:
7474+7575+```dockerfile
7676+# Clone quickslice at the v0.17.3 tag (includes sub claim fix)
7777+RUN git clone --depth 1 --branch v0.17.3 https://github.com/bigmoves/quickslice.git /build
7878+```
7979+8080+### secrets configuration
8181+8282+quickslice needs two secrets for OAuth to work:
8383+8484+```bash
8585+fly secrets set SECRET_KEY_BASE="$(openssl rand -base64 64 | tr -d '\n')"
8686+fly secrets set OAUTH_SIGNING_KEY="$(goat key generate -t p256 | tail -1)"
8787+```
8888+8989+the `OAUTH_SIGNING_KEY` must be just the multibase key (starts with `z`), not the full output from goat.
9090+9191+### EXTERNAL_BASE_URL
9292+9393+without this, quickslice uses `0.0.0.0:8080` in its OAuth client metadata, which breaks the flow. set it to your public URL:
9494+9595+```toml
9696+[env]
9797+ EXTERNAL_BASE_URL = 'https://zzstoatzz-quickslice-status.fly.dev'
9898+```
9999+100100+### PDS caching
101101+102102+when 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.
103103+104104+## deployment architecture
105105+106106+```
107107+┌─────────────────────────────────────────────────────────┐
108108+│ cloudflare pages │
109109+│ status.zzstoatzz.io │
110110+│ │
111111+│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
112112+│ │ index.html │ │ app.js │ │ styles.css │ │
113113+│ └─────────────┘ └─────────────┘ └─────────────┘ │
114114+└─────────────────────────────────────────────────────────┘
115115+ │
116116+ │ GraphQL + OAuth
117117+ ▼
118118+┌─────────────────────────────────────────────────────────┐
119119+│ fly.io │
120120+│ zzstoatzz-quickslice-status.fly.dev │
121121+│ │
122122+│ ┌─────────────────────────────────────────────────┐ │
123123+│ │ quickslice │ │
124124+│ │ • OAuth server (PKCE + DPoP) │ │
125125+│ │ • GraphQL API (auto-generated from lexicons) │ │
126126+│ │ • Jetstream consumer │ │
127127+│ │ • SQLite database │ │
128128+│ └─────────────────────────────────────────────────┘ │
129129+└─────────────────────────────────────────────────────────┘
130130+ │
131131+ │ Jetstream
132132+ ▼
133133+┌─────────────────────────────────────────────────────────┐
134134+│ AT Protocol │
135135+│ (bluesky PDS, jetstream firehose) │
136136+└─────────────────────────────────────────────────────────┘
137137+```
138138+139139+## key takeaways
140140+141141+1. **quickslice eliminates the hard parts** - OAuth and jetstream are notoriously tricky. quickslice handles them so you can focus on your app logic.
142142+143143+2. **separate frontend and backend** - quickslice serves its own admin UI, so host your frontend elsewhere. cloudflare pages is free and fast.
144144+145145+3. **pin your dependencies** - we got bit by `:latest` not being latest. pin to specific versions/tags.
146146+147147+4. **check the image version** - `fly image show` tells you exactly what's deployed. don't assume.
148148+149149+5. **GraphQL is your API** - quickslice auto-generates a GraphQL API from your lexicons. no need to write endpoints.
150150+151151+6. **the sub claim matters** - AT protocol OAuth needs the `sub` claim in token responses. this was the root cause of our redirect loop.
152152+153153+## resources
154154+155155+- [quickslice](https://github.com/bigmoves/quickslice) - the framework
156156+- [AT protocol OAuth](https://atproto.com/specs/oauth) - the spec
157157+- [quickslice-client-js](https://github.com/bigmoves/quickslice/tree/main/quickslice-client-js) - frontend OAuth helper