Skip to content

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.

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.

Event TypeSeveritySubjectDetail
lexicon.createdinfoLexicon NSIDrevision, has_script, source
lexicon.updatedinfoLexicon NSIDrevision, has_script, source
lexicon.deletedinfoLexicon NSID—

Logged when lexicons are uploaded, updated, or deleted via the admin API. The actor_did is the user who performed the action.

Event TypeSeveritySubjectDetail
project.deployedinfoThe bundle’s sha256changes, 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.

Event TypeSeveritySubjectDetail
record.createdinfoRecord AT URIcollection, did, rkey
record.deletedinfoRecord AT URIcollection, did, rkey
record.skippedinfoRecord AT URIcollection, 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.

Event TypeSeveritySubjectDetail
script.executedinfoMethod NSIDmethod, caller_did, duration_ms, response_size, input, response
script.errorerrorMethod NSIDerror, 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.

Event TypeSeveritySubjectDetail
user.createdinfoNew user DIDtemplate (if used)
user.deletedinfoRemoved user ID—
user.bootstrappedinfoBootstrapped user DID—
user.permissions_updatedinfoUser IDgranted, revoked
user.super_transferredwarnNew superadmin’s user IDfrom_user_id

The user.bootstrapped event is logged when the first user is given superadmin automatically (see Auth - Auto-bootstrap).

Event TypeSeveritySubjectDetail
auth.permission_deniederrorEndpoint pathrequired_permission, user_id

Logged when a user attempts to access an endpoint they don’t have permission for.

Event TypeSeveritySubjectDetail
space.moderator_readinfoSpace AT URIaction, 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.

Event TypeSeveritySubjectDetail
api_key.createdinfoKey IDname, permissions
api_key.revokedinfoKey IDname
Event TypeSeveritySubjectDetail
script_variable.upsertedinfoVariable key—
script_variable.deletedinfoVariable key—
Event TypeSeveritySubjectDetail
script.executedinfoRecord or label AT URIhost_kind, host_id, trigger, attempts
script.dead_letterederrorRecord or label AT URIhost_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.

Event TypeSeveritySubjectDetail
backfill.startedinfoCollection NSIDjob_id
backfill.completedinfoCollection NSIDjob_id, total_repos
backfill.failederrorCollection NSIDjob_id, error

See Backfill for background on backfill jobs.

Event TypeSeveritySubjectDetail
jetstream.connectedinfo—url, protocol, compression, dictionary_id
jetstream.disconnectedwarn—reason
jetstream.gapwarn—reason, and more by reason (below)
jetstream.invalid_requesterror—message, retry_in_secs
jetstream.account_deletedinfoAccount DIDpurged, seq
jetstream.account_takendowninfoAccount DIDpurged, seq, status (takendown or suspended)
jetstream.account_inactiveinfoAccount DIDstatus (deactivated, desynchronized or throttled)
jetstream.account_status_mismatchwarnAccount DIDstatus, pds, seq
jetstream.sync_purgedinfoAccount DIDpurged, rev
jetstream.record_invalidwarnAccount DIDuri, seq, error
jetstream.apply_failederrorAccount DIDuri (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_seq is the saved seq (the last one known applied), resumed_at_seq the first seq received after it, and message Jetstream’s explanation. When HappyView replays the gap from the Jetstream archive instead, resumed_at_seq is the archive’s sealed tip and replay_job_id names 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. message names 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_seq is 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.

Use the admin API to query event logs with filters:

const TOKEN = "hv_..."; // your API key
const 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 errors
const 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 lexicon
const 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 events
const lexiconEvents: EventsResponse = await fetch(
"http://127.0.0.1:3000/admin/events?category=lexicon",
{ headers },
).then((r) => r.json());
// Paginate through results
const 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());

See the Admin API reference for full parameter documentation.

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.

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.