A lexicon-driven AppView for ATProto.
happyview docs reference admin-api.md
13 kB
Markdown
at commit 6506ff28

Admin API #

The admin API lets you manage lexicons, monitor records, run backfill jobs, and control admin access. All endpoints live under /admin and require an AIP-issued Bearer token from a DID that exists in the admins table. You can also manage all of this through the web dashboard.

Auth #

The admin API supports two authentication methods:

  1. OAuth (AIP) — the Bearer token is validated against AIP's /oauth/userinfo endpoint to retrieve the caller's DID.
  2. API keys — read/write tokens starting with hv_. See the API Keys guide for details.

In both cases the resolved DID is checked against the admins table.

Auto-bootstrap: If the admins table is empty, the first authenticated request automatically inserts the caller as the initial admin.

Non-admin DIDs receive a 403 Forbidden response.

All error responses return JSON with an error field:

{
  "error": "description of what went wrong"
}
Status Meaning
400 Bad Request Invalid input (missing required fields, malformed lexicon JSON)
401 Unauthorized Missing or invalid Bearer token. See AIP documentation for token issues
403 Forbidden Authenticated DID is not in the admins table
404 Not Found Lexicon, admin, or backfill job not found
# All examples assume $TOKEN is an AIP-issued access token or API key
AUTH="Authorization: Bearer $TOKEN"

Lexicons #

Upload / upsert a lexicon #

POST /admin/lexicons
curl -X POST http://localhost:3000/admin/lexicons \
  -H "$AUTH" \
  -H "Content-Type: application/json" \
  -d '{
    "lexicon_json": { "lexicon": 1, "id": "xyz.statusphere.status", "defs": { "main": { "type": "record", "key": "tid", "record": { "type": "object", "required": ["status", "createdAt"], "properties": { "status": { "type": "string", "maxGraphemes": 1 }, "createdAt": { "type": "string", "format": "datetime" } } } } } },
    "backfill": true,
    "target_collection": null
  }'
Field Type Required Description
lexicon_json object yes Raw lexicon JSON (must have lexicon: 1 and id)
backfill boolean no Whether uploading triggers historical backfill (default true)
target_collection string no For query/procedure lexicons, the record collection they operate on
script string no Lua script for query/procedure endpoints
index_hook string no Index hook Lua script for record lexicons

Response: 201 Created (new) or 200 OK (upsert)

{
  "id": "xyz.statusphere.status",
  "revision": 1
}

List lexicons #

GET /admin/lexicons
curl http://localhost:3000/admin/lexicons -H "$AUTH"

Response: 200 OK

[
  {
    "id": "xyz.statusphere.status",
    "revision": 1,
    "lexicon_type": "record",
    "backfill": true,
    "created_at": "2025-01-01T00:00:00Z",
    "updated_at": "2025-01-01T00:00:00Z"
  }
]

Get a lexicon #

GET /admin/lexicons/{id}
curl http://localhost:3000/admin/lexicons/xyz.statusphere.status -H "$AUTH"

Response: 200 OK with full lexicon details including raw JSON.

Delete a lexicon #

DELETE /admin/lexicons/{id}
curl -X DELETE http://localhost:3000/admin/lexicons/xyz.statusphere.status -H "$AUTH"

Response: 204 No Content

Network Lexicons #

Network lexicons are fetched from the AT Protocol network via DNS TXT resolution and kept updated via Tap. See Lexicons - Network lexicons for background.

Add a network lexicon #

POST /admin/network-lexicons
curl -X POST http://localhost:3000/admin/network-lexicons \
  -H "$AUTH" \
  -H "Content-Type: application/json" \
  -d '{
    "nsid": "xyz.statusphere.status",
    "target_collection": null
  }'
Field Type Required Description
nsid string yes The NSID of the lexicon to watch
target_collection string no For query/procedure lexicons, the record collection they operate on

HappyView resolves the NSID authority via DNS TXT, fetches the lexicon from the authority's PDS, parses it, and stores it.

Response: 201 Created

{
  "nsid": "xyz.statusphere.status",
  "authority_did": "did:plc:authority",
  "revision": 1
}

List network lexicons #

GET /admin/network-lexicons
curl http://localhost:3000/admin/network-lexicons -H "$AUTH"

Response: 200 OK

[
  {
    "nsid": "xyz.statusphere.status",
    "authority_did": "did:plc:authority",
    "target_collection": null,
    "last_fetched_at": "2025-01-01T00:00:00Z",
    "created_at": "2025-01-01T00:00:00Z"
  }
]

Remove a network lexicon #

DELETE /admin/network-lexicons/{nsid}
curl -X DELETE http://localhost:3000/admin/network-lexicons/xyz.statusphere.status \
  -H "$AUTH"

Removes the network lexicon tracking and also deletes the lexicon from the lexicons table and in-memory registry.

Response: 204 No Content

Stats #

Record counts #

GET /admin/stats
curl http://localhost:3000/admin/stats -H "$AUTH"

Response: 200 OK

{
  "total_records": 12345,
  "collections": [{ "collection": "xyz.statusphere.status", "count": 500 }]
}

Tap Stats #

Aggregate stats from the Tap instance. Useful for monitoring backfill progress. See Backfill - Job lifecycle for context.

Get Tap stats #

GET /admin/tap/stats
curl http://localhost:3000/admin/tap/stats -H "$AUTH"

Response: 200 OK

{
  "repo_count": 5234,
  "record_count": 1048576,
  "outbox_buffer": 42
}
Field Type Description
repo_count number Total repos Tap is tracking
record_count number Total records Tap has indexed
outbox_buffer number Pending events awaiting delivery (high = Tap is busy)

Returns 502 Bad Gateway if Tap is unreachable.

Backfill #

Create a backfill job #

POST /admin/backfill
curl -X POST http://localhost:3000/admin/backfill \
  -H "$AUTH" \
  -H "Content-Type: application/json" \
  -d '{ "collection": "xyz.statusphere.status" }'
Field Type Required Description
collection string no Limit to a single collection (backfills all if omitted)
did string no Limit to a single DID (discovers all via relay if omitted)

Response: 201 Created

{
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "status": "pending"
}

List backfill jobs #

GET /admin/backfill/status
curl http://localhost:3000/admin/backfill/status -H "$AUTH"

Response: 200 OK

[
  {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "collection": "xyz.statusphere.status",
    "did": null,
    "status": "completed",
    "total_repos": 42,
    "processed_repos": 42,
    "total_records": 1000,
    "error": null,
    "started_at": "2025-01-01T00:01:00Z",
    "completed_at": "2025-01-01T00:05:00Z",
    "created_at": "2025-01-01T00:00:00Z"
  }
]

Event Logs #

HappyView records an audit trail of system events: lexicon changes, record operations, Lua script executions and errors, admin actions, backfill jobs, and Tap connectivity. See the Event Logs guide for details on event types and retention.

List event logs #

GET /admin/events
curl "http://localhost:3000/admin/events?severity=error&limit=10" -H "$AUTH"
Param Type Required Description
event_type string no Filter by exact event type (e.g. script.error)
category string no Filter by category prefix (e.g. lexicon matches all lexicon events)
severity string no Filter by severity: info, warn, or error
subject string no Filter by subject (lexicon ID, record URI, admin DID, etc.)
cursor string no Pagination cursor (ISO 8601 timestamp from previous response)
limit number no Results per page (default 50, max 100)

Response: 200 OK

{
  "events": [
    {
      "id": "550e8400-e29b-41d4-a716-446655440000",
      "event_type": "script.error",
      "severity": "error",
      "actor_did": "did:plc:abc123",
      "subject": "com.example.feed.like",
      "detail": {
        "error": "attempt to index nil value",
        "script_source": "function handle() ... end",
        "input": { "status": "hello" },
        "caller_did": "did:plc:abc123",
        "method": "com.example.feed.like"
      },
      "created_at": "2026-03-01T12:00:00Z"
    }
  ],
  "cursor": "2026-03-01T11:59:00Z"
}

Events are returned in reverse chronological order (newest first). Pass the cursor value from the response to fetch the next page.

API Keys #

Manage API keys for programmatic access. See the API Keys guide for usage details.

Create an API key #

POST /admin/api-keys
curl -X POST http://localhost:3000/admin/api-keys \
  -H "$AUTH" \
  -H "Content-Type: application/json" \
  -d '{ "name": "CI Deploy" }'
Field Type Required Description
name string yes A label to identify this key's usage

Response: 201 Created

{
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "name": "CI Deploy",
  "key": "hv_a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4",
  "key_prefix": "hv_a1b2c3d4"
}

The key field contains the full API key. It is only returned in this response — store it securely.

List API keys #

GET /admin/api-keys
curl http://localhost:3000/admin/api-keys -H "$AUTH"

Response: 200 OK

[
  {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "name": "CI Deploy",
    "key_prefix": "hv_a1b2c3d4",
    "created_at": "2026-03-01T00:00:00Z",
    "last_used_at": "2026-03-06T12:00:00Z",
    "revoked_at": null
  }
]

Only returns keys belonging to the authenticated admin. The full key is never included — only the prefix.

Revoke an API key #

DELETE /admin/api-keys/{id}
curl -X DELETE http://localhost:3000/admin/api-keys/550e8400-e29b-41d4-a716-446655440000 \
  -H "$AUTH"

Sets revoked_at on the key. The key remains in the database for audit purposes but can no longer authenticate.

Response: 204 No Content

Admin management #

Add an admin #

POST /admin/admins
curl -X POST http://localhost:3000/admin/admins \
  -H "$AUTH" \
  -H "Content-Type: application/json" \
  -d '{ "did": "did:plc:newadmin" }'

Response: 201 Created

{
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "did": "did:plc:newadmin"
}

List admins #

GET /admin/admins
curl http://localhost:3000/admin/admins -H "$AUTH"

Response: 200 OK

[
  {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "did": "did:plc:admin",
    "created_at": "2025-01-01T00:00:00Z",
    "last_used_at": "2025-01-02T12:00:00Z"
  }
]

Remove an admin #

DELETE /admin/admins/{id}
curl -X DELETE http://localhost:3000/admin/admins/550e8400-e29b-41d4-a716-446655440000 \
  -H "$AUTH"

Response: 204 No Content