gotta store my records
Gleam 87%
CSS 5%
Shell 3%
3%
JavaScript <1%
TypeScript <1%
Makefile <1%
HCL <1%
HTML <1%
Dockerfile <1%

README.md

at-record #

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 BarcodeDetector where 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 @handle attribution, 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/at_record/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