GleamView#
A lexicon-driven ATProto AppView written in Gleam — a port of HappyView.
Upload lexicon schemas and get XRPC endpoints, record storage, Jetstream indexing, and an admin API without hand-wiring AppView plumbing.
Status: MVP core. HappyView is ~50k+ lines of Rust (Lua scripts, OAuth/DPoP, WASM plugins, spaces, full dashboard). GleamView implements the schema-first path that makes HappyView useful on day one.
What works#
| Area | Status |
|---|---|
SQLite storage (happyview_* tables) |
✅ |
| Lexicon upload / registry (runtime) | ✅ |
XRPC queries (list + get by uri) |
✅ |
| XRPC procedures (local index mode) | ✅ (dev) |
API client keys (X-Client-Key, optional) |
✅ |
| Admin API (lexicons, records, clients, stats) | ✅ |
| Jetstream consumer | ✅ (basic) |
| DID/PDS resolve helpers | ✅ |
| OAuth + DPoP PDS write proxy | ✅ |
| Lua / script hooks | ⏳ planned |
| WASM plugins, spaces, backfill jobs | ⏳ planned |
| Next.js admin dashboard | ⏳ planned |
Web console#
Open http://127.0.0.1:3000/ after gleam run for the built-in UI:
- Feed — list indexed
com.example.postrecords - Compose — create posts (local index or PDS via OAuth)
- Sign in — ATProto OAuth (DPoP)
- Lexicons / Admin — registry, stats, API clients (needs admin key)
- Settings — admin key (optional client key for third-party apps)
Static assets live in priv/public/ and are served at /static/*.
Quick start#
Requires Gleam and Erlang/OTP on PATH.
cd gleamview
gleam deps download
gleam test
gleam run
Server listens on http://127.0.0.1:3000 by default. On first start it prints a bootstrap admin API key (Bearer token for /admin/*).
# optional
cp .env.example .env
# DATABASE_URL=sqlite:gleamview.db
# JETSTREAM_ENABLED=0 # disable live network sync while developing
Walkthrough#
1. Create an API client#
ADMIN=gva_… # bootstrap key from startup logs
curl -s -X POST http://127.0.0.1:3000/admin/api-clients \
-H "Authorization: Bearer $ADMIN" \
-H "Content-Type: application/json" \
-d '{"name":"demo"}'
# → { "clientKey": "hvc_…", "clientSecret": "hvs_…", … }
2. Upload a record lexicon + query#
# Record type
curl -s -X POST 'http://127.0.0.1:3000/admin/lexicons' \
-H "Authorization: Bearer $ADMIN" \
-H "Content-Type: application/json" \
-d '{
"lexicon": 1,
"id": "com.example.post",
"defs": {
"main": {
"type": "record",
"key": "tid",
"record": {
"type": "object",
"required": ["text"],
"properties": { "text": { "type": "string" } }
}
}
}
}'
# Query that lists that collection
curl -s -X POST 'http://127.0.0.1:3000/admin/lexicons?targetCollection=com.example.post' \
-H "Authorization: Bearer $ADMIN" \
-H "Content-Type: application/json" \
-d '{
"lexicon": 1,
"id": "com.example.getPosts",
"defs": {
"main": {
"type": "query",
"parameters": {
"type": "params",
"properties": {
"limit": { "type": "integer" },
"cursor": { "type": "string" },
"did": { "type": "string" },
"uri": { "type": "string" }
}
},
"output": { "encoding": "application/json" }
}
}
}'
3. Index a record (local write mode) and query it#
CLIENT=hvc_…
# Procedure lexicon (create)
curl -s -X POST 'http://127.0.0.1:3000/admin/lexicons?targetCollection=com.example.post&action=create' \
-H "Authorization: Bearer $ADMIN" \
-H "Content-Type: application/json" \
-d '{
"lexicon": 1,
"id": "com.example.createPost",
"defs": {
"main": {
"type": "procedure",
"input": { "encoding": "application/json" },
"output": { "encoding": "application/json" }
}
}
}'
curl -s -X POST http://127.0.0.1:3000/xrpc/com.example.createPost \
-H "X-Client-Key: $CLIENT" \
-H "X-Gleamview-Did: did:plc:alice" \
-H "Content-Type: application/json" \
-d '{"text":"hello gleamview"}'
curl -s 'http://127.0.0.1:3000/xrpc/com.example.getPosts?limit=10' \
-H "X-Client-Key: $CLIENT"
ALLOW_LOCAL_WRITES=1 (default) indexes procedures into SQLite without proxying to a PDS. With ALLOW_LOCAL_WRITES=0, procedures proxy create/put/delete to the user's PDS via OAuth+DPoP (see OAuth section).
OAuth (PDS writes)#
Browser login (public loopback client metadata via redirect URI) stores a DPoP-bound session, then procedures with ALLOW_LOCAL_WRITES=0 proxy to the user's PDS:
# 1. Login (opens authorization server)
open "http://127.0.0.1:3000/oauth/login?handle=you.bsky.social"
# callback: GET /oauth/callback → session on disk
# 2. Check session
curl -s 'http://127.0.0.1:3000/oauth/session?handle=you.bsky.social'
# 3. Call a procedure (PDS createRecord + local index)
# set ALLOW_LOCAL_WRITES=0 and use the DID from the session
curl -s -X POST http://127.0.0.1:3000/xrpc/com.example.createPost \
-H "X-Client-Key: $CLIENT" \
-H "X-Gleamview-Did: did:plc:…" \
-H "Content-Type: application/json" \
-d '{"text":"from pds"}'
Ported from gitproxy OAuth (PKCE + PAR + DPoP ES256).
Architecture#
Mirrors HappyView's core flow:
Client ──GET /xrpc/{nsid}──► Query handler ──► SQLite records
Client ──POST /xrpc/{nsid}─► Procedure ──────► (local index | PDS proxy*)
Jetstream WS ──────────────► Record handler ─► SQLite records
Admin API ─────────────────► Lexicon registry (runtime)
| Module | Role |
|---|---|
gleamview/lexicon |
Parse + in-memory registry + DB persistence |
gleamview/xrpc |
Catch-all XRPC router |
gleamview/xrpc/query |
Default list / get |
gleamview/xrpc/procedure |
Default create / update / delete (local) |
gleamview/record_handler |
Jetstream / write indexing |
gleamview/jetstream |
WebSocket consumer (stratus) |
gleamview/admin |
Admin REST surface |
gleamview/auth |
API clients + admin keys |
gleamview/db |
SQLite actor |
gleamview/resolve |
PLC / did:web helpers |
gleamview/oauth |
ATProto OAuth (PKCE + PAR + DPoP) |
gleamview/pds_write |
Proxy create/put/delete to PDS |
Config#
See .env.example. HappyView-compatible names where practical: DATABASE_URL, PUBLIC_URL, JETSTREAM_URL, PLC_URL, PORT, …
Source#
Port of gamesgamesgamesgames.games/happyview · docs at happyview.dev.
License#
MIT