Skip to content

Architecture

Guide for contributors working on HappyView itself. For a user-facing overview, see the Introduction.

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>"]
    Spaces["Spaces<br/><small>Permissioned Data</small>"]
  end

  Procedure --> DB
  Query --> DB
  Spaces --> DB

  Procedure -->|proxy write| PDS["User PDS"]

  DB[("SQLite / PostgreSQL<br/><small>records · lexicons · spaces</small>")]

  Jetstream["Jetstream<br/><small>WebSocket</small>"] -->|record events| DB
  Relay["Relay<br/><small>listReposByCollection</small>"] -->|repo discovery| Backfill
  Backfill["Backfill Worker"] -->|listRecords| PDS
  Backfill --> DB

  Labeler["Labeler<br/><small>WebSocket (out-of-band)</small>"] -->|label events| DB

Queries go through the query handler to the database (SQLite by default, or Postgres). Writes go through the procedure handler to the user’s PDS, then HappyView indexes the record locally. Real-time record events stream in via Jetstream (its v2 subscribeEvents stream when the host offers it, otherwise v1), applied in order for each repo, with the stream position saved only once every earlier event has been applied; historical records are backfilled in-process, either by replaying the Jetstream archive or by discovering repos via the relay’s listReposByCollection and fetching records directly from each PDS. Labelers are external services that emit content labels over a direct WebSocket connection — they operate out-of-band, outside the relay/repo system. Spaces provide permissioned data containers with membership-gated access, per-user repo state tracking (LtHash + signed commits), and cross-service credential-based authentication.

sequenceDiagram
    participant C as Client
    participant X as xrpc_get()
    participant R as LexiconRegistry
    participant L as Lua Script
    participant D as Database

    C->>X: GET /xrpc/{method}?params
    X->>R: Lookup (must be Query type)
    alt Lua script attached
        R->>L: Execute script
        L->>D: happyview.db / happyview.sql
        D-->>L: Results
        L-->>X: Response table
    else No script
        R->>D: Default SQL query (collection from target_collection)
        D-->>X: Results
    end
    X-->>C: JSON response
sequenceDiagram
    participant C as Client
    participant A as Claims Extractor
    participant X as xrpc_post()
    participant R as LexiconRegistry
    participant L as Lua Script
    participant S as OAuth Session
    participant P as User PDS
    participant D as Database

    C->>A: POST /xrpc/{method} + DPoP auth + X-Client-Key
    A->>X: Validated claims
    X->>R: Lookup (must be Procedure type)
    alt Lua script attached
        R->>L: Execute script
        L->>S: happyview.record create/put
    else No script
        R->>S: Default create/update (auto-detect from uri field)
    end
    S->>P: Proxy write (createRecord or putRecord)
    P-->>S: PDS response
    S->>D: Upsert record locally
    S-->>C: Forward PDS response
sequenceDiagram
    participant C as Client
    participant A as AdminAuth Extractor
    participant U as Users Table
    participant H as Admin Handler
    participant D as Database

    C->>A: Request + Bearer token
    A->>A: Validate claims (API key or service auth JWT)
    A->>U: DID lookup
    alt Users table empty
        U-->>A: Auto-bootstrap as super user
    else User found
        U-->>A: Load permissions
    end
    A->>A: Permission check (403 if missing)
    A->>H: Authorized request
    H->>D: Database operation
    D-->>H: Result
    H-->>C: JSON response
sequenceDiagram
    participant J as Jetstream WebSocket
    participant H as HappyView
    participant D as Database
    participant R as LexiconRegistry

    H->>J: Connect (collection filters from indexed lexicons)
    loop Stream events
        J->>H: Record commit event
        alt create / update
            H->>D: UPSERT into records table
        else delete
            H->>D: DELETE from records table
        end
    end
    J->>H: Lexicon schema event (com.atproto.lexicon.schema)
    H->>D: Update tracked network lexicons
    H->>R: Update in-memory registry
    Note over H,D: Applied watermark persisted to instance_settings for resume on reconnect
    Note over H,J: Reconnects on collection filter changes (lexicon add/remove)

Ordering. Events are split across JETSTREAM_WORKERS workers by account DID, so each repo’s events apply in the order they arrived while different repos apply in parallel. Each worker runs one event at a time, record script included, so script latency bounds throughput: with slow scripts on Postgres, raise JETSTREAM_WORKERS. The saved cursor is the applied watermark: the position below which every event has been applied. After a restart or reconnect HappyView resumes from it, so delivery is at-least-once, and writes are idempotent (a replayed record carries the same rev and is ignored).

Accounts. An account event with active=false is handled by its status:

  • deleted, takendown and suspended purge the account’s records and leave an account tombstone (jetstream.account_deleted, jetstream.account_takendown). A relay can still be relaying a PDS that a deleted account has left. So before purging a deleted account it holds records for, HappyView asks the account’s current PDS (com.atproto.sync.getRepoStatus, 5 second timeout). If that PDS says the account is active, nothing is purged or tombstoned and jetstream.account_status_mismatch is logged. If the PDS cannot be asked, the purge goes ahead. takendown and suspended are purged without asking, because a relay may impose them and the account’s PDS cannot see that.
  • deactivated, desynchronized and throttled keep the records (jetstream.account_inactive).
  • active=true lifts the account’s tombstone. Records that were purged come back only through a new backfill.
  • A #sync removes the repo’s records in registered collections, and the current ones that follow replace them. identity events change nothing, because HappyView caches no identities.

Failures.

  • A database outage is retried indefinitely with growing delays, at most every 16 seconds by default, so ingest pauses rather than skipping events. While paused, the consumer cannot notice a collection change or a shutdown until the queue drains.
  • Any other failure is retried for about 31 seconds, then logged as jetstream.apply_failed and skipped.
  • Invalid input (no record body, an unknown operation, data the database rejects) is skipped at once with one jetstream.record_invalid warning.

Recovering a cursor.

  • A cursor older than Jetstream keeps (CursorTooOld) is replayed from the Jetstream archive when it has one and BACKFILL_SOURCE is auto or replay: see Gap replay under Backfill. Otherwise it resumes from the stored witnessed time. Jetstream clamps that to its oldest event and announces the gap with #info OutdatedCursor, which is logged as jetstream.gap with reason outdated_cursor. Only when there is no stored time does HappyView resume from the live tip, logged as cursor_too_old. A backfill recovers what was missed.
  • If the server renumbers its seqs (FutureCursor), HappyView resumes by witnessed time too.
  • An unexplained 400 first drops compression, then the seq cursor (only when a seq was sent and a witnessed time is saved; otherwise it goes straight to the hourly retry and keeps the cursor), then retries hourly and logs jetstream.invalid_request.
  • Under auto, a 404 falls back to v1 until the process restarts.

Edge cases of revs and tombstones.

  • The Lua record script can run for a write that a tombstone then refuses.
  • An account tombstone’s rev is a TID of the time Jetstream witnessed the event. A PDS whose clock differs from Jetstream’s can therefore let an older replay through, or block a newer write until the tombstone is swept (RECORD_TOMBSTONE_RETENTION_HOURS, 72 by default).
  • A stale record tombstone left after a newer recreate blocks writes without a rev (crawl writes) to that record until the sweep.
  • A crawl row fetched before a live update but written after it can overwrite the newer body while keeping the stored rev. A later replay at that rev heals it.
  • A delete with a rev removes a crawled row that has none.
  • A delete for a record HappyView does not hold leaves a tombstone only when the account holds records in that collection, or a running or pausing backfill covers it. Those are the cases where a create could still arrive. The network sends many deletes for records an instance never held, and tombstoning each would fill the table.

Admins can watch the consumer with GET /admin/jetstream/status and the About page’s Jetstream card. See Jetstream status.

sequenceDiagram
    participant A as Admin
    participant H as HappyView
    participant D as Database
    participant Relay as Relay
    participant PLC as PLC Directory
    participant PDS as User PDS

    A->>H: POST /admin/backfill
    H->>D: Create backfill_jobs record (status = running)

    rect rgb(240, 255, 240)
    note over H,PDS: Discovery, resolution and fetching run concurrently
    par Discovery (network backfills)
        loop Each collection, while the queue has room
            H->>Relay: listReposByCollection (one page)
            Relay-->>H: DIDs
            H->>D: Queue (did, collection) units, save the relay cursor
        end
    and Resolver task
        loop Unresolved units
            H->>PLC: Resolve DID document
            PLC-->>H: PDS endpoint
            H->>D: Record the unit's PDS
        end
    and Fetcher task
        loop Resolved units (as they arrive)
            H->>PDS: listRecords for the unit's collection (paginated)
            PDS-->>H: Records
            H->>D: UPSERT each page
            H->>D: Delete the completed unit, bump counters
        end
    end
    end
    Note over H,D: At most BACKFILL_DISCOVERY_WINDOW units are queued per job

    H->>D: Mark job completed (or failed)

Two sources. A backfill job reads its records from one of two places, chosen by BACKFILL_SOURCE (default crawl) or the job’s source. A crawl runs the pipeline above, and every account backfill crawls. A replay asks the Jetstream archive for a plan (planSnapshot), downloads the segments and blocks that hold the job’s collections, and writes the records through the same path as live events. A second sweep then applies the account and sync events for the accounts that hold records. Markers are applied as of their own time, so the two sweeps give the same result in either order and never remove records written after them. auto replays when the Jetstream has an archive and crawls when it doesn’t. A replay returns only what the archive holds; some repos’ history in Bluesky’s archive starts after their first record, which a crawl would find.

Gap replay. When the live consumer’s cursor is older than Jetstream keeps, HappyView creates a replay job for the gap between its last applied seq and the archive’s sealed tip, and resumes the live stream at that tip. The consumer waits at most 30 seconds for the archive’s plan; past that it resumes without a replay. Live events and the replay overlap safely, because the rev guard and tombstones make every write idempotent. Instances on BACKFILL_SOURCE=crawl (the default), or on a Jetstream without an archive, keep the old behaviour of skipping the gap.

ColumnTypeDescription
uritext (PK)AT URI (at://did/collection/rkey)
didtextAuthor DID
collectiontextLexicon NSID
rkeytextRecord key
recordjsonbRecord value
cidtextContent identifier
indexed_attimestamptzWhen HappyView indexed this version of the record. Redelivering an identical record (same CID and body) keeps the original value. A local write that Jetstream will echo back (an XRPC procedure, a happyview.record mirror, a linked-repo write) stores NULL, on insert and on update, until the echo stamps it. A plugin’s direct local-index write gets no echo, so an update through it keeps the existing value
revtextTID of the repo commit that wrote this version, from Jetstream. NULL for local writes, listRecords backfill rows and rows written before revs were stored. A delivery with an older rev than the stored one is ignored

Recently deleted records (uri, rev, deleted_at) and deleted accounts (did, rev, seq, deleted_at). A create or update at or before a tombstone’s rev, or one without a rev, is not written, so a late or replayed copy cannot bring a deleted record back. Tombstones are kept for RECORD_TOMBSTONE_RETENTION_HOURS (72 by default).

ColumnTypeDescription
idtext (PK)Lexicon NSID
revisionintegerIncremented on upsert
lexicon_jsonjsonbRaw lexicon definition
lexicon_typetextrecord, query, procedure, definitions
backfillbooleanWhether to backfill on upload
target_collectiontextFor queries/procedures: which record collection
created_attimestamptz
updated_attimestamptz
ColumnTypeDescription
iduuid (PK)
didtext (unique)User’s atproto DID
is_superbooleanWhether this operator holds superadmin
created_attimestamptz
last_used_attimestamptzUpdated on each authenticated request
ColumnTypeDescription
user_iduuid (FK)References users.id
permissiontextPermission string (e.g. lexicons:create)
(PK)Composite primary key: (user_id, permission)
ColumnTypeDescription
iduuid (PK)
user_iduuid (FK)References users.id
nametextDescriptive label
key_hashtextSHA-256 hash of the full key
key_prefixtextFirst 11 characters for display
permissionstext[]Permissions granted to this key
created_attimestamptz
last_used_attimestamptz
revoked_attimestamptzSet when revoked (soft delete)
ColumnTypeDescription
didtext (PK)User’s atproto DID
session_datatextSerialized OAuth session (managed by atrium)
created_attimestamptz
updated_attimestamptz
ColumnTypeDescription
state_keytext (PK)OAuth state parameter
state_datatextSerialized state (managed by atrium)
created_attimestamptz
ColumnTypeDescription
keytext (PK)Setting name (e.g. app_name)
valuetextSetting value
updated_attimestamptzLast modified
ColumnTypeDescription
iduuid (PK)
event_typetextCategory.action format (e.g. user.created)
severitytextinfo, warn, or error
actor_didtextDID of the user who triggered the event
subjecttextWhat was affected (DID, NSID, URI, etc.)
detailjsonbEvent-specific data
created_attimestamptz
ColumnTypeDescription
keytext (PK)Variable name
valuetextVariable value (encrypted at rest)
created_attimestamptz
updated_attimestamptz
ColumnTypeDescription
idtext (PK)Internal space identifier
didtextDID in the space URI; the authority’s DID
authority_didtextThe space authority: the instance DID, or the creator’s DID when the instance has no service identity
creator_didtextDID of the user who created and administers the space
type_nsidtextSpace type as an NSID
skeytextSpace key (differentiates spaces of the same type)
display_nametextHuman-readable name (optional)
descriptiontextDescription (optional)
read_policytext (JSON)Policy gating credential issuance ($type-tagged)
write_policytext (JSON)Policy gating which writers the space tracks ($type-tagged)
app_accesstext (JSON){"$type":"com.atproto.simplespace.defs#open"} or {"$type":"com.atproto.simplespace.defs#allowList","allowed":[...]}
configtext (JSON)Space config (membership_public, records_public, extras)
revisiontextCurrent revision TID
created_attext
updated_attext
ColumnTypeDescription
idtext (PK)
space_idtext (FK)References spaces.id
didtextMember’s DID (or space URI for delegation)
can_readbooleanWhether the member can read the space
can_writebooleanWhether the member can write to the space
read_selfbooleanWhether reads are limited to the member’s own records
is_delegationbooleanWhether this member is a delegated space
granted_bytextDID of who granted membership
created_attext
ColumnTypeDescription
uritext (PK)at:// URI of the record
space_idtext (FK)References spaces.id
author_didtextDID of the record author
collectiontextLexicon NSID
rkeytextRecord key
recordjsonbRecord value
cidtextContent identifier
indexed_attext
ColumnTypeDescription
idtext (PK)
space_idtext (FK)References spaces.id
author_didtextDID of the repo author
lthash_statebytea2048-byte LtHash state
revtextCurrent revision
hashbyteaContent hash
ikmbyteaInput keying material for deniable signatures
sigbyteaSignature
macbyteaMessage authentication code
updated_attext
ColumnTypeDescription
idtext (PK)
space_idtext (FK)References spaces.id
author_didtextDID of the operation author
revtextRevision this operation belongs to
idxintegerIndex within the revision
actiontextcreate, update, or delete
collectiontextLexicon NSID
rkeytextRecord key
cidtextContent identifier (for create/update)
prevtextPrevious CID (for update/delete)
created_attext
ColumnTypeDescription
idtext (PK)
space_idtext (FK)References spaces.id
servicetextSubscribing service; one registration per space
endpointtextResolved service endpoint, or webhook URL
deliverytextxrpc (notifyWrite calls) or webhook (legacy payload)
registered_bytextDID of who registered
expires_attextWhen the registration expires
created_attext
ColumnTypeDescription
space_idtext (FK)References spaces.id
repo_didtextDID of a repo the space tracks
revtextThe repo’s latest revision
hashblobThe repo’s latest LtHash digest
space_revtextSpace revision assigned to this update
updated_attext
ColumnTypeDescription
issuertextIssuer of an exchanged delegation token
jtitextThe token’s jti; a token is exchanged once
expires_attextWhen the token expires
ColumnTypeDescription
idtext (PK)
space_idtext (FK)References spaces.id
token_hashtextSHA-256 hash of the invite token
created_bytextDID of the user who created the invite
accesstextAccess level granted: read, read_self, write
max_usesinteger?Maximum number of uses (null = unlimited)
usesintegerCurrent use count
expires_attext?Expiry timestamp (null = never)
revokedbooleanWhether the invite has been revoked
created_attext
ColumnTypeDescription
idtext (PK)
space_idtext (FK)References spaces.id
issued_totextDID the credential was issued to
token_hashtextHash of the credential token
jtitextThe credential’s jti claim
expires_attextWhen the credential expires
revoked_attext?When the credential was revoked
created_attext
ColumnTypeDescription
idtext (PK)
didtextThe space’s DID
space_idtext (FK)References spaces.id
signing_key_enctextEncrypted signing key (AES-256-GCM)
rotation_key_enctextEncrypted rotation key (AES-256-GCM)
created_bytextDID of who provisioned the key
created_attext
ColumnTypeDescription
idinteger (PK)Always 1 (singleton)
modetextdid_web, did_plc, or linked_account
didtextThe service’s DID
signing_key_enctextEncrypted signing key
rotation_key_enctext?Encrypted rotation key (did:plc only)
attached_account_didtext?Linked account DID (linked_account mode)
setup_completebooleanWhether setup has been finalized
created_attext
updated_attext
ColumnTypeDescription
idinteger (PK)
fragment_idtextDID document fragment identifier
service_typetextService type (e.g. AtprotoAppView)
access_modetextall or scoped to specific XRPCs
created_attext
updated_attext
ColumnTypeDescription
service_entry_idinteger (FK)References service_entries.id
lexicon_idtextLexicon NSID this entry handles
ColumnTypeDescription
idtext (PK)
fragment_idtextDID document fragment (e.g. #atproto_space)
key_typetextAlways Multikey
public_key_multibasetextPublic key in multibase encoding
private_key_enctextEncrypted private key (AES-256-GCM)
created_attext
ColumnTypeDescription
iduuid (PK)
collectiontextTarget collection (null = all)
didtextTarget DID (null = all)
statustextpending, running, pausing, paused, cancelling, cancelled, completed, failed
stagetextpending, discovering_repos, resolving_and_fetching, completed, failed, cancelled
total_reposintegerWork units discovered (a unit is one repo under one collection; one per account for account-targeted jobs)
resolved_reposintegerUnits with PDS endpoint resolved
processed_reposintegerUnits with records fetched
total_recordsintegerTotal records indexed
errortextError message if failed
started_attimestamptz
completed_attimestamptz
created_attimestamptz
Terminal window
# 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.