Event Logs
HappyView maintains an internal event log that records system activity — lexicon changes, record operations, Lua script executions and errors, user actions, API key events, backfill jobs, and Jetstream connectivity. Events are stored in the database and queryable via the admin API.
Event types
Section titled “Event types”Events follow a category.action naming convention. Each event has a severity level (info, warn, or error), an optional actor_did (the user who triggered it), an optional subject (what was affected), and a detail JSON object with event-specific data.
Lexicon events
Section titled “Lexicon events”| Event Type | Severity | Subject | Detail |
|---|---|---|---|
lexicon.created | info | Lexicon NSID | revision, has_script, source |
lexicon.updated | info | Lexicon NSID | revision, has_script, source |
lexicon.deleted | info | Lexicon NSID | — |
Logged when lexicons are uploaded, updated, or deleted via the admin API. The actor_did is the user who performed the action.
Project events
Section titled “Project events”| Event Type | Severity | Subject | Detail |
|---|---|---|---|
project.deployed | info | The bundle’s sha256 | changes, backfills |
Logged when a project is deployed through PUT /admin/project. A dry run logs nothing. changes and backfills are the ones the deploy answered with.
Record events
Section titled “Record events”| Event Type | Severity | Subject | Detail |
|---|---|---|---|
record.created | info | Record AT URI | collection, did, rkey |
record.deleted | info | Record AT URI | collection, did, rkey |
record.skipped | info | Record AT URI | collection, did, rkey, reason |
Logged when records are received from Jetstream and stored or removed from the local database, or skipped because a record script returned nil. These are system-triggered events (actor_did is null). They write one row per record, so successful operations are only logged when verbose_event_logging is on. If a database error occurs, the same event type is logged with error severity regardless of that setting, and the error message is included in the detail.
Script events
Section titled “Script events”| Event Type | Severity | Subject | Detail |
|---|---|---|---|
script.executed | info | Method NSID | method, caller_did, duration_ms, response_size, input, response |
script.error | error | Method NSID | error, script_source, input, caller_did, method |
Logged when Lua scripts run for XRPC query or procedure endpoints. script.executed is written on every call and stores the full request and response, so it is only logged when verbose_event_logging is on. script.error is always logged. Script errors capture the full context needed to reproduce and debug the issue: the error message, the complete Lua script source, the input that triggered it, and the caller’s DID.
User events
Section titled “User events”| Event Type | Severity | Subject | Detail |
|---|---|---|---|
user.created | info | New user DID | template (if used) |
user.deleted | info | Removed user ID | — |
user.bootstrapped | info | Bootstrapped user DID | — |
user.permissions_updated | info | User ID | granted, revoked |
user.super_transferred | warn | New superadmin’s user ID | from_user_id |
The user.bootstrapped event is logged when the first user is given superadmin automatically (see Auth - Auto-bootstrap).
Auth events
Section titled “Auth events”| Event Type | Severity | Subject | Detail |
|---|---|---|---|
auth.permission_denied | error | Endpoint path | required_permission, user_id |
Logged when a user attempts to access an endpoint they don’t have permission for.
Space events
Section titled “Space events”| Event Type | Severity | Subject | Detail |
|---|---|---|---|
space.moderator_read | info | Space AT URI | action, space_id, user_id, and what was read (see below) |
Logged when an admin reads a space’s records or blobs through the admin spaces API. action is list_records or get_blob. A list_records event includes the repo and collection filters and the uris returned. A get_blob event includes the blob cid and the repo that references it. Listing spaces and viewing their metadata or members is not logged.
API Key events
Section titled “API Key events”| Event Type | Severity | Subject | Detail |
|---|---|---|---|
api_key.created | info | Key ID | name, permissions |
api_key.revoked | info | Key ID | name |
Script Variable events
Section titled “Script Variable events”| Event Type | Severity | Subject | Detail |
|---|---|---|---|
script_variable.upserted | info | Variable key | — |
script_variable.deleted | info | Variable key | — |
Script events
Section titled “Script events”| Event Type | Severity | Subject | Detail |
|---|---|---|---|
script.executed | info | Record or label AT URI | host_kind, host_id, trigger, attempts |
script.dead_lettered | error | Record or label AT URI | host_kind, host_id, trigger, error |
Logged when record/label scripts run. script.executed fires once per record or label that reaches a script, so it is only logged when verbose_event_logging is on. Dead-lettered events indicate a script failed all retry attempts. You can manage dead letters from the Data > Dead Letters page in the dashboard — see Dead Letters below.
Backfill events
Section titled “Backfill events”| Event Type | Severity | Subject | Detail |
|---|---|---|---|
backfill.started | info | Collection NSID | job_id |
backfill.completed | info | Collection NSID | job_id, total_repos |
backfill.failed | error | Collection NSID | job_id, error |
See Backfill for background on backfill jobs.
Jetstream events
Section titled “Jetstream events”| Event Type | Severity | Subject | Detail |
|---|---|---|---|
jetstream.connected | info | — | url, protocol, compression, dictionary_id |
jetstream.disconnected | warn | — | reason |
jetstream.gap | warn | — | reason, and more by reason (below) |
jetstream.invalid_request | error | — | message, retry_in_secs |
jetstream.account_deleted | info | Account DID | purged, seq |
jetstream.account_takendown | info | Account DID | purged, seq, status (takendown or suspended) |
jetstream.account_inactive | info | Account DID | status (deactivated, desynchronized or throttled) |
jetstream.account_status_mismatch | warn | Account DID | status, pds, seq |
jetstream.sync_purged | info | Account DID | purged, rev |
jetstream.record_invalid | warn | Account DID | uri, seq, error |
jetstream.apply_failed | error | Account DID | uri (or kind and, for an account event, status), seq, error |
jetstream.connected and jetstream.disconnected are logged when the WebSocket connection to Jetstream is established or lost.
jetstream.gap means events may have been missed. The same gap (same reason and cursor) is logged once per run of the Jetstream consumer. Its reason is one of:
cursor_too_old: the saved cursor was past Jetstream’s retention and no saved time could replace it, so HappyView resumed from the live tip.after_seqis the saved seq (the last one known applied),resumed_at_seqthe first seq received after it, andmessageJetstream’s explanation. When HappyView replays the gap from the Jetstream archive instead,resumed_at_seqis the archive’s sealed tip andreplay_job_idnames the backfill job that fills the gap.outdated_cursor: Jetstream moved a time cursor older than its retention up to the oldest event it has.messagenames where it resumed.future_cursor: the saved seq was past Jetstream’s newest, because its seq numbering restarted. HappyView reconnects by the saved witnessed time, which Jetstream translates to the new numbering, or, with none saved, follows the new numbering from the live tip.from_seqis the cursor it asked for.
jetstream.invalid_request means Jetstream refused the subscription as invalid, or kept refusing it without saying why after HappyView dropped what it could from the request. HappyView retries after retry_in_secs (an hour by default).
jetstream.account_deleted is logged when an account this instance holds records for is deleted: its records (other than local writes not yet echoed back) are removed. jetstream.account_takendown is logged the same way for an account that is taken down or suspended. A tombstone is written for every deleted, taken-down or suspended account, but the event is logged only when this instance held, and purged, records for it. The tombstone means a late or replayed copy of the account’s records is refused. jetstream.account_status_mismatch is logged instead when the relay reports an account deleted (status) but the account’s current PDS (pds) says it is active: HappyView keeps its records and writes no tombstone. This happens when the relay is still relaying a PDS the account moved away from. Takedowns and suspensions are never checked this way, because a relay may impose them without the account’s PDS knowing. jetstream.account_inactive is logged when such an account is deactivated, desynchronized or throttled; its records are kept. When an account becomes active again, its tombstone is lifted, but removed records come back only through a new backfill. jetstream.sync_purged is logged when a repo is resynced: its records in registered collections are removed, and the current ones that follow in the stream replace them. Events for accounts this instance holds nothing for are not logged.
jetstream.record_invalid is logged once for an event HappyView skips immediately because retrying cannot help: a create or update with no record body, an unknown operation, data the database rejects, or a network lexicon schema that cannot be applied.
jetstream.apply_failed is logged when an event keeps failing for a reason that is not a database outage. HappyView retries it for about 31 seconds, logs this event and skips it, so one bad event does not stall the stream. Database outages are never skipped: see Real-time indexing.
Querying events
Section titled “Querying events”Use the admin API to query event logs with filters:
const TOKEN = "hv_..."; // your API keyconst headers = { Authorization: `Bearer ${TOKEN}` };
interface Event { id: string; event_type: string; severity: string; actor_did?: string; subject?: string; detail: Record<string, unknown>; created_at: string;}
interface EventsResponse { events: Event[]; cursor?: string;}
// Get all errorsconst errors: EventsResponse = await fetch( "http://127.0.0.1:3000/admin/events?severity=error", { headers },).then((r) => r.json());
// Get script errors for a specific lexiconconst scriptErrors: EventsResponse = await fetch( "http://127.0.0.1:3000/admin/events?event_type=script.error&subject=com.example.feed.like", { headers },).then((r) => r.json());
// Get all lexicon-related eventsconst lexiconEvents: EventsResponse = await fetch( "http://127.0.0.1:3000/admin/events?category=lexicon", { headers },).then((r) => r.json());
// Paginate through resultsconst page: EventsResponse = await fetch( "http://127.0.0.1:3000/admin/events?limit=20&cursor=2026-03-01T11:59:00Z", { headers },).then((r) => r.json());const TOKEN = "hv_..."; // your API keyconst headers = { Authorization: `Bearer ${TOKEN}` };
// Get all errorsconst errors = await fetch( "http://127.0.0.1:3000/admin/events?severity=error", { headers },).then((r) => r.json());
// Get script errors for a specific lexiconconst scriptErrors = await fetch( "http://127.0.0.1:3000/admin/events?event_type=script.error&subject=com.example.feed.like", { headers },).then((r) => r.json());
// Get all lexicon-related eventsconst lexiconEvents = await fetch( "http://127.0.0.1:3000/admin/events?category=lexicon", { headers },).then((r) => r.json());
// Paginate through resultsconst page = await fetch( "http://127.0.0.1:3000/admin/events?limit=20&cursor=2026-03-01T11:59:00Z", { headers },).then((r) => r.json());let client = reqwest::Client::new();let token = "hv_..."; // your API key
// Get all errorslet errors: serde_json::Value = client .get("http://127.0.0.1:3000/admin/events") .query(&[("severity", "error")]) .bearer_auth(token) .send() .await? .json() .await?;
// Get script errors for a specific lexiconlet script_errors: serde_json::Value = client .get("http://127.0.0.1:3000/admin/events") .query(&[ ("event_type", "script.error"), ("subject", "com.example.feed.like"), ]) .bearer_auth(token) .send() .await? .json() .await?;
// Get all lexicon-related eventslet lexicon_events: serde_json::Value = client .get("http://127.0.0.1:3000/admin/events") .query(&[("category", "lexicon")]) .bearer_auth(token) .send() .await? .json() .await?;
// Paginate through resultslet page: serde_json::Value = client .get("http://127.0.0.1:3000/admin/events") .query(&[("limit", "20"), ("cursor", "2026-03-01T11:59:00Z")]) .bearer_auth(token) .send() .await? .json() .await?;token := "hv_..." // your API key
// Get all errorsreq, _ := http.NewRequest("GET", "http://127.0.0.1:3000/admin/events?severity=error", nil)req.Header.Set("Authorization", "Bearer "+token)errors, err := http.DefaultClient.Do(req)
// Get script errors for a specific lexiconreq, _ = http.NewRequest("GET", "http://127.0.0.1:3000/admin/events?event_type=script.error&subject=com.example.feed.like", nil)req.Header.Set("Authorization", "Bearer "+token)scriptErrors, err := http.DefaultClient.Do(req)
// Get all lexicon-related eventsreq, _ = http.NewRequest("GET", "http://127.0.0.1:3000/admin/events?category=lexicon", nil)req.Header.Set("Authorization", "Bearer "+token)lexiconEvents, err := http.DefaultClient.Do(req)
// Paginate through resultsreq, _ = http.NewRequest("GET", "http://127.0.0.1:3000/admin/events?limit=20&cursor=2026-03-01T11:59:00Z", nil)req.Header.Set("Authorization", "Bearer "+token)page, err := http.DefaultClient.Do(req)AUTH="Authorization: Bearer hv_..." # your API key
# Get all errorscurl "http://127.0.0.1:3000/admin/events?severity=error" -H "$AUTH"
# Get script errors for a specific lexiconcurl "http://127.0.0.1:3000/admin/events?event_type=script.error&subject=com.example.feed.like" -H "$AUTH"
# Get all lexicon-related eventscurl "http://127.0.0.1:3000/admin/events?category=lexicon" -H "$AUTH"
# Paginate through resultscurl "http://127.0.0.1:3000/admin/events?limit=20&cursor=2026-03-01T11:59:00Z" -H "$AUTH"See the Admin API reference for full parameter documentation.
Retention
Section titled “Retention”Event logs are automatically cleaned up based on the EVENT_LOG_RETENTION_DAYS environment variable (default: 30 days). A background task runs hourly to delete events older than the configured retention period.
Set EVENT_LOG_RETENTION_DAYS=0 to disable automatic cleanup and keep logs indefinitely.
See Configuration for all environment variables.
Dead Letters
Section titled “Dead Letters”When a record or label script fails after all retry attempts, the event is stored in the dead letters queue. You can manage dead letters from the Data > Dead Letters page in the dashboard.
From the dead letters page you can:
- Retry Script — replay the stored event through the script (use after fixing the script)
- Re-index — fetch the record fresh from the PDS and run it through the full indexing pipeline (use when the record may have changed)
- Dismiss — mark the dead letter as resolved without retrying
Bulk actions are available for selected rows or all entries matching the current filters.
Next steps
Section titled “Next steps”- Admin API — Event Logs — full query parameters and response format
- Permissions — control which users can read event logs
- Troubleshooting — using event logs to diagnose issues

