Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 0 additions & 33 deletions .github/workflows/ci.yml

This file was deleted.

52 changes: 0 additions & 52 deletions .github/workflows/fly-review.yml

This file was deleted.

16 changes: 0 additions & 16 deletions .github/workflows/fly.yml

This file was deleted.

9 changes: 5 additions & 4 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
/target
.idea
.env
statusphere.sqlite3
# wrangler/cloudflare
.wrangler/

# notes
oauth-experience.md
99 changes: 59 additions & 40 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,55 +1,74 @@
# Build stage
FROM rustlang/rust:nightly-slim AS builder
ARG GLEAM_VERSION=v1.13.0

# Install build dependencies
RUN apt-get update && apt-get install -y \
pkg-config \
libssl-dev \
&& rm -rf /var/lib/apt/lists/*
# Build stage - compile the application
FROM ghcr.io/gleam-lang/gleam:${GLEAM_VERSION}-erlang-alpine AS builder

WORKDIR /app
# Install build dependencies (including PostgreSQL client for multi-database support)
RUN apk add --no-cache \
bash \
git \
nodejs \
npm \
build-base \
sqlite-dev \
postgresql-dev

# Copy manifests
COPY Cargo.toml Cargo.lock ./
# Configure git for non-interactive use
ENV GIT_TERMINAL_PROMPT=0

# Copy source code
COPY src ./src
COPY templates ./templates
COPY lexicons ./lexicons
COPY static ./static
# Clone quickslice at the v0.17.3 tag (includes sub claim fix)
RUN git clone --depth 1 --branch v0.17.3 https://github.com/bigmoves/quickslice.git /build

# Build for release
RUN cargo build --release
# Install dependencies for all projects
RUN cd /build/client && gleam deps download
RUN cd /build/lexicon_graphql && gleam deps download
RUN cd /build/server && gleam deps download

# Runtime stage
FROM debian:bookworm-slim
# Apply patches to dependencies
RUN cd /build && patch -p1 < patches/mist-websocket-protocol.patch

# Install runtime dependencies
RUN apt-get update && apt-get install -y \
ca-certificates \
libssl3 \
&& rm -rf /var/lib/apt/lists/*
# Install JavaScript dependencies for client
RUN cd /build/client && npm install

WORKDIR /app
# Compile the client code and output to server's static directory
RUN cd /build/client \
&& gleam add --dev lustre_dev_tools \
&& gleam run -m lustre/dev build quickslice_client --minify --outdir=/build/server/priv/static

# Compile the server code
RUN cd /build/server \
&& gleam export erlang-shipment

# Runtime stage - slim image with only what's needed to run
FROM ghcr.io/gleam-lang/gleam:${GLEAM_VERSION}-erlang-alpine

# Copy the built binary
COPY --from=builder /app/target/release/nate-status /app/nate-status
# Install runtime dependencies and dbmate for migrations
ARG TARGETARCH
RUN apk add --no-cache sqlite-libs sqlite libpq curl \
&& DBMATE_ARCH=$([ "$TARGETARCH" = "arm64" ] && echo "arm64" || echo "amd64") \
&& curl -fsSL -o /usr/local/bin/dbmate https://github.com/amacneil/dbmate/releases/latest/download/dbmate-linux-${DBMATE_ARCH} \
&& chmod +x /usr/local/bin/dbmate

# Copy templates and lexicons
COPY templates ./templates
COPY lexicons ./lexicons
# Copy static files
COPY static ./static
# Copy the compiled server code from the builder stage
COPY --from=builder /build/server/build/erlang-shipment /app

# Copy database migrations and config
COPY --from=builder /build/server/db /app/db
COPY --from=builder /build/server/.dbmate.yml /app/.dbmate.yml
COPY --from=builder /build/server/docker-entrypoint.sh /app/docker-entrypoint.sh

# Set up the entrypoint
WORKDIR /app

# Create directory for SQLite database
RUN mkdir -p /data
# Create the data directory for the SQLite database and Fly.io volume mount
RUN mkdir -p /data && chmod 755 /data

# Set environment variables
ENV DB_PATH=/data/status.db
ENV ENABLE_FIREHOSE=true
ENV HOST=0.0.0.0
ENV PORT=8080

# Expose port
EXPOSE 8080
# Expose the port the server will run on
EXPOSE $PORT

# Run the binary
CMD ["./nate-status"]
# Run the server
CMD ["/app/docker-entrypoint.sh", "run"]
120 changes: 50 additions & 70 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,94 +1,74 @@
# status
# quickslice-status

a personal status tracker built on at protocol, where i can post my current status (like slack status) decoupled from any specific platform.
a status app for bluesky, built with [quickslice](https://github.com/bigmoves/quickslice).

live at: [status.zzstoatzz.io](https://status.zzstoatzz.io)
**live:** https://quickslice-status.pages.dev

## about
## architecture

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.
- **backend**: [quickslice](https://github.com/bigmoves/quickslice) on fly.io - handles oauth, graphql api, jetstream ingestion
- **frontend**: static site on cloudflare pages - vanilla js spa

## credits
## deployment

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.
### backend (fly.io)

major thanks to:
- [bailey townsend (@baileytownsend.dev)](https://bsky.app/profile/baileytownsend.dev) for the rusty statusphere boilerplate
- the atrium-rs maintainers for the rust at protocol libraries
- the rocketman maintainers for the jetstream consumer

## development
builds quickslice from source at v0.17.3 tag.

```bash
cp .env.template .env
cargo run
# navigate to http://127.0.0.1:8080
fly deploy
```

### custom emojis (no redeploys)

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`.

- Local dev: add image files to `static/emojis/` (or set `EMOJI_DIR` in `.env`).
- Production (Fly.io): upload files directly into the mounted volume at `/data/emojis` — no rebuild or redeploy needed.

Examples with Fly CLI:

required secrets:
```bash
# Open an SSH console to the machine
fly ssh console -a zzstoatzz-status

# Inside the VM, copy or fetch files into /data/emojis
mkdir -p /data/emojis
curl -L -o /data/emojis/my_new_emoji.png https://example.com/my_new_emoji.png
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)"
```

Or from your machine using SFTP:
### frontend (cloudflare pages)

```bash
fly ssh sftp -a zzstoatzz-status
sftp> put ./static/emojis/my_new_emoji.png /data/emojis/
cd site
npx wrangler pages deploy . --project-name=quickslice-status
```

The app serves them at `/emojis/<filename>` and lists them via `/api/custom-emojis`.

### admin upload endpoint

When logged in as the admin DID, you can upload PNG or GIF emojis without SSH via a simple endpoint:

- Endpoint: `POST /admin/upload-emoji`
- Auth: session-based; only the admin DID is allowed
- Form fields (multipart/form-data):
- `file`: the image file (PNG or GIF), max 5MB
- `name` (optional): base filename (letters, numbers, `-`, `_`) without extension

Example with curl:

```bash
curl -i -X POST \
-F "file=@./static/emojis/sample.png" \
-F "name=my_sample" \
http://localhost:8080/admin/upload-emoji
## oauth client registration

register an oauth client in the quickslice admin ui at `https://zzstoatzz-quickslice-status.fly.dev/`

redirect uri: `https://quickslice-status.pages.dev/callback`

## lexicon

uses `io.zzstoatzz.status` lexicon for user statuses.

```json
{
"lexicon": 1,
"id": "io.zzstoatzz.status",
"defs": {
"main": {
"type": "record",
"key": "self",
"record": {
"type": "object",
"required": ["status", "createdAt"],
"properties": {
"status": { "type": "string", "maxLength": 128 },
"createdAt": { "type": "string", "format": "datetime" }
}
}
}
}
}
```

Response will include the public URL (e.g., `/emojis/my_sample.png`).

### available commands

we use [just](https://github.com/casey/just) for common tasks:
## local development

serve the frontend locally:
```bash
just watch # run with hot-reloading
just deploy # deploy to fly.io
just lint # run clippy
just fmt # format code
just clean # clean build artifacts
cd site
python -m http.server 8000
```

## tech stack

- [rust](https://www.rust-lang.org/) with [actix web](https://actix.rs/)
- [at protocol](https://atproto.com/) (via [atrium-rs](https://github.com/sugyan/atrium))
- [sqlite](https://www.sqlite.org/) for local storage
- [jetstream](https://github.com/bluesky-social/jetstream) for firehose consumption
- [fly.io](https://fly.io/) for hosting
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`.
Loading