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

Configure Feed

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

2 1 0

Clone this repository

https://git.vm.fail/nandi.uk/gleamview https://git.vm.fail/did:plc:zhyiljm7ox4aqj5mknxtjcng
ssh://git@knot1.tangled.sh:2222/nandi.uk/gleamview ssh://git@knot1.tangled.sh:2222/did:plc:zhyiljm7ox4aqj5mknxtjcng

For self-hosted knots, clone URLs may differ based on your setup.


README.md

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.post records
  • 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