crate #
A Discogs-like, atproto-native record collection app, written in Gleam. Your
crate (the vinyl kind) is stored as records (the atproto kind) in your own
PDS, under the dev.mokkenstorm.crate.* lexicons.
Live in beta at crate.mokkenstorm.dev.
What it does today #
- atproto OAuth login (PAR + PKCE + DPoP); tokens live server-side, encrypted at rest.
- Your crate is an append-only event log, folded into current crate, wishlist, and history.
- Discogs seed search, account connection, and collection/wantlist import.
- Barcode scan-to-import in any browser: the native
BarcodeDetectorwhere it exists, a bundled ZXing WASM ponyfill elsewhere; photo capture mode and a did-you-mean review flow for misses. - Cover art stored as blobs in your repo, not hotlinked.
- A shared, browsable catalog: one-tap adoption with
via @handleattribution, public crate pages with taste overlap, crate-native follows, and a friends feed grouped by activity. - A collaborative edit loop: amending your own pressing supersedes it, amending a foreign one files an edit proposal into the owner's inbox.
- Catalog entities (artists, genres) minted from Discogs data, with MusicBrainz ids as the cross-app bridge.
Layout #
Gleam multi-target monorepo. server/web depend on shared by path; the
generic atproto plumbing comes from Hex
(gleam-atproto:
atproto_client +
atproto_sdl +
atproto_codegen).
| Package | Target | What it is |
|---|---|---|
shared |
erlang+js | Generated lexicon codecs (gen/) + StoredItem. |
server |
erlang | Wisp BFF: routes, OAuth, sessions, Discogs client. |
web |
javascript | Lustre SPA, talks to the BFF (+ the public Bluesky AppView directly for handle search). |
Lexicons are authored in Lexicon SDL (lexicons/**/*.sdl); make gen
converts them to lexicon JSON and generates codecs plus a typed XRPC client
into shared/src/crate/gen/, which is generated and gitignored. Run
make gen (or make test, which depends on it) before building a fresh
checkout.
Run it locally #
Needs gleam, erlang, node, bun, docker, make.
make dev # watch loop: Postgres in docker, rebuilds server/web on change
Browse http://127.0.0.1:8080, not localhost (cookies are host-specific
and OAuth callbacks land on the loopback IP).
Config comes from the environment (scripts/dev.sh / docker-compose provide
dev defaults, read .env for secrets):
| Variable | Required | Purpose |
|---|---|---|
SECRET_KEY_BASE |
yes | cookie signing secret |
STORE_KEY |
yes | at-rest encryption key (base64, 32 bytes) |
DATABASE_URL |
no | Postgres (in-memory fallback) |
DISCOGS_CONSUMER_KEY / DISCOGS_CONSUMER_SECRET |
no | Discogs search + import |
BASE_URL, OAUTH_CLIENT_JWK |
deploys | switches OAuth to the confidential client |
PORT, SLINGSHOT_URL |
no | port/identity-resolver overrides |
Confirm your data lives on your PDS:
curl "https://<your-pds>/xrpc/com.atproto.repo.listRecords?repo=<your-did>&collection=dev.mokkenstorm.crate.shelf.entry"
With Docker #
make docker-build # or: docker compose build
make up # server + Postgres on http://localhost:8080
make down
Tests #
make test # codegen, then shared + server + web suites
Real OAuth and cross-account flows are covered by a separate e2e suite against a pinned local atproto devnet:
make e2e-setup # once
make e2e