Something went wrong. Try again.
A lexicon-driven AppView for ATProto.
Something went wrong. Try again.
Architecture #
Guide for contributors working on HappyView itself. For a user-facing overview, see the Introduction.
System overview #
graph LR
Application
Application -->|"GET /xrpc/{method}"| Query
Application -->|"POST /xrpc/{method}"| Procedure
subgraph HappyView
Query["Query Handler<br/><small>Lua Script (Optional)</small>"]
Procedure["Procedure Handler<br/><small>Lua Script (Optional)</small>"]
end
Procedure --> DB
Query --> DB
Procedure -->|proxy write| PDS["User PDS"]
DB[("PostgreSQL<br/><small>records · lexicons</small>")]
Tap["Tap<br/><small>WebSocket</small>"] -->|record events| DB
Relay["Relay<br/><small>Firehose</small>"] --> Tap
Reads flow top-down through the query handler to Postgres. Writes flow through the procedure handler to the user's PDS, then HappyView indexes the record locally. All record data enters the system through Tap, which handles both real-time firehose events and historical backfill. HappyView syncs collection filters to Tap and discovers repos via the relay for backfill, but Tap performs all record fetching.
Module overview #
src/
main.rs Startup: config, DB, migrations, spawn Tap worker, start server
lib.rs AppState struct, module declarations
config.rs Environment variable loading
error.rs AppError enum (Auth, BadRequest, Forbidden, Internal, NotFound, PdsError)
server.rs Axum router: fixed routes + admin nest + XRPC catch-all + static files
lexicon.rs ParsedLexicon, LexiconRegistry (Arc<RwLock<HashMap>>)
profile.rs DID document resolution, PDS discovery, profile fetching
tap.rs Tap WebSocket listener, collection filter sync, backfill delegation
aip.rs AIP reverse proxy
resolve.rs NSID authority resolution (DNS TXT → DID → PDS)
auth/
mod.rs Re-exports
middleware.rs Claims extractor (validates Bearer token via AIP /oauth/userinfo)
jwks.rs JWKS key fetching
admin/
mod.rs Admin route definitions
auth.rs AdminAuth extractor (Claims + DID lookup + auto-bootstrap)
admins.rs Admin CRUD handlers
lexicons.rs Lexicon CRUD handlers
network_lexicons.rs Network lexicon tracking (add, list, remove)
records.rs Record listing handler
stats.rs Record count stats
backfill.rs Backfill job creation (relay discovery + Tap delegation)
types.rs Request/response structs for admin endpoints
lua/
mod.rs Re-exports
context.rs Lua context globals (method, params, input, caller_did, collection)
db_api.rs Lua database API (db.query, db.get, db.count)
execute.rs Script execution and sandbox setup
record.rs Lua Record API (constructor, save, delete, load)
sandbox.rs Restricted Lua environment (removed modules, instruction limit)
tid.rs TID generation for Lua scripts
repo/
mod.rs Re-exports
dpop.rs DPoP JWT proof generation (ES256/P-256)
pds.rs PDS proxy helpers (JSON POST, blob POST, response forwarding)
session.rs ATP session fetching from AIP
upload_blob.rs Blob upload handler
xrpc/
mod.rs Re-exports
query.rs Dynamic GET handler (Lua script or default: single record + list)
procedure.rs Dynamic POST handler (Lua script or default: create vs put)
Request flow #
Reads (queries) #
Client GET /xrpc/{method}?params
-> xrpc::xrpc_get()
-> LexiconRegistry lookup (must be Query type)
-> If Lua script attached: execute script (has access to db API)
-> Else: default SQL query on records table (collection from target_collection)
-> JSON response
Writes (procedures) #
Client POST /xrpc/{method} + Bearer token
-> Claims extractor validates token via AIP /oauth/userinfo
-> xrpc::xrpc_post()
-> LexiconRegistry lookup (must be Procedure type)
-> If Lua script attached: execute script (has access to Record API)
-> Else: default create/update (auto-detect based on uri field)
-> Fetch ATP session from AIP /api/atprotocol/session
-> Generate DPoP proof (ES256)
-> Proxy to user's PDS (createRecord or putRecord)
-> Upsert record locally
-> Forward PDS response
Admin endpoints #
Client request + Bearer token
-> AdminAuth extractor:
1. Claims validation via AIP
2. DID lookup in admins table (auto-bootstrap if empty)
3. 403 if not admin
-> Admin handler
-> JSON response
Data flow #
Real-time indexing #
Tap WebSocket connection (tap::spawn)
-> Collection filters synced to Tap on startup and lexicon changes
-> Record events:
create/update -> UPSERT into records table
delete -> DELETE from records table
-> Lexicon schema events (com.atproto.lexicon.schema):
-> Update tracked network lexicons in DB and registry
-> Reconnects automatically on errors or collection filter changes
Backfill #
POST /admin/backfill
-> Create backfill_jobs record (status = running)
-> Relay listReposByCollection -> list of DIDs
-> Send DIDs to Tap in batches of 1000 (POST /repos/add)
-> Mark job as completed
-> Tap fetches records asynchronously and delivers via WebSocket
Database schema #
records #
| Column | Type | Description |
|---|---|---|
uri |
text (PK) | AT URI (at://did/collection/rkey) |
did |
text | Author DID |
collection |
text | Lexicon NSID |
rkey |
text | Record key |
record |
jsonb | Record value |
cid |
text | Content identifier |
indexed_at |
timestamptz | When HappyView indexed this record |
lexicons #
| Column | Type | Description |
|---|---|---|
id |
text (PK) | Lexicon NSID |
revision |
integer | Incremented on upsert |
lexicon_json |
jsonb | Raw lexicon definition |
lexicon_type |
text | record, query, procedure, definitions |
backfill |
boolean | Whether to backfill on upload |
target_collection |
text | For queries/procedures: which record collection |
created_at |
timestamptz | |
updated_at |
timestamptz |
admins #
| Column | Type | Description |
|---|---|---|
id |
uuid (PK) | |
did |
text (unique) | Admin's AT Protocol DID |
created_at |
timestamptz | |
last_used_at |
timestamptz | Updated on each authenticated request |
backfill_jobs #
| Column | Type | Description |
|---|---|---|
id |
uuid (PK) | |
collection |
text | Target collection (null = all) |
did |
text | Target DID (null = all) |
status |
text | pending, running, completed, failed |
total_repos |
integer | |
processed_repos |
integer | |
total_records |
integer | |
error |
text | Error message if failed |
started_at |
timestamptz | |
completed_at |
timestamptz | |
created_at |
timestamptz |
Testing #
# Unit tests (no database needed)
cargo test --lib
# All tests including end-to-end (requires Postgres)
docker compose -f docker-compose.test.yml up -d
TEST_DATABASE_URL=postgres://happyview:happyview@localhost:5433/happyview_test cargo test
docker compose -f docker-compose.test.yml down
End-to-end tests use wiremock to mock external services (AIP, PLC directory, PDSes) and a real Postgres database for full integration coverage.