A lexicon-driven AppView for ATProto.
happyview docs reference architecture.md
14 kB

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[("SQLite / 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 the database (SQLite by default, or 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, build OAuth client, spawn Tap worker, start server
  lib.rs            AppState struct (incl. OAuth client + cookie key), module declarations
  config.rs         Environment variable loading
  dns.rs            DNS TXT resolver for atrium handle resolution
  error.rs          AppError enum (Auth, BadRequest, Forbidden, Internal, NotFound, PdsError)
  server.rs         Axum router: fixed routes + admin nest + auth routes + 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
  resolve.rs        NSID authority resolution (DNS TXT → DID → PDS)
  auth/
    mod.rs          Re-exports, COOKIE_NAME constant
    middleware.rs   Claims extractor (cookie auth, API key, or service auth JWT)
    routes.rs       OAuth endpoints (/auth/login, /auth/callback, /auth/logout, /auth/me)
    oauth_store.rs  Database-backed session and state stores for atrium-oauth
    service_auth.rs XRPC service-to-service JWT validation (ES256/ES256K)
  admin/
    mod.rs          Admin route definitions
    auth.rs         UserAuth extractor (Claims + DID lookup + permission check + auto-bootstrap)
    users.rs        User CRUD handlers (create, list, get, delete, update permissions, transfer super)
    permissions.rs  Permission enum (20 permissions), templates (Viewer, Operator, Manager, FullAccess)
    api_keys.rs     API key CRUD handlers (create, list, revoke) with scoped permissions
    events.rs       Event log query handler
    settings.rs     Instance settings CRUD handlers (list, upsert, delete, logo upload/serve)
    script_variables.rs  Script variable CRUD handlers (list, upsert, delete)
    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
    pds.rs          PDS proxy helpers (JSON POST, blob POST, response forwarding via OAuth session)
    session.rs      OAuth session restoration from atrium store
    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} + session cookie or Bearer token
  -> Claims extractor (cookie, API key, or service auth JWT)
  -> 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)
  -> Restore OAuth session from atrium store (by DID)
  -> atrium handles DPoP proof generation and token refresh
  -> Proxy to user's PDS (createRecord or putRecord)
  -> Upsert record locally
  -> Forward PDS response

Admin endpoints #

Client request + session cookie or Bearer token
  -> AdminAuth extractor:
     1. Claims validation (cookie, API key, or service auth JWT)
     2. DID lookup in users table (auto-bootstrap super user if empty)
     3. Permission check (403 if missing required permission)
  -> 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

users #

Column Type Description
id uuid (PK)
did text (unique) User's AT Protocol DID
is_super boolean Whether this is the super user (only one allowed)
created_at timestamptz
last_used_at timestamptz Updated on each authenticated request

user_permissions #

Column Type Description
user_id uuid (FK) References users.id
permission text Permission string (e.g. lexicons:create)
(PK) Composite primary key: (user_id, permission)

api_keys #

Column Type Description
id uuid (PK)
user_id uuid (FK) References users.id
name text Descriptive label
key_hash text SHA-256 hash of the full key
key_prefix text First 11 characters for display
permissions text[] Permissions granted to this key
created_at timestamptz
last_used_at timestamptz
revoked_at timestamptz Set when revoked (soft delete)

oauth_sessions #

Column Type Description
did text (PK) User's AT Protocol DID
session_data text Serialized OAuth session (managed by atrium)
created_at timestamptz
updated_at timestamptz

oauth_state #

Column Type Description
state_key text (PK) OAuth state parameter
state_data text Serialized state (managed by atrium)
created_at timestamptz

instance_settings #

Column Type Description
key text (PK) Setting name (e.g. app_name)
value text Setting value
updated_at timestamptz Last modified

event_logs #

Column Type Description
id uuid (PK)
event_type text Category.action format (e.g. user.created)
severity text info, warn, or error
actor_did text DID of the user who triggered the event
subject text What was affected (DID, NSID, URI, etc.)
detail jsonb Event-specific data
created_at timestamptz

script_variables #

Column Type Description
key text (PK) Variable name
value text Variable value (encrypted at rest)
created_at timestamptz
updated_at timestamptz

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 (SQLite by default)
cargo test

# Or run against 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 (PLC directory, PDSes) and a real database for full integration coverage. By default tests use SQLite; set TEST_DATABASE_URL to a Postgres connection string to test against Postgres.