Lexicon-driven ATProto AppView in Gleam — port of HappyView
0

Configure Feed

Select the types of activity you want to include in your feed.

gleamview / README.md
6.9 kB 213 lines
1# GleamView 2 3A **lexicon-driven ATProto AppView** written in [Gleam](https://gleam.run) — a port of [HappyView](https://tangled.org/gamesgamesgamesgames.games/happyview). 4 5Upload lexicon schemas and get XRPC endpoints, record storage, Jetstream indexing, and an admin API without hand-wiring AppView plumbing. 6 7> **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. 8 9## What works 10 11| Area | Status | 12|------|--------| 13| SQLite storage (`happyview_*` tables) | ✅ | 14| Lexicon upload / registry (runtime) | ✅ | 15| XRPC queries (list + get by `uri`) | ✅ | 16| XRPC procedures (local index mode) | ✅ (dev) | 17| API client keys (`X-Client-Key`, optional) | ✅ | 18| Admin API (lexicons, records, clients, stats) | ✅ | 19| Jetstream consumer | ✅ (basic) | 20| DID/PDS resolve helpers | ✅ | 21| OAuth + DPoP PDS write proxy | ✅ | 22| Lua / script hooks | ⏳ planned | 23| WASM plugins, spaces, backfill jobs | ⏳ planned | 24| Next.js admin dashboard | ⏳ planned | 25 26## Web console 27 28Open `http://127.0.0.1:3000/` after `gleam run` for the built-in UI: 29 30- **Feed** — list indexed `com.example.post` records 31- **Compose** — create posts (local index or PDS via OAuth) 32- **Sign in** — ATProto OAuth (DPoP) 33- **Lexicons / Admin** — registry, stats, API clients (needs admin key) 34- **Settings** — admin key (optional client key for third-party apps) 35 36Static assets live in `priv/public/` and are served at `/static/*`. 37 38## Quick start 39 40Requires [Gleam](https://gleam.run/getting-started/) and Erlang/OTP on `PATH`. 41 42```bash 43cd gleamview 44gleam deps download 45gleam test 46gleam run 47``` 48 49Server listens on `http://127.0.0.1:3000` by default. On first start it prints a **bootstrap admin API key** (Bearer token for `/admin/*`). 50 51```bash 52# optional 53cp .env.example .env 54# DATABASE_URL=sqlite:gleamview.db 55# JETSTREAM_ENABLED=0 # disable live network sync while developing 56``` 57 58## Walkthrough 59 60### 1. Create an API client 61 62```bash 63ADMIN=gva_… # bootstrap key from startup logs 64 65curl -s -X POST http://127.0.0.1:3000/admin/api-clients \ 66 -H "Authorization: Bearer $ADMIN" \ 67 -H "Content-Type: application/json" \ 68 -d '{"name":"demo"}' 69# → { "clientKey": "hvc_…", "clientSecret": "hvs_…", … } 70``` 71 72### 2. Upload a record lexicon + query 73 74```bash 75# Record type 76curl -s -X POST 'http://127.0.0.1:3000/admin/lexicons' \ 77 -H "Authorization: Bearer $ADMIN" \ 78 -H "Content-Type: application/json" \ 79 -d '{ 80 "lexicon": 1, 81 "id": "com.example.post", 82 "defs": { 83 "main": { 84 "type": "record", 85 "key": "tid", 86 "record": { 87 "type": "object", 88 "required": ["text"], 89 "properties": { "text": { "type": "string" } } 90 } 91 } 92 } 93 }' 94 95# Query that lists that collection 96curl -s -X POST 'http://127.0.0.1:3000/admin/lexicons?targetCollection=com.example.post' \ 97 -H "Authorization: Bearer $ADMIN" \ 98 -H "Content-Type: application/json" \ 99 -d '{ 100 "lexicon": 1, 101 "id": "com.example.getPosts", 102 "defs": { 103 "main": { 104 "type": "query", 105 "parameters": { 106 "type": "params", 107 "properties": { 108 "limit": { "type": "integer" }, 109 "cursor": { "type": "string" }, 110 "did": { "type": "string" }, 111 "uri": { "type": "string" } 112 } 113 }, 114 "output": { "encoding": "application/json" } 115 } 116 } 117 }' 118``` 119 120### 3. Index a record (local write mode) and query it 121 122```bash 123CLIENT=hvc_… 124 125# Procedure lexicon (create) 126curl -s -X POST 'http://127.0.0.1:3000/admin/lexicons?targetCollection=com.example.post&action=create' \ 127 -H "Authorization: Bearer $ADMIN" \ 128 -H "Content-Type: application/json" \ 129 -d '{ 130 "lexicon": 1, 131 "id": "com.example.createPost", 132 "defs": { 133 "main": { 134 "type": "procedure", 135 "input": { "encoding": "application/json" }, 136 "output": { "encoding": "application/json" } 137 } 138 } 139 }' 140 141curl -s -X POST http://127.0.0.1:3000/xrpc/com.example.createPost \ 142 -H "X-Client-Key: $CLIENT" \ 143 -H "X-Gleamview-Did: did:plc:alice" \ 144 -H "Content-Type: application/json" \ 145 -d '{"text":"hello gleamview"}' 146 147curl -s 'http://127.0.0.1:3000/xrpc/com.example.getPosts?limit=10' \ 148 -H "X-Client-Key: $CLIENT" 149``` 150 151`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). 152 153 154### OAuth (PDS writes) 155 156Browser 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: 157 158```bash 159# 1. Login (opens authorization server) 160open "http://127.0.0.1:3000/oauth/login?handle=you.bsky.social" 161# callback: GET /oauth/callback → session on disk 162 163# 2. Check session 164curl -s 'http://127.0.0.1:3000/oauth/session?handle=you.bsky.social' 165 166# 3. Call a procedure (PDS createRecord + local index) 167# set ALLOW_LOCAL_WRITES=0 and use the DID from the session 168curl -s -X POST http://127.0.0.1:3000/xrpc/com.example.createPost \ 169 -H "X-Client-Key: $CLIENT" \ 170 -H "X-Gleamview-Did: did:plc:…" \ 171 -H "Content-Type: application/json" \ 172 -d '{"text":"from pds"}' 173``` 174 175Ported from [gitproxy](../gitproxy) OAuth (PKCE + PAR + DPoP ES256). 176 177## Architecture 178 179Mirrors HappyView's core flow: 180 181```text 182Client ──GET /xrpc/{nsid}──► Query handler ──► SQLite records 183Client ──POST /xrpc/{nsid}─► Procedure ──────► (local index | PDS proxy*) 184Jetstream WS ──────────────► Record handler ─► SQLite records 185Admin API ─────────────────► Lexicon registry (runtime) 186``` 187 188| Module | Role | 189|--------|------| 190| `gleamview/lexicon` | Parse + in-memory registry + DB persistence | 191| `gleamview/xrpc` | Catch-all XRPC router | 192| `gleamview/xrpc/query` | Default list / get | 193| `gleamview/xrpc/procedure` | Default create / update / delete (local) | 194| `gleamview/record_handler` | Jetstream / write indexing | 195| `gleamview/jetstream` | WebSocket consumer (stratus) | 196| `gleamview/admin` | Admin REST surface | 197| `gleamview/auth` | API clients + admin keys | 198| `gleamview/db` | SQLite actor | 199| `gleamview/resolve` | PLC / did:web helpers | 200| `gleamview/oauth` | ATProto OAuth (PKCE + PAR + DPoP) | 201| `gleamview/pds_write` | Proxy create/put/delete to PDS | 202 203## Config 204 205See [`.env.example`](.env.example). HappyView-compatible names where practical: `DATABASE_URL`, `PUBLIC_URL`, `JETSTREAM_URL`, `PLC_URL`, `PORT`, … 206 207## Source 208 209Port of [gamesgamesgamesgames.games/happyview](https://tangled.org/gamesgamesgamesgames.games/happyview) · docs at [happyview.dev](https://happyview.dev). 210 211## License 212 213MIT