···11-# Build stage
22-FROM rustlang/rust:nightly-slim AS builder
11+ARG GLEAM_VERSION=v1.13.0
3244-# Install build dependencies
55-RUN apt-get update && apt-get install -y \
66- pkg-config \
77- libssl-dev \
88- && rm -rf /var/lib/apt/lists/*
33+# Build stage - compile the application
44+FROM ghcr.io/gleam-lang/gleam:${GLEAM_VERSION}-erlang-alpine AS builder
951010-WORKDIR /app
66+# Install build dependencies (including PostgreSQL client for multi-database support)
77+RUN apk add --no-cache \
88+ bash \
99+ git \
1010+ nodejs \
1111+ npm \
1212+ build-base \
1313+ sqlite-dev \
1414+ postgresql-dev
11151212-# Copy manifests
1313-COPY Cargo.toml Cargo.lock ./
1616+# Configure git for non-interactive use
1717+ENV GIT_TERMINAL_PROMPT=0
14181515-# Copy source code
1616-COPY src ./src
1717-COPY templates ./templates
1818-COPY lexicons ./lexicons
1919-COPY static ./static
1919+# Clone quickslice at the v0.17.3 tag (includes sub claim fix)
2020+RUN git clone --depth 1 --branch v0.17.3 https://github.com/bigmoves/quickslice.git /build
2121+2222+# Install dependencies for all projects
2323+RUN cd /build/client && gleam deps download
2424+RUN cd /build/lexicon_graphql && gleam deps download
2525+RUN cd /build/server && gleam deps download
2626+2727+# Apply patches to dependencies
2828+RUN cd /build && patch -p1 < patches/mist-websocket-protocol.patch
2929+3030+# Install JavaScript dependencies for client
3131+RUN cd /build/client && npm install
20322121-# Build for release
2222-RUN cargo build --release
3333+# Compile the client code and output to server's static directory
3434+RUN cd /build/client \
3535+ && gleam add --dev lustre_dev_tools \
3636+ && gleam run -m lustre/dev build quickslice_client --minify --outdir=/build/server/priv/static
3737+3838+# Compile the server code
3939+RUN cd /build/server \
4040+ && gleam export erlang-shipment
23412424-# Runtime stage
2525-FROM debian:bookworm-slim
4242+# Runtime stage - slim image with only what's needed to run
4343+FROM ghcr.io/gleam-lang/gleam:${GLEAM_VERSION}-erlang-alpine
26442727-# Install runtime dependencies
2828-RUN apt-get update && apt-get install -y \
2929- ca-certificates \
3030- libssl3 \
3131- && rm -rf /var/lib/apt/lists/*
4545+# Install runtime dependencies and dbmate for migrations
4646+ARG TARGETARCH
4747+RUN apk add --no-cache sqlite-libs sqlite libpq curl \
4848+ && DBMATE_ARCH=$([ "$TARGETARCH" = "arm64" ] && echo "arm64" || echo "amd64") \
4949+ && curl -fsSL -o /usr/local/bin/dbmate https://github.com/amacneil/dbmate/releases/latest/download/dbmate-linux-${DBMATE_ARCH} \
5050+ && chmod +x /usr/local/bin/dbmate
32513333-WORKDIR /app
5252+# Copy the compiled server code from the builder stage
5353+COPY --from=builder /build/server/build/erlang-shipment /app
34543535-# Copy the built binary
3636-COPY --from=builder /app/target/release/nate-status /app/nate-status
5555+# Copy database migrations and config
5656+COPY --from=builder /build/server/db /app/db
5757+COPY --from=builder /build/server/.dbmate.yml /app/.dbmate.yml
5858+COPY --from=builder /build/server/docker-entrypoint.sh /app/docker-entrypoint.sh
37593838-# Copy templates and lexicons
3939-COPY templates ./templates
4040-COPY lexicons ./lexicons
4141-# Copy static files
4242-COPY static ./static
6060+# Set up the entrypoint
6161+WORKDIR /app
43624444-# Create directory for SQLite database
4545-RUN mkdir -p /data
6363+# Create the data directory for the SQLite database and Fly.io volume mount
6464+RUN mkdir -p /data && chmod 755 /data
46654766# Set environment variables
4848-ENV DB_PATH=/data/status.db
4949-ENV ENABLE_FIREHOSE=true
6767+ENV HOST=0.0.0.0
6868+ENV PORT=8080
50695151-# Expose port
5252-EXPOSE 8080
7070+# Expose the port the server will run on
7171+EXPOSE $PORT
53725454-# Run the binary
5555-CMD ["./nate-status"]7373+# Run the server
7474+CMD ["/app/docker-entrypoint.sh", "run"]
+45-65
README.md
···11-# status
11+# quickslice-status
2233-a personal status tracker built on at protocol, where i can post my current status (like slack status) decoupled from any specific platform.
33+a status app for bluesky, built with [quickslice](https://github.com/bigmoves/quickslice).
4455-live at: [status.zzstoatzz.io](https://status.zzstoatzz.io)
66-77-## about
55+**live:** https://quickslice-status.pages.dev
8699-this is my personal status url - think of it like a service health page, but for a person. i can update my status with an emoji and optional text, and it's stored permanently in my at protocol repository.
77+## architecture
1081111-## credits
99+- **backend**: [quickslice](https://github.com/bigmoves/quickslice) on fly.io - handles oauth, graphql api, jetstream ingestion
1010+- **frontend**: static site on cloudflare pages - vanilla js spa
12111313-this app is based on [bailey townsend's rusty statusphere](https://github.com/fatfingers23/rusty_statusphere_example_app), which is an excellent rust implementation of the at protocol quick start guide. bailey did all the heavy lifting with the atproto integration and the overall architecture. i've adapted it for my personal use case.
1212+## deployment
14131515-major thanks to:
1616-- [bailey townsend (@baileytownsend.dev)](https://bsky.app/profile/baileytownsend.dev) for the rusty statusphere boilerplate
1717-- the atrium-rs maintainers for the rust at protocol libraries
1818-- the rocketman maintainers for the jetstream consumer
1414+### backend (fly.io)
19152020-## development
1616+builds quickslice from source at v0.17.3 tag.
21172218```bash
2323-cp .env.template .env
2424-cargo run
2525-# navigate to http://127.0.0.1:8080
1919+fly deploy
2620```
27212828-### custom emojis (no redeploys)
2929-3030-Emojis are now served from a runtime directory configured by `EMOJI_DIR` (defaults to `static/emojis` locally; set to `/data/emojis` on Fly.io). On startup, if the runtime emoji directory is empty, it will be seeded from the bundled `static/emojis`.
3131-3232-- Local dev: add image files to `static/emojis/` (or set `EMOJI_DIR` in `.env`).
3333-- Production (Fly.io): upload files directly into the mounted volume at `/data/emojis` — no rebuild or redeploy needed.
3434-3535-Examples with Fly CLI:
3636-2222+required secrets:
3723```bash
3838-# Open an SSH console to the machine
3939-fly ssh console -a zzstoatzz-status
4040-4141-# Inside the VM, copy or fetch files into /data/emojis
4242-mkdir -p /data/emojis
4343-curl -L -o /data/emojis/my_new_emoji.png https://example.com/my_new_emoji.png
2424+fly secrets set SECRET_KEY_BASE="$(openssl rand -base64 64 | tr -d '\n')"
2525+fly secrets set OAUTH_SIGNING_KEY="$(goat key generate -t p256 | tail -1)"
4426```
45274646-Or from your machine using SFTP:
2828+### frontend (cloudflare pages)
47294830```bash
4949-fly ssh sftp -a zzstoatzz-status
5050-sftp> put ./static/emojis/my_new_emoji.png /data/emojis/
3131+cd site
3232+npx wrangler pages deploy . --project-name=quickslice-status
5133```
52345353-The app serves them at `/emojis/<filename>` and lists them via `/api/custom-emojis`.
3535+## oauth client registration
54365555-### admin upload endpoint
3737+register an oauth client in the quickslice admin ui at `https://zzstoatzz-quickslice-status.fly.dev/`
56385757-When logged in as the admin DID, you can upload PNG or GIF emojis without SSH via a simple endpoint:
3939+redirect uri: `https://quickslice-status.pages.dev/callback`
58405959-- Endpoint: `POST /admin/upload-emoji`
6060-- Auth: session-based; only the admin DID is allowed
6161-- Form fields (multipart/form-data):
6262- - `file`: the image file (PNG or GIF), max 5MB
6363- - `name` (optional): base filename (letters, numbers, `-`, `_`) without extension
4141+## lexicon
64426565-Example with curl:
4343+uses `io.zzstoatzz.status` lexicon for user statuses.
66446767-```bash
6868-curl -i -X POST \
6969- -F "file=@./static/emojis/sample.png" \
7070- -F "name=my_sample" \
7171- http://localhost:8080/admin/upload-emoji
4545+```json
4646+{
4747+ "lexicon": 1,
4848+ "id": "io.zzstoatzz.status",
4949+ "defs": {
5050+ "main": {
5151+ "type": "record",
5252+ "key": "self",
5353+ "record": {
5454+ "type": "object",
5555+ "required": ["status", "createdAt"],
5656+ "properties": {
5757+ "status": { "type": "string", "maxLength": 128 },
5858+ "createdAt": { "type": "string", "format": "datetime" }
5959+ }
6060+ }
6161+ }
6262+ }
6363+}
7264```
73657474-Response will include the public URL (e.g., `/emojis/my_sample.png`).
7575-7676-### available commands
6666+## local development
77677878-we use [just](https://github.com/casey/just) for common tasks:
7979-6868+serve the frontend locally:
8069```bash
8181-just watch # run with hot-reloading
8282-just deploy # deploy to fly.io
8383-just lint # run clippy
8484-just fmt # format code
8585-just clean # clean build artifacts
7070+cd site
7171+python -m http.server 8000
8672```
87738888-## tech stack
8989-9090-- [rust](https://www.rust-lang.org/) with [actix web](https://actix.rs/)
9191-- [at protocol](https://atproto.com/) (via [atrium-rs](https://github.com/sugyan/atrium))
9292-- [sqlite](https://www.sqlite.org/) for local storage
9393-- [jetstream](https://github.com/bluesky-social/jetstream) for firehose consumption
9494-- [fly.io](https://fly.io/) for hosting
7474+for oauth to work locally, you'd need to register a separate oauth client with `http://localhost:8000/callback` as the redirect uri and update `CONFIG.clientId` in `app.js`.