Lexicon-driven ATProto AppView in Gleam — port of HappyView
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