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.
When 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
rev
text
TID 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).
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.