HappyView v2.9
Trigger-keyed scripts, backfill concurrency, db.query filters, multi-device DPoP sessions, plus bugs fixes and performance improvements.
2.9 is a big one. Scripts got an overhaul, backfill got way faster, and there's a mountain of bug fixes and performance improvements across the board.
Trigger-keyed scripts
The biggest conceptual change in 2.9: scripts are no longer embedded with lexicons, and lexicons are no longer limited to a single script.
| Trigger | Fires when... |
|---|---|
record.index:<nsid> | Any record event (create, update, delete) — the wildcard fallback |
record.create:<nsid> | A record is created |
record.update:<nsid> | A record is updated |
record.delete:<nsid> | A record is deleted |
xrpc.query:<nsid> | An XRPC query is called |
xrpc.procedure:<nsid> | An XRPC procedure is called |
labeler.apply:<nsid> | A label arrives on a record of this type |
labeler.apply:_actor | A label arrives on a bare DID (actor-level) |
For record events, the dispatcher tries the action-specific trigger first (e.g. record.create:com.example.post), then falls back to the wildcard record.index:com.example.post. This means you can have one general-purpose script that handles everything, or surgical scripts for specific actions — or both.
Scripts are managed through the dashboard under Settings > Scripts, or via the new /admin/scripts API endpoints. The lexicon detail page also shows which scripts target each lexicon, with links to create or edit them.
If you're upgrading from v2.x to v2.9: existing index hooks and lexicon scripts will be migrated to the new system automatically.
Full docs: Record & Label Scripts, Lua Scripting, Admin API — Scripts.
Backfill, but concurrent
The biggest change is that PDS resolution and record fetching now run concurrently. Previously, HappyView resolved every DID's PDS endpoint before it started fetching any records. For large backfills with hundreds of thousands of DIDs, that meant the fetcher sat idle for potentially hours. Now fetching starts as soon as the first DIDs are resolved and runs alongside resolution for the rest of the job.
On top of that:
- Pause and resume — you can now manually pause a running backfill and pick it back up later. No lost progress.
- Concurrency settings — new settings in the dashboard let you tune PDS concurrency, DID concurrency per PDS, and PLC directory concurrency. HappyView will recommend a restart if the settings require a larger connection pool than the one currently running.
- Concurrent collection discovery — the repo discovery phase now runs multiple collection queries in parallel instead of sequentially.
- Batch record inserts — record inserts are now batched, significantly reducing database round trips during the fetch phase.
- Separate connection pool — backfill jobs now use their own database connection pool so they can't starve the main app of connections during heavy backfills.
The backfill details view got a significant overhaul: progress indicators are more detailed and more accessible.
db.query filters
You can now filter records directly in db.query without writing raw SQL or post-processing in Lua:
local result = db.query({
collection = "com.example.post",
filter = { field = "status", value = "published" },
})Filters support comparison operators (=, !=, >, <, >=, <=), AND/OR groups, and nesting up to 5 levels deep. Field paths use the same dot notation and array indices as sort (e.g. author.handle, scores[0]).
local result = db.query({
collection = "com.example.post",
filter = {
op = "AND",
conditions = {
{ field = "status", value = "published" },
{ field = "views", op = ">", value = 100 },
},
},
})Full docs are in the Database API reference.
Auth fixes
These were bugs that have been making my life hard, but I finally figured out what was causing them.
First, users were basically limited to one auth session per client. If you signed into Cartridge from a second device, it would kill your other auth session. Whoops.
Second, there were scenarios where the PDS may refresh the auth session while a HappyView XRPC was in-progress. If that happened, HappyView would handle it internally so any other requests in that XRPC worked, but it didn't return the refreshed tokens to the client. Follow up requests from the client would break. Double whoops.
Both of these are fixed properly now, AND I added a couple new endpoints so clients can allow users to see and manage their active sessions:
GET /oauth/sessions/{did}/devices— list all active sessionsDELETE /oauth/sessions/{did}/devices/{session_id}— revoke a session
The existing DELETE /oauth/sessions/{did} endpoint still works: confidential clients revoke all device sessions for the user, and public clients revoke the session matching their DPoP key. Full details in the Authentication guide.
SDK fix
If you tried to use @happyview/oauth-client with the latest versions of the @atproto/* SDKs, things would break because of a missing parameter. @happyview/oauth-client now provides that parameter and should also be backwards-compatible.
CI & infrastructure
- Binary releases — Rust binaries are now published to GitHub Releases alongside Docker images, so you can grab a prebuilt binary directly if you're not into Docker.
Contributors
Huge thanks to Chris Pardy for doing the bulk of the work for the new script system. 🥰
Go play
Full changelog is on GitHub. If you have questions, feature requests, or just need a little help, join the Cartridge Discord Server and hop into the #happyview channel.