🪩 Podium#
A search engine for open data published on the Atmosphere: Matadisco dataset records, Astrosky/Nebra transient astronomy events, and whatever else gets added over time.
Live at https://podium.mycopunk.it.
How it works#
podium.mycopunk.it
│
caddy-front ──────────── reverse_proxy http://podium:80 (supramundane network)
│
┌──────┴─────────────────────────────────────────────┐
│ podium (caddy) │
│ / → the frontend │
│ /xrpc/* → rewritten to /hv/xrpc/* │
│ /hv/* → podium-hv │
├────────────────────────────────────────────────────┤
│ podium-hv (HappyView) │
│ indexes cx.vmx.matadisco + eco.astrosky.transient │
│ .gcn from Jetstream + backfill, SQLite on /data, │
│ serves XRPC, Lua query scripts, admin dashboard │
└────────────────────────────────────────────────────┘
- Backend is HappyView: a lexicon-driven AppView.
The collections to index, Podium's own query lexicons
(
it.mycopunk.podium.*), and the Lua scripts that implement them all live in this repo (backend/, wired up bypodium.config.json) and are pushed to the instance through its admin API — no pasting into dashboards. - Frontend is Lit 3 + refrakt.
- Deployment is a Supramundane service:
service.json+ a hand-authored multi-containercompose.yamlon thesupramundanenetwork. HappyView's SQLite lives in${SM_DATA_ROOT}/podium, so it's backed up in prod.
XRPC endpoints#
| Query | What it does |
|---|---|
it.mycopunk.podium.search?q=&collection=&limit= |
full-text search across collections (per-collection field lists in backend/scripts/search.lua) |
it.mycopunk.podium.listRecords?collection=&limit=&cursor= |
freshest records, merged or per collection |
it.mycopunk.podium.getRecord?uri= |
one full record by AT URI (GCN records keep their raw data here; list/search replace it with a summary) |
it.mycopunk.podium.stats |
per-collection record counts |
All available on both /xrpc/… and /hv/xrpc/….
The podium CLI#
npm link once (or call node bin/podium.js). Hits prod by
default; add --local for the local stack.
podium deploy [--local] [--no-push] # env file → .env, sm deploy, then push
podium push [--local] [--watch] # sync domains, lexicons + Lua scripts to HappyView
podium backfill [collection] [--local] # (re)start historical backfill
podium bootstrap <handle> [--local] # first-boot: seed super user + API key (prod goes over SSH)
podium status [--local] # what the instance currently has
push --watch re-pushes on every save under backend/ — that's the dev loop
for Lua/lexicon work. Frontend dev is npm run dev (Vite on :5173, proxying
XRPC to the local HappyView on 127.0.0.1:3300).
First boot#
Local (needs Docker, the supramundane network from a running
caddy-front, and .bast DNS):
cp .env.example .env.local # fill in PODIUM_SESSION_SECRET, check SM_DATA_ROOT
podium deploy --local # brings the stack up (no push yet — no key)
podium bootstrap robin.berjon.com --local # seeds super user + API key into .env.local
podium push --local # domains, lexicons, scripts, kicks off backfill
The atproto OAuth dance can't run against a .bast domain (the PDS must fetch
the client metadata), which is why local uses bootstrap (a direct SQLite
seed, done with the container stopped) and PODIUM_PUBLIC_URL points at the
loopback port. The dashboard is at http://127.0.0.1:3300/hv/dashboard/, and
logging in there with the bootstrapped handle works because the seeded user
is that identity.
Prod (needs $SUPRAMUNDANE for the SSH steps):
cp .env.example .env.prod # prod URLs, fresh PODIUM_SESSION_SECRET, no key yet
podium deploy --no-push # rsyncs + builds on $SUPRAMUNDANE, wires the front
podium bootstrap robin.berjon.com # seeds super user + API key on the server, over SSH
podium push # domains, lexicons, scripts, kicks off backfill
podium bootstrap is the no-browser path; dashboard OAuth login at
https://podium.mycopunk.it/hv also works from first boot (first login
becomes super user) provided PODIUM_PUBLIC_URL is the bare origin — see
"Careful" below.
Careful#
PUBLIC_URLmust be the bare origin — never include/hv. HappyView appendsBASE_PATHitself (effective_public_url()), so a path-ful value yields/hv/hv/OAuth metadata URLs (login fails withinvalid_client_metadata) and a primary domain whose host key contains the path, which matches no Host header → everything domain-gated 421s.- HappyView 421s any Host it doesn't know. Its primary domain comes from
PUBLIC_URL; every other public origin (e.g.https://podium.bast) must be registered, whichpodium pushdoes fromPODIUM_DOMAINS. Domain URLs are UNIQUE in its DB and the boot-time primary sync panics on conflict, so never register an extra domain equal to what the primary will sync to (push skips ones that already exist, which covers this). - Don't touch the SQLite file while the container runs. It's on a Docker
bind mount; host-side writes against the live WAL corrupt it (this is why
bootstrapstops the container first). If it happens anyway: stop the stack, deletehappyview.db*, start, re-bootstrap, re-push. - Re-POSTing a network lexicon is not idempotent — it re-fetches and
re-kicks sync machinery.
podium pushtherefore only creates missing ones, and only auto-backfills on creation. - The records in the wild don't all match the published lexicons.
Matadisco has (at least) three shapes:
resource/publishedAt(/tags), andmetadata/createdwith a text or imagepreview. Search fields and the result cards handle all of them; keep that in mind when adding sources. - GCN
datapayloads can be tens of KB, so list/search responses strip them server-side (slim()in the Lua scripts) and keep a humansummaryextracted from the event JSON. .envis a deploy-time artifact.podium deploywrites it from.env.prodor.env.local, and restores the local flavor after a prod deploy (the prod copy lives on in the server's service dir). If localdocker composeever fails on a/srv/...mount,.envhas prod values —cp .env.local .env.- A dashboard login poisons same-origin XRPC. HappyView allows anonymous
XRPC but hard-rejects authenticated calls without a client key — so once
you've logged into the dashboard, your session cookie would break the
frontend's fetches ("Missing client identification"). The frontend fetches
with
credentials: 'omit'to stay anonymous; keep that if you touchapi.ts.
Adding a data source#
- Add its NSID to
networkLexiconsinpodium.config.json(or a local lexicon file underbackend/lexicons/if it isn't resolvable on-network). - Add a source entry (collection + searchable fields) to
backend/scripts/search.lua, and to theCOLLECTIONSlists inlistRecords.lua/stats.lua. - Teach
frontend/src/components/podium-result-card.tshow to render it (falls back to a generic card otherwise). podium push --local, check,podium push.