atmo.quest#
An event-companion app on ATProto. Scan a QR, write a private note, leave a conference with a real follow-up list. Records live in your PDS, not ours.
Quick start#
cp .env.example .env
go tool task live
Then open the URL printed in the logs (defaults to http://127.0.0.1:9090). The task live recipe runs templ-watch, the web-asset bundler, and an air-driven live-reloading Go server. task is registered as a Go tool dependency, so no separate install is needed. If you'd rather have a bare task on your PATH: brew install go-task. For a one-shot run without live reload: go run -tags dev ./cmd/web.
On first boot the app creates data/atmoquest.db (SQLite) and data/admin_signing.key (ed25519, badge signing). No further setup needed.
Use
127.0.0.1, notlocalhost— atproto OAuth requires the redirect URI to match the configuredPUBLIC_URLexactly. If port 9090 is taken on your machine, edit.envand update bothPORTandPUBLIC_URLto match.
Useful routes#
| Route | Notes |
|---|---|
/ |
Guest landing or authed dashboard, depending on session |
/signin |
Start OAuth via your Bluesky handle |
/profile, /profile/edit |
View / edit your atmo.quest profile |
/c/{did} |
Connect page (target of QR scans) |
/e/{token} |
Event check-in page |
/admin |
Admin console — requires users.is_admin = 1 |
/healthz |
Liveness probe |
To grant yourself admin after signing in once, either:
# Local dev: edit the SQLite file directly.
sqlite3 data/atmoquest.db "UPDATE users SET is_admin=1 WHERE did='did:plc:…';"
Or set BOOTSTRAP_ADMIN_DIDS (space-separated) and restart the app. The listed DIDs are promoted to admin on every boot — idempotent and safe to leave set. This is the supported path in deployed environments where sqlite3 isn't reachable (the prod image is scratch-based):
BOOTSTRAP_ADMIN_DIDS="did:plc:xxx did:plc:yyy"
Either way, the DID must have signed in at least once first (so the users row exists).
Layout#
cmd/web/ entrypoint
config/ env + config singleton
router/ top-level route wiring
features/<name>/ handlers.go, routes.go, pages/*.templ
internal/<name>/ storage & domain logic (event, checkin, profile, …)
web/resources/ static assets (css, js, fonts, datastar)
Env vars#
| Var | Default | Required |
|---|---|---|
PORT |
8080 |
no |
PUBLIC_URL |
http://127.0.0.1:$PORT |
no (dev) / yes (prod) |
SESSION_SECRET |
dev default | yes in prod |
DATABASE_URL |
file:data/atmoquest.db?… |
no |
ADMIN_SIGNING_KEY |
auto-generated in dev | yes in prod |
BOOTSTRAP_ADMIN_DIDS |
unset | no |
LOG_LEVEL |
INFO |
no |
-tags dev selects the dev config (random session key, auto-generated admin key). -tags prod (or no tag, in CI) requires the real secrets above.
Tests#
go test -tags dev ./...
Production build#
go tool task build # writes bin/main, embeds prod tag
./bin/main
Or Docker:
docker build -t atmoquest:latest .
docker run --rm -p 8080:8080 \
-v atmoquest_data:/data \
-e PUBLIC_URL=https://your.domain \
-e SESSION_SECRET=$(openssl rand -hex 32) \
-e ADMIN_SIGNING_KEY=$(openssl rand -base64 32) \
-e BUCKET_NAME=your-bucket \
-e AWS_ENDPOINT_URL_S3=https://your-s3-endpoint \
-e AWS_ACCESS_KEY_ID=… \
-e AWS_SECRET_ACCESS_KEY=… \
-e AWS_REGION=auto \
atmoquest:latest
The image bundles Litestream as its supervisor process: it watches /data/atmoquest.db's WAL and streams changes to S3-compatible object storage. Replica config lives in litestream.yml and reads bucket + credentials from the BUCKET_NAME / AWS_* env vars above. Litestream is required — without those vars set the container won't start. Mount a volume at /data so the database and the OAuth signing key (/data/oauth_key.pem) survive restarts.
Contributing#
PRs and feature requests welcome.