(most of this was lockfile / not source code tbh)
a while ago, i forked @baileytownsend.dev's wonderful rusty statusphere repo and added in a whole lotta bufo so it could serve as any atproto identity's personal status page. thanks bailey :D
then, yesterday @chadtmiller.com released quickslice!
My crazy idea to cram all of the AT Protocol into a single deployable container. Works with SQLite, Postgres and potentially more data stores in the future. Steps are literally deploy container -> import your lexicons -> backfill -> build your app with the GraphQL APIs. quickslice.slices.network
quickslice - Introduction
and i thought:
hey i should be able to replace the rusty-statusphere-derivative backend for status.zzstoatzz.io with a quickslice instance!
i wasn't yet sure how to write/deploy a UI that consumes the quickslice instance, but i had already ran the quickslice server locally and figured out how to configure the /settings more or less:
upload my pre-existing lexicons
make a zip like
zip -j lexicons.zip lexicons/*.jsonupload it in Lexicon portion of the settings
set a domain authority
io.zzstoatzz
set supported oauth scopes (no
transition:genericallowed!)io.zzstoatzz.status.recordio.zzstoatzz.status.preferences
register an OAuth client
client name: {anything} (e.g. "status")
redirect URI (one per line)
http://localhost:8000/ (for local dev)
scope (space delimited)
atprotorepo:io.zzstoatzz.status.recordrepo:io.zzstoatzz.status.preferences
note previously, preferences were persisted in the sqlite db that my rust backend talked to so font/accent preferences stuck across devices, but during this refactor i realized i could just store them as a record! not sure what the meta for "hey i wanna store just a little somethin non-standard in the quickslice db" is quite yet
the config looks like this in the admin panel of my quickslice instance
backend secrets and config
the quickslice instance needed two secrets for OAuth to work:
fly secrets set SECRET_KEY_BASE="$(openssl rand -base64 64 | tr -d '\n')"
fly secrets set OAUTH_SIGNING_KEY="$(goat key generate -t p256 | tail -1)"(i think?) the OAUTH_SIGNING_KEY should be just the multibase key (starts with z), not the full output from goat... that's what worked for me anyways, lmk if i am wrong about this!
without EXTERNAL_BASE_URL, quickslice uses 0.0.0.0:8080 in its OAuth client metadata, so go ahead and set it to your public URL:
[env]
EXTERNAL_BASE_URL = 'https://your-app.fly.dev'i used this thread to figure this out
going to try and get quickslice set up (tried like 2 weeks ago but failed miserably) ๐
so then after a smol bug encounter where quickslice was not returning sub from the token endpoint (causing login to redirect back to a logged out state), i rather expediently had a quickslice instance that could (very quickly!) backfill all the status records! excellent!
now i just needed to rewrite (or have claude rewrite tbh) the UI to consume my new backend in a box!
the UI!
this part turned out to be easier than i thought!
your lexicons are the API! after uploading the lexicons, you can just write GraphQL queries from your front-end to get your data!
const response = await fetch(`https://your-quickslice.com/api/graphql`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
query: `
query GetStatuses($did: String!) {
ioZzstoatzzStatusRecords(
where: { did: { eq: $did } }
orderBy: { createdAt: DESC }
first: 50
) {
nodes { uri did actorHandle emoji text createdAt }
}
}
`,
variables: { did }
})
});
i just told claude to broadly replace the calls to my old rust server endpoints in the UI with graphQL calls.... and then i went in the admin panel and clicked "Backfill" (once or twice, since it seemed it didn't totally backfill the first time? not sure).
then quickslice-client-js handles the OAuth flow in the browser:
const client = await QuicksliceClient.create({
server: 'https://your-quickslice.com', // your quickslice instance
clientId: 'client_xxx', // from quickslice admin UI
redirectUri: window.location.origin + '/', // where OAuth redirects back
});
// start login
await client.signIn(handle);
// after redirect, client.agent is authenticated
const { data } = await client.agent.getProfile({ actor: client.agent.session.did });the clientId comes from registering an OAuth client in the quickslice admin UI. the redirect URI should match what you registered.
since quickslice serves its own admin UI at the root, we host our frontend separately on cloudflare pages. the frontend is vanilla JS - no framework, just a single app.js file (i do backend by trade! shh)
this is what worked for me as i tried to figure this out! if you have suggestions for improvements, please leave a comment here!
once i had my new backend and cloudflare pages, i switched my CNAME for status.zzstoatzz.io to point at the new cloudflare page.
and i deleted my old rust/fly backend and bob was then my uncle!
the whole thing!
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ cloudflare pages โ
โ status.zzstoatzz.io โ
โ โ
โ โโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโ โ
โ โ index.html โ โ app.js โ โ styles.css โ โ
โ โโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
โ GraphQL + OAuth
โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ fly.io โ
โ zzstoatzz-quickslice-status.fly.dev โ
โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ quickslice โ โ
โ โ โข OAuth server (PKCE + DPoP) โ โ
โ โ โข GraphQL API (auto-generated from lexicons) โ โ
โ โ โข Jetstream consumer โ โ
โ โ โข SQLite database โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
โ Jetstream
โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ AT Protocol โ
โ (bluesky PDS, jetstream firehose) โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโget sliced!
links!
quickslice - the framework
AT protocol OAuth - the spec
quickslice-client-js - frontend OAuth helper
thanks chad for reviewing a draft of this post!