A lexicon-driven AppView for ATProto. happyview.dev
backfill firehose jetstream atproto appview oauth lexicon
8
fork

Configure Feed

Select the types of activity you want to include in your feed.

docs: update for v2

Trezy 066b0f7b cb4b1c1a

+2420 -1963
+2 -2
packages/docs/docs/README.md
··· 2 2 3 3 HappyView is the best way to build an [AppView](https://atproto.com/guides/glossary#app-view) for the [AT Protocol](https://atproto.com). Upload your [lexicon](reference/glossary.md#at-protocol-terms) schemas and get a fully functional AppView, complete with [XRPC](reference/glossary.md#at-protocol-terms) endpoints, OAuth, real-time network sync, and historical [backfill](guides/backfill.md), without writing a single line of server code. 4 4 5 - Building an AppView from scratch means wiring up firehose connections, record storage, XRPC routing, OAuth flows, and PDS write proxying before you can even think about your application. HappyView handles all of that. Define your data model with lexicons, add custom logic with Lua scripts when you need it, and ship your app. 5 + Building an AppView from scratch means wiring up real-time event streams, record storage, XRPC routing, OAuth flows, and PDS write proxying before you can even think about your application. HappyView handles all of that. Define your data model with lexicons, add custom logic with Lua scripts when you need it, and ship your app. 6 6 7 7 ## Features 8 8 ··· 16 16 17 17 - **Schema-first**: Your Lexicons are the source of truth. Upload a schema and HappyView derives endpoints, indexing rules, and network sync from it. You describe _what_ your data looks like; HappyView figures out the rest. 18 18 19 - - **Zero boilerplate**: HappyView handles AppView infrastructure (firehose, backfill, OAuth, PDS proxying) for you. You should be writing application logic from minute one, not plumbing. 19 + - **Zero boilerplate**: HappyView handles AppView infrastructure (Jetstream, backfill, OAuth, PDS proxying) for you. You should be writing application logic from minute one, not plumbing. 20 20 21 21 - **Runtime-configurable**: Lexicons can be added, updated, and removed without restarting the server. New endpoints and sync rules take effect immediately, so you can iterate on your data model in real time. 22 22
+30 -31
packages/docs/docs/getting-started/authentication.md
··· 3 3 HappyView has two distinct authentication surfaces: 4 4 5 5 - **XRPC** (`/xrpc/*`) — client-level identification via an **API client key** on every request, plus optional user-level AT Protocol OAuth for endpoints that need a specific user's identity (e.g. procedures that write to a PDS). 6 - - **Admin API** (`/admin/*`) — user-level authentication via session cookies, admin API keys, or service auth JWTs, gated by [permissions](../guides/permissions.md). 6 + - **Admin API** (`/admin/*`) — user-level authentication via admin API keys or service auth JWTs, gated by [permissions](../guides/permissions.md). 7 7 8 8 ## Which endpoints require what? 9 9 10 - | Endpoint type | Client identification | User authentication | 11 - | ----------------------------------- | ------------------------ | ------------------------------------------------------------------------------------ | 12 - | Queries (`GET /xrpc/{method}`) | `X-Client-Key` required | Optional — DPoP auth if the query needs to know who the user is | 13 - | Procedures (`POST /xrpc/{method}`) | `X-Client-Key` required | Required — DPoP auth so HappyView can proxy writes to the user's PDS | 14 - | Admin API (`/admin/*`) | — | Required — session cookie, admin API key, or service auth JWT with the right [permissions](../guides/permissions.md) | 15 - | Health check (`GET /health`) | — | — | 10 + | Endpoint type | Client identification | User authentication | 11 + | ---------------------------------- | ----------------------- | ------------------------------------------------------------------------------------------------ | 12 + | Queries (`GET /xrpc/{method}`) | `X-Client-Key` required | Optional — DPoP auth if the query needs to know who the user is | 13 + | Procedures (`POST /xrpc/{method}`) | `X-Client-Key` required | Required — DPoP auth so HappyView can proxy writes to the user's PDS | 14 + | Admin API (`/admin/*`) | — | Required — admin API key or service auth JWT with the right [permissions](../guides/permissions.md) | 15 + | Health check (`GET /health`) | — | — | 16 16 17 17 ## XRPC: API client identification 18 18 ··· 22 22 23 23 HappyView resolves the client key from the first of: 24 24 25 - 1. The session cookie, if the user logged in through this client's OAuth flow (the cookie carries the `client_key` that minted it). 26 - 2. The `X-Client-Key` request header. 27 - 3. A `client_key` query-string parameter. 25 + 1. The `X-Client-Key` request header. 26 + 2. A `client_key` query-string parameter. 28 27 29 28 On top of the client key, HappyView does best-effort validation that the caller actually controls the client: 30 29 ··· 52 51 53 52 Queries that don't care who is calling need nothing more than the client key. Procedures — and queries whose Lua scripts read the caller's DID — need a real AT Protocol OAuth session. 54 53 55 - XRPC routes only accept **DPoP auth** (`Authorization: DPoP <token>` + `DPoP` proof header + `X-Client-Key`). Bearer tokens, service auth JWTs, and session cookies are not accepted on XRPC endpoints. 54 + XRPC routes only accept **DPoP auth** (`Authorization: DPoP <token>` + `DPoP` proof header + `X-Client-Key`). Bearer tokens and service auth JWTs are not accepted on XRPC endpoints. 56 55 57 56 Third-party apps authenticate users through the [DPoP key provisioning](#dpop-key-provisioning-for-third-party-apps) flow: your app gets a DPoP keypair from HappyView, runs a standard OAuth flow with the user's PDS using that keypair, then registers the resulting tokens back with HappyView. 58 57 ··· 86 85 87 86 For procedures, HappyView proxies the write to the user's PDS using the stored OAuth session (see [Proxying procedures](#proxying-procedures-to-the-users-pds) below). 88 87 89 - :::note 90 - The HappyView dashboard uses a separate cookie-based OAuth flow where HappyView itself acts as the OAuth server. This is only for the dashboard — third-party apps always use DPoP key provisioning. 91 - ::: 92 - 93 88 ## Admin API: user authentication 94 89 95 - Admin endpoints don't use API clients. They require a real HappyView user, identified by one of three methods: 96 - 97 - ### Session cookie (dashboard) 98 - 99 - When you log in to the dashboard via AT Protocol OAuth, HappyView sets a signed, HttpOnly session cookie containing your DID. That cookie is honored on admin endpoints as long as the DID is a HappyView user with the required permission for the call. 90 + Admin endpoints don't use API clients. They require a real HappyView user, identified by one of two methods: 100 91 101 92 ### Admin API key 102 93 ··· 125 116 126 117 ### Admin access and the first user 127 118 128 - On a fresh deployment, the `users` table is empty. The first authenticated request to any admin endpoint auto-bootstraps that user as the **super user** with all permissions granted — so the first handle to log in owns the instance. 119 + On a fresh deployment, the `users` table is empty. The first authenticated request to any admin endpoint auto-bootstraps that user as the **super user** with all permissions granted. This includes logging in to the dashboard — the dashboard makes admin API calls on your behalf, so the first person to log in becomes the super user. 129 120 130 - To add more users after that, use `POST /admin/users` or the [dashboard](dashboard.md). You can assign permissions individually or use a template (`viewer`, `operator`, `manager`, `full_access`). See [Admin API](../reference/admin-api.md#user-management) for details. 121 + To add more users after that, use `POST /admin/users` or the [dashboard](dashboard.md). You can assign permissions individually or use a template (`viewer`, `operator`, `manager`, `full_access`). See [Admin API — Users](../reference/admin/users.md) for details. 131 122 132 123 ## Proxying procedures to the user's PDS 133 124 134 - When a client calls an XRPC procedure that writes a record, HappyView proxies the write to the user's PDS. There are two auth paths that support this: 125 + When a client calls an XRPC procedure that writes a record, HappyView proxies the write to the user's PDS. This requires a DPoP-authenticated session — the app must have gone through the [DPoP key provisioning](#dpop-key-provisioning-for-third-party-apps) flow and registered tokens for the user. HappyView uses the app's provisioned DPoP key to generate fresh proofs and attach the stored access token to the outbound PDS request. 135 126 136 - - **Cookie auth (dashboard)** — `atrium-oauth` attaches a DPoP proof and a DPoP-bound access token to the outbound request automatically. 137 - - **DPoP key provisioning (third-party apps)** — HappyView uses the app's provisioned DPoP key to generate fresh proofs and attach the stored access token (see below). 138 - 139 - A request that only carries an `X-Client-Key` header (no session cookie or DPoP token) can hit queries but can't proxy writes — there's no user to write as. Service auth JWTs and admin API keys similarly don't carry a user session. 127 + A request that only carries an `X-Client-Key` header (no DPoP token) can hit queries but can't proxy writes — there's no user to write as. 140 128 141 129 ## DPoP key provisioning for third-party apps 142 130 143 - Third-party apps that want HappyView to make PDS writes on behalf of their users use the **DPoP key provisioning** flow instead of cookie auth. This avoids browser-based redirects through HappyView's domain, which can be blocked by Firefox's Bounce Tracker Protection. 131 + Third-party apps that want HappyView to make PDS writes on behalf of their users use the **DPoP key provisioning** flow. This avoids browser-based redirects through HappyView's domain, which can be blocked by Firefox's Bounce Tracker Protection. 144 132 145 133 The idea: the app gets a DPoP keypair from HappyView, uses that keypair during its own OAuth flow with the user's PDS, then registers the resulting tokens back with HappyView. From that point on, XRPC requests authenticated with `Authorization: DPoP <access_token>` plus a `DPoP` proof header and `X-Client-Key` will have HappyView proxy writes using the stored session. 146 134 ··· 184 172 ```json 185 173 { 186 174 "provision_id": "hvp_...", 187 - "dpop_key": { "kty": "EC", "crv": "P-256", "x": "...", "y": "...", "d": "..." } 175 + "dpop_key": { 176 + "kty": "EC", 177 + "crv": "P-256", 178 + "x": "...", 179 + "y": "...", 180 + "d": "..." 181 + } 188 182 } 189 183 ``` 190 184 ··· 223 217 "provision_id": "hvp_...", 224 218 "pkce_verifier": "...", 225 219 "did": "did:plc:user123", 226 - ... 220 + "access_token": "...", 221 + "refresh_token": "...", 222 + "expires_at": "2026-04-17T00:00:00Z", 223 + "scopes": "atproto transition:generic", 224 + "pds_url": "https://bsky.social", 225 + "issuer": "https://bsky.social" 227 226 } 228 227 ``` 229 228 ··· 283 282 - [JavaScript SDK](../sdk/overview.md) — authenticate and make XRPC calls from JavaScript 284 283 - [Permissions](../guides/permissions.md) — full list of permissions and what each one grants 285 284 - [API Keys](../guides/api-keys.md) — create scoped admin API keys for automation 286 - - [Admin API — API Clients](../reference/admin-api.md#api-clients) — register API clients and configure rate limits 285 + - [Admin API — API Clients](../reference/admin/api-clients.md) — register API clients and configure rate limits
+1 -1
packages/docs/docs/getting-started/configuration.md
··· 9 9 | `DATABASE_URL` | yes | --- | Database connection string. SQLite (`sqlite://path/to/db?mode=rwc`) or Postgres (`postgres://user:pass@host/db`) | 10 10 | `DATABASE_BACKEND` | no | auto-detected | Force `sqlite` or `postgres`. Auto-detected from `DATABASE_URL` scheme if not set | 11 11 | `PUBLIC_URL` | yes | --- | Public-facing URL for HappyView (used for OAuth callbacks, e.g. `https://happyview.example.com`) | 12 - | `SESSION_SECRET` | no | dev default | Secret key for signing session cookies. **Must be set in production** | 12 + | `SESSION_SECRET` | no | dev default | Secret key for signing session cookies (at least 64 characters). **Must be set in production** | 13 13 | `HOST` | no | `0.0.0.0` | Bind host | 14 14 | `PORT` | no | `3000` | Bind port | 15 15 | `JETSTREAM_URL` | no | `wss://jetstream1.us-east.bsky.network` | Jetstream WebSocket URL for real-time record streaming |
+61 -4
packages/docs/docs/getting-started/dashboard.md
··· 1 1 # Dashboard 2 2 3 - HappyView ships with a web dashboard that provides a visual interface for everything the [admin API](../reference/admin-api.md) offers: managing lexicons, viewing indexed records, and monitoring backfill jobs. It runs as a separate Next.js application alongside the Rust backend and authenticates via AT Protocol OAuth. 3 + HappyView ships with a web dashboard that provides a visual interface for everything the [admin API](../reference/admin-api.md) offers. It runs as a separate Next.js application alongside the Rust backend and authenticates via AT Protocol OAuth. 4 4 5 - On a fresh deployment with no users in the database, the first handle to log in is automatically bootstrapped as the super user with all permissions — so log in with the handle you want to own the instance first. 5 + On a fresh deployment with no users in the database, the first person to log in to the dashboard is automatically bootstrapped as the super user with all permissions — so log in with the handle you want to own the instance first. 6 6 7 - ## Adding a lexicon 7 + ## Lexicons 8 8 9 - Navigate to **Lexicons > Add Lexicon** and choose **Local** or **Network**. 9 + Navigate to **Lexicons** to see all uploaded lexicons. Each entry shows the NSID, type (record, query, procedure), and whether a Lua script is attached. 10 + 11 + ### Adding a lexicon 12 + 13 + Click **Add Lexicon** and choose **Local** or **Network**. 10 14 11 15 **Local** lexicons are defined by you. The editor shows two side-by-side panels (stacked on mobile): 12 16 ··· 35 39 36 40 See [Lua Scripting](../guides/scripting.md) for the full runtime reference and examples. 37 41 42 + ## Records 43 + 44 + Navigate to **Records** to browse all indexed AT Protocol records. Records are grouped by collection and searchable. Each record shows its AT URI, author DID, and the raw record JSON. 45 + 46 + ## Backfill 47 + 48 + Navigate to **Backfill** to view and manage backfill jobs. You can start a new backfill for any record-type lexicon to import historical records from the network. The page shows job status, progress (repos processed / total), and record counts. See [Backfill](../guides/backfill.md) for how the process works. 49 + 50 + ## Users 51 + 52 + Navigate to **Users** to manage who can access the admin API and dashboard. You can add users by DID, assign permissions individually or via a template (`viewer`, `operator`, `manager`, `full_access`), and remove users. The super user is highlighted and has all permissions by default. See [Permissions](../guides/permissions.md) for what each permission grants. 53 + 54 + ## Events 55 + 56 + Navigate to **Events** to view the audit log of admin actions. Events include user creation, lexicon uploads, permission changes, backfill starts, and more. Each entry shows the event type, severity, actor, subject, and timestamp. Events are retained for the number of days configured by `EVENT_LOG_RETENTION_DAYS` (default 30). 57 + 58 + ## Settings 59 + 60 + The **Settings** section contains several sub-pages: 61 + 62 + ### General 63 + 64 + Configure instance-level settings: application name, logo, terms of service URL, and privacy policy URL. These values appear on OAuth authorization screens and can also be set via environment variables — dashboard values take precedence. 65 + 66 + ### API Clients 67 + 68 + Register and manage third-party API clients. Each client gets an `hvc_…` client key and `hvs_…` client secret. You can configure the client type (confidential or public), allowed origins, scopes, and per-client rate limits. See [Authentication — API client identification](authentication.md#xrpc-api-client-identification) for how clients are used. 69 + 70 + ### API Keys 71 + 72 + Create and revoke admin API keys for automation. Each key is scoped to specific permissions and tied to the creating user. See [API Keys](../guides/api-keys.md) for details. 73 + 74 + ### Users 75 + 76 + An alternative path to the top-level Users page for managing user accounts and permissions. 77 + 78 + ### Plugins 79 + 80 + Manage installed plugins and configure plugin secrets. Plugins extend HappyView with additional functionality. Plugin secrets are encrypted at rest when `TOKEN_ENCRYPTION_KEY` is configured. See [Plugins](../guides/plugins.md) for details. 81 + 82 + ### Labelers 83 + 84 + Configure labeler subscriptions for content labeling. See [Labelers](../guides/labelers.md) for details. 85 + 86 + ### Environment Variables 87 + 88 + View the current values of all environment variables that affect HappyView's behavior. This is a read-only view — values are set via your deployment environment, not the dashboard. 89 + 90 + ### Accounts 91 + 92 + Manage connected AT Protocol accounts used by the instance. 93 + 38 94 ## Next steps 39 95 40 96 - [Lexicons](../guides/lexicons.md) — how lexicons drive HappyView's indexing and routing 41 97 - [Lua Scripting](../guides/scripting.md) — write custom query and procedure logic 42 98 - [Permissions](../guides/permissions.md) — manage user access to admin features 99 + - [Configuration](configuration.md) — full list of environment variables
+1 -1
packages/docs/docs/getting-started/deployment/docker.md
··· 14 14 cp .env.example .env 15 15 ``` 16 16 17 - Edit `.env` and set at least `PUBLIC_URL` (e.g. `http://localhost:3000`) and `SESSION_SECRET`. The defaults work for everything else. See [Configuration](../configuration.md) for the full list of environment variables. 17 + Edit `.env` and set at least `PUBLIC_URL` (e.g. `http://localhost:3000`) and `SESSION_SECRET` (at least 64 characters). The defaults work for everything else. See [Configuration](../configuration.md) for the full list of environment variables. 18 18 19 19 ## 2. Start the stack 20 20
+4 -1
packages/docs/docs/getting-started/deployment/railway.md
··· 8 8 9 9 After deploying the template, you'll need to configure a few things before the stack works properly: 10 10 11 - 1. **Set your session secret.** In the HappyView service variables, set `SESSION_SECRET` to a strong random value. This is used to sign session cookies. 11 + 1. **Set your session secret.** In the HappyView service variables, set `SESSION_SECRET` to a random string of at least 64 characters. This is used to sign session cookies. 12 + ```sh 13 + openssl rand -base64 48 14 + ``` 12 15 13 16 2. **Assign a public domain.** In the Railway dashboard, add a public domain to the HappyView service. The service needs a publicly accessible URL for OAuth callbacks. Set `PUBLIC_URL` to this domain (e.g. `https://happyview-production.up.railway.app`). 14 17 :::note
+3 -3
packages/docs/docs/guides/backfill.md
··· 7 7 - **Automatically** when a record-type lexicon is uploaded with `backfill: true` (the default). See [Lexicons - Backfill flag](lexicons.md#backfill-flag). 8 8 - **Manually** via `POST /admin/backfill` or the [dashboard](../getting-started/dashboard.md). You can scope a manual backfill to a specific collection, a specific DID, or both. 9 9 10 - See the [admin API](../reference/admin-api.md#backfill) for endpoint details. 10 + See the [admin API](../reference/admin/backfill.md) for endpoint details. 11 11 12 12 ## How it works 13 13 ··· 19 19 20 20 ## Job lifecycle 21 21 22 - A backfill job moves through `pending → running → completed` (or `failed`). Unlike earlier versions of HappyView, the job is only marked `completed` once every discovered repo has been processed end-to-end — there is no separate downstream queue. Progress is visible in real time on the dashboard's Backfill page. 22 + A backfill job moves through `pending → running → completed` (or `failed`). Unlike earlier versions of HappyView that relied on Tap, the job is only marked `completed` once every discovered repo has been processed end-to-end — there is no separate downstream queue. Progress is visible in real time on the dashboard's Backfill page. 23 23 24 24 If a job fails midway, the `error` field contains the failure reason. Re-running the backfill resumes from scratch but is idempotent (records are upserted by URI). 25 25 ··· 34 34 ## Next steps 35 35 36 36 - [Lexicons](lexicons.md#backfill-flag): Control whether lexicons trigger backfill on upload 37 - - [Admin API](../reference/admin-api.md#backfill): Full reference for backfill endpoints 37 + - [Admin API — Backfill](../reference/admin/backfill.md): Full reference for backfill endpoints
+1
packages/docs/docs/guides/database-setup.md
··· 72 72 73 73 ## Next steps 74 74 75 + - [SQLite → Postgres migration](sqlite-to-postgres-migration.md) — switch an existing instance from SQLite to Postgres 75 76 - [Postgres → SQLite migration](postgres-to-sqlite-migration.md) — switch an existing instance from Postgres to SQLite 76 77 - [Lua scripting](scripting.md) — write queries that target either backend 77 78 - [Configuration](../getting-started/configuration.md) — `DATABASE_URL` and related variables
+105
packages/docs/docs/guides/developing-plugins.md
··· 1 + # Developing Plugins 2 + 3 + This guide covers how to build your own HappyView WASM plugins. For installing and configuring plugins, see the [Plugins guide](plugins.md). 4 + 5 + See the [happyview-plugins](https://github.com/gamesgamesgamesgames/happyview-plugins) repository for examples and the plugin SDK. 6 + 7 + ## Plugin Manifest 8 + 9 + Each plugin has a `manifest.json` that describes its metadata: 10 + 11 + ```json 12 + { 13 + "id": "steam", 14 + "name": "Steam", 15 + "version": "1.0.0", 16 + "api_version": "1", 17 + "description": "Import your Steam game library and playtime data.", 18 + "icon_url": "https://example.com/steam-icon.png", 19 + "auth_type": "openid", 20 + "wasm_file": "steam.wasm", 21 + "required_secrets": [ 22 + { 23 + "key": "PLUGIN_STEAM_API_KEY", 24 + "name": "Steam Web API Key", 25 + "description": "Get your API key at steamcommunity.com/dev/apikey" 26 + } 27 + ] 28 + } 29 + ``` 30 + 31 + | Field | Description | 32 + | ------------------ | ----------------------------------------------------- | 33 + | `id` | Unique plugin identifier | 34 + | `name` | Display name | 35 + | `version` | Semantic version | 36 + | `api_version` | Plugin API version (currently "1") | 37 + | `description` | Brief description shown during install | 38 + | `icon_url` | Optional icon URL | 39 + | `auth_type` | Authentication type: `oauth2`, `openid`, or `api_key` | 40 + | `wasm_file` | WASM binary filename (default: `plugin.wasm`) | 41 + | `required_secrets` | Array of secrets the plugin needs | 42 + 43 + ## API Endpoints 44 + 45 + ### Public Endpoints 46 + 47 + | Endpoint | Description | 48 + | --------------------------------------- | ---------------------------------------------- | 49 + | `GET /external-auth/providers` | List available auth providers | 50 + | `GET /external-auth/accounts` | List user's linked accounts | 51 + | `GET /external-auth/{plugin}/authorize` | Start OAuth flow | 52 + | `GET /external-auth/{plugin}/callback` | OAuth callback handler | 53 + | `POST /external-auth/{plugin}/sync` | Sync data from linked account | 54 + | `POST /external-auth/{plugin}/unlink` | Unlink account | 55 + | `POST /external-auth/{plugin}/connect` | Connect with API key (for `api_key` auth type) | 56 + 57 + ### Admin Endpoints 58 + 59 + | Endpoint | Description | 60 + | --------------------------------------- | ------------------------------------------- | 61 + | `GET /admin/plugins` | List installed plugins | 62 + | `POST /admin/plugins` | Install a plugin | 63 + | `POST /admin/plugins/preview` | Preview plugin before installing | 64 + | `GET /admin/plugins/official` | Browse the official plugin registry catalog | 65 + | `DELETE /admin/plugins/{id}` | Remove a plugin | 66 + | `POST /admin/plugins/{id}/reload` | Reload plugin from source | 67 + | `POST /admin/plugins/{id}/check-update` | Check whether a newer version is available | 68 + | `GET /admin/plugins/{id}/secrets` | Get configured secrets (masked) | 69 + | `PUT /admin/plugins/{id}/secrets` | Update plugin secrets | 70 + 71 + The dashboard's **Settings > Plugins** page calls `GET /admin/plugins/official` to populate the install browser, and `POST /admin/plugins/{id}/check-update` to display update badges on installed plugins. 72 + 73 + ## Plugin Exports 74 + 75 + Plugins must export these functions: 76 + 77 + | Export | Signature | Description | 78 + | ------------------- | ----------------------------- | ---------------------------- | 79 + | `alloc` | `(size: u32) -> u32` | Allocate memory | 80 + | `dealloc` | `(ptr: u32, size: u32)` | Deallocate memory | 81 + | `get_authorize_url` | `(ptr: u32, len: u32) -> i64` | Generate OAuth authorize URL | 82 + | `handle_callback` | `(ptr: u32, len: u32) -> i64` | Handle OAuth callback | 83 + | `refresh_tokens` | `(ptr: u32, len: u32) -> i64` | Refresh expired tokens | 84 + | `get_profile` | `(ptr: u32, len: u32) -> i64` | Get external profile info | 85 + | `sync_account` | `(ptr: u32, len: u32) -> i64` | Sync data and return records | 86 + 87 + ## Host Functions 88 + 89 + Plugins can import these host functions: 90 + 91 + | Import | Description | 92 + | ------------------- | ----------------------- | 93 + | `host_http_request` | Make HTTP requests | 94 + | `host_get_secret` | Read configured secrets | 95 + | `host_log` | Write to server logs | 96 + | `host_kv_get` | Read from KV storage | 97 + | `host_kv_set` | Write to KV storage | 98 + | `host_kv_delete` | Delete from KV storage | 99 + 100 + ## Next steps 101 + 102 + - [Official plugins repository](https://github.com/gamesgamesgamesgames/happyview-plugins) — ready-to-use plugins and the plugin SDK 103 + - [Plugins guide](plugins.md) — install and configure plugins 104 + - [API Keys](api-keys.md) — authenticate programmatic access to admin endpoints 105 + - [Permissions](permissions.md) — configure user access to plugin management
+4 -4
packages/docs/docs/guides/event-logs.md
··· 1 1 # Event Logs 2 2 3 - 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](../reference/admin-api.md#event-logs). 3 + 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](../reference/admin/events.md). 4 4 5 5 ## Event types 6 6 ··· 14 14 | `lexicon.updated` | info | Lexicon NSID | `revision`, `has_script`, `source` | 15 15 | `lexicon.deleted` | info | Lexicon NSID | — | 16 16 17 - Logged when lexicons are uploaded, updated, or deleted via the [admin API](../reference/admin-api.md#lexicons). The `actor_did` is the user who performed the action. 17 + Logged when lexicons are uploaded, updated, or deleted via the [admin API](../reference/admin/lexicons.md). The `actor_did` is the user who performed the action. 18 18 19 19 ### Record events 20 20 ··· 118 118 curl "http://localhost:3000/admin/events?limit=20&cursor=2026-03-01T11:59:00Z" -H "$AUTH" 119 119 ``` 120 120 121 - See the [Admin API reference](../reference/admin-api.md#list-event-logs) for full parameter documentation. 121 + See the [Admin API reference](../reference/admin/events.md#list-event-logs) for full parameter documentation. 122 122 123 123 ## Retention 124 124 ··· 130 130 131 131 ## Next steps 132 132 133 - - [Admin API — Event Logs](../reference/admin-api.md#event-logs) — full query parameters and response format 133 + - [Admin API — Event Logs](../reference/admin/events.md) — full query parameters and response format 134 134 - [Permissions](permissions.md) — control which users can read event logs 135 135 - [Troubleshooting](../reference/troubleshooting.md) — using event logs to diagnose issues
+8 -8
packages/docs/docs/guides/index-hooks.md
··· 2 2 3 3 Index hooks are Lua scripts that run automatically whenever a record in a collection is created, updated, or deleted on the network. They run **before** the record is indexed, giving you the ability to filter out unwanted records, transform record data before storage, or trigger side effects like syncing with external services. 4 4 5 - Unlike [query and procedure scripts](scripting.md) that run in response to XRPC requests, index hooks are triggered by the firehose. 5 + Unlike [query and procedure scripts](scripting.md) that run in response to XRPC requests, index hooks are triggered by incoming Jetstream events. 6 6 7 7 ## Attaching a hook 8 8 9 - Each record-type lexicon can have one index hook. You can add it through the [dashboard](../getting-started/dashboard.md) (click "Add Index Hook" on any record lexicon's detail page) or via the [admin API](../reference/admin-api.md#upload--upsert-a-lexicon) by including the `index_hook` field when uploading a lexicon. 9 + Each record-type lexicon can have one index hook. You can add it through the [dashboard](../getting-started/dashboard.md) (click "Add Index Hook" on any record lexicon's detail page) or via the [admin API](../reference/admin/lexicons.md#upload--upsert-a-lexicon) by including the `index_hook` field when uploading a lexicon. 10 10 11 11 ## Script structure 12 12 ··· 50 50 | `rkey` | string | The record key | 51 51 | `record` | table? | The full record as a Lua table (nil on delete) | 52 52 53 - Index hooks do **not** have access to `caller_did`, `input`, `params`, `method`, or the `Record` API. They run from the firehose, not from a user request. 53 + Index hooks do **not** have access to `caller_did`, `input`, `params`, `method`, or the `Record` API. They run from the Jetstream event stream, not from a user request. 54 54 55 55 ## Available APIs 56 56 57 57 Index hooks have access to: 58 58 59 - - **[Database API](scripting.md#database-api)** — `db.query`, `db.get`, `db.search`, `db.backlinks`, `db.count`, `db.raw` 60 - - **[HTTP API](scripting.md#http-api)** — `http.get`, `http.post`, `http.put`, `http.patch`, `http.delete`, `http.head` 61 - - **[JSON API](scripting.md#json-api)** — `json.encode`, `json.decode` 59 + - **[Database API](../reference/lua/database-api.md)** — `db.query`, `db.get`, `db.search`, `db.backlinks`, `db.count`, `db.raw` 60 + - **[HTTP API](../reference/lua/http-api.md)** — `http.get`, `http.post`, `http.put`, `http.patch`, `http.delete`, `http.head` 61 + - **[JSON API](../reference/lua/json-api.md)** — `json.encode`, `json.decode` 62 62 - **[Utility globals](scripting.md#utility-globals)** — `log()`, `now()`, `TID()`, `toarray()` 63 63 64 64 ## Error handling and retries ··· 73 73 74 74 ### Performance considerations 75 75 76 - Because hooks run synchronously before indexing, they block the firehose consumer while executing. With retry logic (1s + 2s + 4s backoff), a persistently failing hook could block for ~7 seconds per record. Keep hook scripts fast and ensure external services they depend on are reliable. 76 + Because hooks run synchronously before indexing, they block the Jetstream consumer while executing. With retry logic (1s + 2s + 4s backoff), a persistently failing hook could block for ~7 seconds per record. Keep hook scripts fast and ensure external services they depend on are reliable. 77 77 78 78 ### Dead letter table 79 79 ··· 218 218 219 219 - [Lua Scripting](scripting.md): Full reference for the sandbox, APIs, and debugging 220 220 - [Lexicons](lexicons.md): Understand how record, query, and procedure lexicons work together 221 - - [Admin API](../reference/admin-api.md#upload--upsert-a-lexicon): Upload lexicons with index hooks via the API 221 + - [Admin API — Lexicons](../reference/admin/lexicons.md#upload--upsert-a-lexicon): Upload lexicons with index hooks via the API
+3 -3
packages/docs/docs/guides/labelers.md
··· 65 65 66 66 Self-labels (applied by the record author) use an outline badge style to distinguish them from external labels. Hover over a badge to see the source labeler's DID. 67 67 68 - Labels are also available in the records API response and in Lua scripts via the `get_labels` and `get_labels_batch` functions. See the [Scripting guide](scripting.md) for details. 68 + Labels are also available in the records API response and in Lua scripts via the [`atproto.get_labels` and `atproto.get_labels_batch`](../reference/lua/atproto-api.md#atprotoget_labels) functions. 69 69 70 70 ## Permissions 71 71 ··· 77 77 78 78 ## Next steps 79 79 80 - - [Admin API reference](../reference/admin-api.md#labelers) — full endpoint documentation 81 - - [Scripting](scripting.md) — access labels in Lua scripts with `get_labels` and `get_labels_batch` 80 + - [Admin API — Labelers](../reference/admin/labelers.md) — full endpoint documentation 81 + - [AT Protocol API](../reference/lua/atproto-api.md) — access labels in Lua scripts with `get_labels` and `get_labels_batch` 82 82 - [Permissions](permissions.md) — manage user access to labeler operations
+10 -10
packages/docs/docs/guides/lexicons.md
··· 2 2 3 3 Lexicons are the core building block of HappyView. They're [AT Protocol schema definitions](https://atproto.com/specs/lexicon) that describe your data model, and HappyView uses them to decide which records to index from the network and what XRPC endpoints to serve. 4 4 5 - You don't write route handlers or database queries; you upload a lexicon and HappyView generates the infrastructure from it. There are two ways to add lexicons: uploading them via the [admin API](../reference/admin-api.md#lexicons) or [dashboard](../getting-started/dashboard.md), or fetching them directly from the AT Protocol network via [DNS authority resolution](#network-lexicons). 5 + You don't write route handlers or database queries; you upload a lexicon and HappyView generates the infrastructure from it. There are two ways to add lexicons: uploading them via the [admin API](../reference/admin/lexicons.md) or [dashboard](../getting-started/dashboard.md), or fetching them directly from the AT Protocol network via [DNS authority resolution](#network-lexicons). 6 6 7 7 ## Supported lexicon types 8 8 ··· 21 21 22 22 For example, a query lexicon `xyz.statusphere.listStatuses` would set `target_collection` to `xyz.statusphere.status` to read from that record collection. 23 23 24 - See the [admin API](../reference/admin-api.md#upload--upsert-a-lexicon) for how to set `target_collection` when uploading. 24 + See the [admin API](../reference/admin/lexicons.md#upload--upsert-a-lexicon) for how to set `target_collection` when uploading. 25 25 26 26 :::note 27 27 The `target_collection` is available in Lua scripts as the `collection` global, but it is not required if your endpoint uses a Lua script. ··· 35 35 36 36 When record-type lexicons change (uploaded or deleted), HappyView reconnects to Jetstream with an updated collection filter. HappyView always includes `com.atproto.lexicon.schema` in the filter to track network lexicon updates. 37 37 38 - Deleting a lexicon stops live indexing for that collection but does **not** remove previously indexed records from the database. To fully reset a collection's state, delete the lexicon and the associated records, re-add the lexicon, and run a [backfill](backfill.md). 38 + Deleting a lexicon stops live indexing for that collection but does **not** remove previously indexed records from the database. If you want to start fresh, you'll need to delete the records separately (e.g. via the admin API or directly in the database) before re-adding the lexicon and running a [backfill](backfill.md). 39 39 40 40 ## Network lexicons 41 41 ··· 67 67 68 68 ### Live updates via Jetstream 69 69 70 - The Jetstream subscription always includes `com.atproto.lexicon.schema` alongside the dynamic record collections. When a record event arrives: 70 + HappyView's Jetstream subscription always includes the `com.atproto.lexicon.schema` collection, so it receives real-time events whenever a lexicon schema record is created, updated, or deleted on the network. When an event arrives, HappyView checks whether the record's DID and rkey (the NSID) match any tracked network lexicon: 71 71 72 - - **create/update**: If the event's DID and rkey match a tracked network lexicon (`authority_did` and `nsid`), the lexicon is parsed, upserted into the `lexicons` table and in-memory registry, and collection filters are updated if it's a record type. 73 - - **delete**: The lexicon is removed from the `lexicons` table and registry. 72 + - **create/update**: The new schema is parsed and upserted into the `lexicons` table and the in-memory registry. If it's a record-type lexicon, Jetstream collection filters are updated to include the new collection. 73 + - **delete**: The lexicon is removed from the `lexicons` table and registry, and collection filters are updated accordingly. 74 74 75 75 ### Startup re-fetch 76 76 ··· 78 78 79 79 ## XRPC routing for unknown methods 80 80 81 - When a client calls `/xrpc/{method}` and HappyView has a local lexicon (with a handler or Lua script) for that NSID, the request is served locally. Otherwise, HappyView proxies the request to the method's **home authority** using the same DNS-based authority resolution described above: 81 + When a client calls `/xrpc/{method}` and HappyView has a local lexicon for that NSID, the request is handled by the lexicon's Lua script (or HappyView's default behavior if no script is attached). Otherwise, HappyView attempts to proxy the request to the method's **home authority** using the same DNS-based authority resolution described above: 82 82 83 83 1. Extract the authority from the NSID (all segments except the last). `com.example.foo.getBar` → authority `com.example.foo`. 84 84 2. Reverse it to form a domain: `foo.example.com`. ··· 90 90 91 91 - HappyView does **not** proxy to the reversed hostname directly. `foo.example.com` is only the DNS host for the TXT record — the actual XRPC request goes to whatever PDS endpoint the authority DID resolves to. 92 92 - Proxying applies equally to queries and procedures. For procedures, HappyView uses the caller's OAuth session to attach a DPoP-bound access token (see [Authentication](../getting-started/authentication.md#proxying-procedures-to-the-users-pds)). 93 - - If authority resolution fails — no TXT record, unresolvable DID, or the target PDS 404s the method — the client gets an error back. HappyView does not fall back to any other routing strategy. 94 - - "Network lexicons" (lexicons you've explicitly tracked via the dashboard) are only about **indexing** record collections and keeping the schema up to date. They don't add handler logic. An unknown query against a tracked network lexicon still proxies out — it doesn't run against your local record table unless you also upload a local query lexicon with a matching `target_collection`. 93 + - If authority resolution fails — no TXT record, unresolvable DID, or the target PDS doesn't support the method — the client gets an error back. HappyView does not fall back to any other routing strategy. 94 + - Tracking a network lexicon does **not** make HappyView handle requests for that NSID locally. Network lexicons are only about indexing record collections and keeping the schema up to date. If a client calls a query NSID that you've tracked as a network lexicon but haven't uploaded a local query lexicon for, HappyView still proxies the request out — it won't query your local record table. To serve a method locally, upload a local query or procedure lexicon with a matching `target_collection`. 95 95 96 - In short: if you want to serve an XRPC method on your instance, you need a local lexicon for it. Otherwise HappyView acts as a pass-through to the method's home PDS. 96 + In short: if you want to serve an XRPC method on your instance, you need a local lexicon for it. Otherwise HappyView attempts to proxy to the method's home authority. 97 97 98 98 ## Next steps 99 99
+31 -31
packages/docs/docs/guides/permissions.md
··· 8 8 9 9 ### Lexicons 10 10 11 - | Permission | Description | 12 - |---|---| 11 + | Permission | Description | 12 + | ----------------- | ---------------------------------------------- | 13 13 | `lexicons:create` | Upload and upsert lexicons (local and network) | 14 - | `lexicons:read` | List and view lexicon details | 15 - | `lexicons:delete` | Delete lexicons | 14 + | `lexicons:read` | List and view lexicon details | 15 + | `lexicons:delete` | Delete lexicons | 16 16 17 17 ### Records 18 18 19 - | Permission | Description | 20 - |---|---| 21 - | `records:read` | List and view indexed records | 22 - | `records:delete` | Delete individual records | 19 + | Permission | Description | 20 + | --------------------------- | --------------------------------------- | 21 + | `records:read` | List and view indexed records | 22 + | `records:delete` | Delete individual records | 23 23 | `records:delete-collection` | Bulk-delete all records in a collection | 24 24 25 25 ### Script Variables 26 26 27 - | Permission | Description | 28 - |---|---| 29 - | `script-variables:create` | Create and update script variables | 30 - | `script-variables:read` | List script variables (values are masked) | 31 - | `script-variables:delete` | Delete script variables | 27 + | Permission | Description | 28 + | ------------------------- | ----------------------------------------- | 29 + | `script-variables:create` | Create and update script variables | 30 + | `script-variables:read` | List script variables (values are masked) | 31 + | `script-variables:delete` | Delete script variables | 32 32 33 33 ### Users 34 34 35 - | Permission | Description | 36 - |---|---| 37 - | `users:create` | Add new users | 38 - | `users:read` | List and view user details | 39 - | `users:update` | Modify user permissions | 40 - | `users:delete` | Remove users | 35 + | Permission | Description | 36 + | -------------- | -------------------------- | 37 + | `users:create` | Add new users | 38 + | `users:read` | List and view user details | 39 + | `users:update` | Modify user permissions | 40 + | `users:delete` | Remove users | 41 41 42 42 ### API Keys 43 43 44 - | Permission | Description | 45 - |---|---| 44 + | Permission | Description | 45 + | ----------------- | ------------------- | 46 46 | `api-keys:create` | Create new API keys | 47 - | `api-keys:read` | List API keys | 48 - | `api-keys:delete` | Revoke API keys | 47 + | `api-keys:read` | List API keys | 48 + | `api-keys:delete` | Revoke API keys | 49 49 50 50 ### Operations 51 51 52 - | Permission | Description | 53 - |---|---| 54 - | `backfill:create` | Start backfill jobs | 55 - | `backfill:read` | View backfill job status | 56 - | `stats:read` | View record statistics | 57 - | `events:read` | Query the event log | 52 + | Permission | Description | 53 + | ----------------- | ------------------------ | 54 + | `backfill:create` | Start backfill jobs | 55 + | `backfill:read` | View backfill job status | 56 + | `stats:read` | View record statistics | 57 + | `events:read` | Query the event log | 58 58 59 59 ## Permission templates 60 60 ··· 91 91 - Cannot be deleted 92 92 - Cannot have their permissions modified by other users 93 93 94 - There is always exactly one super user. Super status can be transferred to another user via the transfer endpoint. 94 + There is always exactly one super user. Super status can be transferred to another user via the dashboard or transfer endpoint in the Admin API. 95 95 96 96 ## Escalation guards 97 97 ··· 132 132 - `PATCH /admin/users/{id}/permissions` — grant or revoke individual permissions 133 133 - `POST /admin/users/transfer-super` — transfer super user status (super user only) 134 134 135 - See the [Admin API reference](../reference/admin-api.md#user-management) for full details. 135 + See the [Admin API — Users](../reference/admin/users.md) for full details. 136 136 137 137 ## Next steps 138 138
+4 -99
packages/docs/docs/guides/plugins.md
··· 1 1 # Plugins 2 2 3 - HappyView uses WASM plugins to integrate with external platforms. Auth plugins enable users to link their accounts from platforms like Steam, Xbox, itch.io, and others, then sync data (like game libraries) to their AT Protocol identity. 3 + HappyView uses WASM plugins to extend its functionality. Plugins can integrate with external platforms, sync data to users' AT Protocol identities, and more. Auth plugins — the first supported plugin type — enable users to link accounts from platforms like Steam, Xbox, itch.io, and others, then sync data like game libraries. 4 4 5 5 Official plugins for Steam, Xbox, itch.io, and other platforms are available in the [happyview-plugins](https://github.com/gamesgamesgamesgames/happyview-plugins) repository. 6 6 ··· 46 46 **Requires:** `TOKEN_ENCRYPTION_KEY` environment variable (base64-encoded 32-byte key). 47 47 48 48 Generate one with: 49 + 49 50 ```bash 50 51 openssl rand -base64 32 51 52 ``` ··· 60 61 PLUGIN_XBOX_CLIENT_SECRET=your-client-secret 61 62 ``` 62 63 63 - Dashboard-configured secrets take precedence over environment variables. 64 - 65 - ## Plugin Manifest 66 - 67 - Each plugin has a `manifest.json` that describes its metadata: 68 - 69 - ```json 70 - { 71 - "id": "steam", 72 - "name": "Steam", 73 - "version": "1.0.0", 74 - "api_version": "1", 75 - "description": "Import your Steam game library and playtime data.", 76 - "icon_url": "https://example.com/steam-icon.png", 77 - "auth_type": "openid", 78 - "wasm_file": "steam.wasm", 79 - "required_secrets": [ 80 - { 81 - "key": "PLUGIN_STEAM_API_KEY", 82 - "name": "Steam Web API Key", 83 - "description": "Get your API key at steamcommunity.com/dev/apikey" 84 - } 85 - ] 86 - } 87 - ``` 88 - 89 - | Field | Description | 90 - |-------|-------------| 91 - | `id` | Unique plugin identifier | 92 - | `name` | Display name | 93 - | `version` | Semantic version | 94 - | `api_version` | Plugin API version (currently "1") | 95 - | `description` | Brief description shown during install | 96 - | `icon_url` | Optional icon URL | 97 - | `auth_type` | Authentication type: `oauth2`, `openid`, or `api_key` | 98 - | `wasm_file` | WASM binary filename (default: `plugin.wasm`) | 99 - | `required_secrets` | Array of secrets the plugin needs | 100 - 101 - ## API Endpoints 102 - 103 - ### Public Endpoints 104 - 105 - | Endpoint | Description | 106 - |----------|-------------| 107 - | `GET /external-auth/providers` | List available auth providers | 108 - | `GET /external-auth/accounts` | List user's linked accounts | 109 - | `GET /external-auth/{plugin}/authorize` | Start OAuth flow | 110 - | `GET /external-auth/{plugin}/callback` | OAuth callback handler | 111 - | `POST /external-auth/{plugin}/sync` | Sync data from linked account | 112 - | `POST /external-auth/{plugin}/unlink` | Unlink account | 113 - | `POST /external-auth/{plugin}/connect` | Connect with API key (for `api_key` auth type) | 114 - 115 - ### Admin Endpoints 116 - 117 - | Endpoint | Description | 118 - |----------|-------------| 119 - | `GET /admin/plugins` | List installed plugins | 120 - | `POST /admin/plugins` | Install a plugin | 121 - | `POST /admin/plugins/preview` | Preview plugin before installing | 122 - | `GET /admin/plugins/official` | Browse the official plugin registry catalog | 123 - | `DELETE /admin/plugins/{id}` | Remove a plugin | 124 - | `POST /admin/plugins/{id}/reload` | Reload plugin from source | 125 - | `POST /admin/plugins/{id}/check-update` | Check whether a newer version is available | 126 - | `GET /admin/plugins/{id}/secrets` | Get configured secrets (masked) | 127 - | `PUT /admin/plugins/{id}/secrets` | Update plugin secrets | 128 - 129 - The dashboard's **Settings > Plugins** page calls `GET /admin/plugins/official` to populate the install browser, and `POST /admin/plugins/{id}/check-update` to display update badges on installed plugins. 64 + These are only necessary if you can't configure variables via the dashboard. Dashboard-configured secrets take precedence over environment variables. 130 65 131 66 ## Security 132 67 ··· 136 71 - **Scoped storage**: Plugin KV storage is isolated per-plugin and per-user 137 72 - **No filesystem access**: Plugins cannot access the host filesystem 138 73 139 - ## Developing Plugins 140 - 141 - See the [happyview-plugins](https://github.com/gamesgamesgamesgames/happyview-plugins) repository for examples and the plugin SDK. 142 - 143 - ### Plugin Exports 144 - 145 - Plugins must export these functions: 146 - 147 - | Export | Signature | Description | 148 - |--------|-----------|-------------| 149 - | `alloc` | `(size: u32) -> u32` | Allocate memory | 150 - | `dealloc` | `(ptr: u32, size: u32)` | Deallocate memory | 151 - | `get_authorize_url` | `(ptr: u32, len: u32) -> i64` | Generate OAuth authorize URL | 152 - | `handle_callback` | `(ptr: u32, len: u32) -> i64` | Handle OAuth callback | 153 - | `refresh_tokens` | `(ptr: u32, len: u32) -> i64` | Refresh expired tokens | 154 - | `get_profile` | `(ptr: u32, len: u32) -> i64` | Get external profile info | 155 - | `sync_account` | `(ptr: u32, len: u32) -> i64` | Sync data and return records | 156 - 157 - ### Host Functions 158 - 159 - Plugins can import these host functions: 160 - 161 - | Import | Description | 162 - |--------|-------------| 163 - | `host_http_request` | Make HTTP requests | 164 - | `host_get_secret` | Read configured secrets | 165 - | `host_log` | Write to server logs | 166 - | `host_kv_get` | Read from KV storage | 167 - | `host_kv_set` | Write to KV storage | 168 - | `host_kv_delete` | Delete from KV storage | 169 - 170 74 ## Next steps 171 75 76 + - [Developing Plugins](developing-plugins.md) — create your own plugins with the WASM plugin API 172 77 - [Official plugins repository](https://github.com/gamesgamesgamesgames/happyview-plugins) — ready-to-use plugins for Steam, Xbox, itch.io, and more 173 78 - [API Keys](api-keys.md) — authenticate programmatic access to admin endpoints 174 79 - [Permissions](permissions.md) — configure user access to plugin management
+9 -10
packages/docs/docs/guides/postgres-to-sqlite-migration.md
··· 46 46 47 47 ### What the codemod converts automatically 48 48 49 - - `$1`, `$2`, etc. parameter placeholders to `?` positional parameters 50 - - `jsonb` operators (`->`, `->>`, `@>`, `?`) to SQLite `json_extract()` calls 49 + - `$1`, `$2`, etc. parameter placeholders to `?` 50 + - JSON operators (`->`, `->>`) and `::jsonb` casts to `json_extract()` calls 51 51 - `ILIKE` to `LIKE` (SQLite `LIKE` is case-insensitive for ASCII by default) 52 52 - `NOW()` to `datetime('now')` 53 - - `::text`, `::integer`, etc. type casts to SQLite equivalents (`CAST(... AS ...)`) 54 - - `COALESCE` and other standard SQL functions (no change needed) 53 + - `NOW() + INTERVAL '...'` / `NOW() - INTERVAL '...'` to `datetime('now', '...')` 55 54 - `TRUE`/`FALSE` literals to `1`/`0` 56 - - `RETURNING *` clauses (removed, as SQLite has limited RETURNING support) 57 55 58 56 ### What it flags for manual review 59 57 60 58 The tool prints warnings for patterns it cannot convert automatically: 61 59 62 - - Complex Postgres-specific functions (`array_agg`, `string_agg`, `generate_series`, etc.) 63 - - Window functions with Postgres-specific syntax 64 - - `ON CONFLICT` clauses with complex conditions 65 - - CTEs (`WITH` queries) that use Postgres-specific features 66 - - Any SQL that the parser cannot confidently transform 60 + - JSONB `?` (contains-key) operator — consider using `json_each()` with an `EXISTS` subquery 61 + - `make_interval()` — Postgres-specific, needs manual conversion 62 + - `SIMILAR TO` — use `LIKE` or `GLOB` instead 63 + - `ANY()` / `ALL()` array operators — no direct SQLite equivalent 64 + - Type casts other than `::jsonb` (e.g., `::text`, `::integer`) — may need manual conversion to `CAST(... AS ...)` 67 65 68 66 Review the flagged lines and update them manually. 69 67 ··· 87 85 88 86 ## Next steps 89 87 88 + - [SQLite → Postgres migration](sqlite-to-postgres-migration.md) — migrate in the opposite direction 90 89 - [Database setup](database-setup.md) — choose between SQLite and Postgres for new instances 91 90 - [Backfill](backfill.md) — re-index records from the network after switching backends 92 91 - [Lua scripting](scripting.md) — write SQL that works against either backend
+21 -417
packages/docs/docs/guides/scripting.md
··· 35 35 36 36 An instruction limit of 1,000,000 prevents infinite loops. Exceeding it terminates the script with an error. 37 37 38 + See the [Standard Libraries](../reference/lua/standard-libraries.md) reference for the full list of available Lua modules and builtins. 39 + 38 40 ## Context globals 39 41 40 42 These globals are set automatically before `handle()` is called. ··· 85 87 86 88 The `Record` API is only available in **procedure** scripts. It handles creating, updating, loading, and deleting AT Protocol records. Writes are proxied to the caller's PDS and indexed locally. 87 89 88 - ### Constructor 89 - 90 - ```lua 91 - local r = Record("xyz.statusphere.status", { status = "\ud83d\ude0a", createdAt = now() }) 92 - ``` 93 - 94 - Creates a new record instance for the given collection. The optional second argument sets initial field values. The record's `_key_type` is automatically set from the lexicon's `key` definition. Default values from the schema are populated for any missing fields. 90 + See the full [Record API reference](../reference/lua/record-api.md) for constructor, static methods, instance methods, fields, schema validation, and save behavior. 95 91 96 - ### Static methods 92 + Quick example: 97 93 98 94 ```lua 99 - -- Save multiple records in parallel 100 - Record.save_all({ record1, record2, record3 }) 101 - 102 - -- Load a record from the local database by AT URI 103 - local r = Record.load("at://did:plc:abc/xyz.statusphere.status/abc123") 104 - -- Returns nil if not found 105 - 106 - -- Load multiple records in parallel 107 - local records = Record.load_all({ uri1, uri2 }) 108 - -- Returns nil entries for URIs not found 95 + function handle() 96 + local r = Record(collection, input) 97 + r:save() 98 + return { uri = r._uri, cid = r._cid } 99 + end 109 100 ``` 110 101 111 - ### Instance methods 112 - 113 - ```lua 114 - -- Save (creates or updates depending on whether _uri is set) 115 - r:save() 116 - 117 - -- Delete from PDS and local database 118 - r:delete() 119 - 120 - -- Set the record key type (tid, any, nsid, or literal:*) 121 - r:set_key_type("tid") 122 - 123 - -- Set a specific record key 124 - r:set_rkey("my-key") 125 - 126 - -- Auto-generate a record key based on _key_type 127 - local key = r:generate_rkey() 128 - ``` 129 - 130 - **Key type behavior for `generate_rkey()`:** 131 - 132 - | Key type | Generated rkey | 133 - | --------------- | --------------------------------- | 134 - | `tid` | Sortable timestamp-based ID | 135 - | `any` | Same as `tid` | 136 - | `literal:value` | The literal value after the colon | 137 - | `nsid` | Error — use `set_rkey()` instead | 138 - 139 - ### Instance fields 140 - 141 - These fields are set automatically and are read-only (writes raise an error): 142 - 143 - | Field | Type | Description | 144 - | ------------- | ------- | ----------------------------------------------------------- | 145 - | `_uri` | string? | AT URI — set after `save()`, cleared after `delete()` | 146 - | `_cid` | string? | Content hash — set after `save()`, cleared after `delete()` | 147 - | `_key_type` | string? | Record key type from the lexicon definition | 148 - | `_rkey` | string? | Record key — set via `set_rkey()` or `generate_rkey()` | 149 - | `_collection` | string | Collection NSID (always set) | 150 - | `_schema` | table? | Schema definition from the lexicon (used for validation) | 151 - 152 - ### Schema validation 153 - 154 - When a record has a schema (loaded from the lexicon): 155 - 156 - - **On save:** required fields are checked, and missing required fields raise an error 157 - - **On construction:** default values from schema properties are auto-populated 158 - - **On save:** only fields defined in the schema's `properties` are sent to the PDS 159 - 160 - ### Save behavior 161 - 162 - `r:save()` auto-detects create vs update: 163 - 164 - - If `_uri` is nil → calls `createRecord` on the PDS 165 - - If `_uri` is set → calls `putRecord` on the PDS 166 - 167 - After a successful save, `_uri` and `_cid` are updated on the record instance. 168 - 169 102 ## Database API 170 103 171 104 The `db` table provides access to the database. Available in both queries and procedures. 172 105 173 - ### db.query 106 + See the full [Database API reference](../reference/lua/database-api.md) for `db.query`, `db.get`, `db.search`, `db.backlinks`, `db.count`, and `db.raw`. 174 107 175 - ```lua 176 - local result = db.query({ 177 - collection = "xyz.statusphere.status", -- required 178 - did = "did:plc:abc", -- optional: filter by DID 179 - limit = 20, -- optional: max 100, default 20 180 - offset = 0, -- optional: for pagination 181 - sort = "name", -- optional: field to sort by, default "indexed_at" 182 - sortDirection = "asc", -- optional: "asc" or "desc", default "desc" 183 - }) 184 - 185 - -- result.records — array of record tables (each includes a "uri" field) 186 - -- result.cursor — present when more records exist 187 - ``` 188 - 189 - The `sort` field can be a top-level column (`indexed_at`, `did`, `uri`) or any field inside the record's `value` object (e.g. `name`, `createdAt`). Field names must contain only alphanumeric characters and underscores. 190 - 191 - ### db.get 108 + Quick example: 192 109 193 110 ```lua 194 - local record = db.get("at://did:plc:abc/xyz.statusphere.status/abc123") 195 - -- Returns the record table or nil 196 - -- The returned table includes a "uri" field 197 - ``` 198 - 199 - ### db.search 200 - 201 - ```lua 202 - local result = db.search({ 203 - collection = "xyz.statusphere.status", -- required 204 - field = "displayName", -- required: record field to search 205 - query = "alice", -- required: search term 206 - limit = 10, -- optional: max 100, default 10 207 - }) 208 - 209 - -- result.records — array of matching records, ranked by relevance: 210 - -- exact match > prefix match > contains match, then alphabetical 211 - ``` 212 - 213 - ### db.backlinks 214 - 215 - Find records that reference a given AT URI anywhere in their data. Useful for finding likes on a post, replies to a thread, or any record that links to another. 216 - 217 - ```lua 218 - local result = db.backlinks({ 219 - collection = "xyz.statusphere.status", -- required 220 - uri = "at://did:plc:abc/xyz.statusphere.status/foo", -- required: the URI to find references to 221 - did = "did:plc:abc", -- optional: filter by DID 222 - limit = 20, -- optional: max 100, default 20 223 - offset = 0, -- optional: for pagination 224 - }) 225 - 226 - -- result.records — array of records whose data contains the given URI 227 - -- result.cursor — present when more records exist 228 - ``` 229 - 230 - The search checks the full record data, so it works regardless of which field holds the reference (`subject`, `parent`, `reply.root`, etc.). 231 - 232 - ### db.count 233 - 234 - ```lua 235 - local n = db.count("xyz.statusphere.status") 236 - local n = db.count("xyz.statusphere.status", "did:plc:abc") -- filter by DID 237 - ``` 238 - 239 - ### db.raw 240 - 241 - Run a raw SQL query against the database. Supports `SELECT`, `INSERT`, `UPDATE`, `DELETE`, and `CREATE TABLE` statements. 242 - 243 - ```lua 244 - -- Read query 245 - local rows = db.raw( 246 - "SELECT uri, did, record FROM records WHERE collection = $1 AND did = $2 LIMIT $3", 247 - { "xyz.statusphere.status", "did:plc:abc", 10 } 248 - ) 249 - 250 - for _, row in ipairs(rows) do 251 - -- row.uri, row.did, row.record (JSONB is returned as a Lua table) 111 + function handle() 112 + local result = db.query({ collection = collection, limit = 20 }) 113 + return { records = result.records, cursor = result.cursor } 252 114 end 253 - 254 - -- Write query (returns affected rows, if any) 255 - db.raw("CREATE TABLE IF NOT EXISTS my_table (id TEXT PRIMARY KEY, value TEXT NOT NULL)") 256 - db.raw("INSERT INTO my_table (id, value) VALUES ($1, $2) ON CONFLICT (id) DO UPDATE SET value = $2", 257 - { "key1", "hello" }) 258 115 ``` 259 116 260 - Parameters are passed as an array and bound to `$1`, `$2`, etc. Supported parameter types: strings, integers, numbers, booleans, and nil. 261 - 262 - Column types are mapped automatically: 263 - 264 - | Postgres type | Lua type | 265 - | ---------------------- | -------- | 266 - | `TEXT`, `VARCHAR` | string | 267 - | `INT4`, `INT8` | integer | 268 - | `FLOAT4`, `FLOAT8` | number | 269 - | `BOOL` | boolean | 270 - | `JSON`, `JSONB` | table | 271 - | `TIMESTAMPTZ` | string (ISO 8601) | 272 - | Other | string (fallback) | 273 - 274 117 ## HTTP API 275 118 276 119 The `http` table provides async HTTP client functions. Available in both queries and procedures. 277 120 278 - ### Methods 279 - 280 - All methods take a URL and an optional options table, and return a [response table](#response). 281 - 282 - ```lua 283 - http.get(url, opts?) 284 - http.post(url, opts?) 285 - http.put(url, opts?) 286 - http.patch(url, opts?) 287 - http.delete(url, opts?) 288 - http.head(url, opts?) 289 - ``` 290 - 291 - ### Options 292 - 293 - The optional second argument is a table with: 121 + See the full [HTTP API reference](../reference/lua/http-api.md) for all methods, options, and response format. 294 122 295 - | Field | Type | Description | 296 - | --------- | ------ | ---------------------------------------------- | 297 - | `headers` | table | Request headers as key-value string pairs | 298 - | `body` | string | Request body (ignored for GET and HEAD) | 299 - 300 - ### Response 301 - 302 - Every method returns a table with: 303 - 304 - | Field | Type | Description | 305 - | --------- | ------- | ---------------------------------------------------- | 306 - | `status` | integer | HTTP status code | 307 - | `body` | string | Response body text (empty string for HEAD) | 308 - | `headers` | table | Response headers as key-value pairs (lowercase keys) | 309 - 310 - ### Examples 123 + Quick example: 311 124 312 125 ```lua 313 - -- Simple GET 314 126 local resp = http.get("https://api.example.com/data") 315 - -- resp.status = 200, resp.body = "...", resp.headers["content-type"] = "application/json" 316 - 317 - -- GET with custom headers 318 - local resp = http.get("https://api.example.com/data", { 319 - headers = { ["authorization"] = "Bearer token123" } 320 - }) 321 - 322 - -- POST with JSON body 323 - local resp = http.post("https://api.example.com/hook", { 324 - body = '{"key": "value"}', 325 - headers = { ["content-type"] = "application/json" } 326 - }) 327 - 328 - -- PUT, PATCH, DELETE, HEAD follow the same pattern 329 - local resp = http.put(url, { body = data, headers = { ... } }) 330 - local resp = http.patch(url, { body = data, headers = { ... } }) 331 - local resp = http.delete(url, { headers = { ... } }) 332 - local resp = http.head(url) 127 + local data = json.decode(resp.body) 333 128 ``` 334 129 335 130 ## AT Protocol API 336 131 337 - The `atproto` table provides AT Protocol utility functions. Available in queries, procedures, and [index hooks](index-hooks.md). 338 - 339 - ### atproto.resolve_service_endpoint 340 - 341 - ```lua 342 - local endpoint = atproto.resolve_service_endpoint(did) 343 - ``` 344 - 345 - Resolves a DID to its AT Protocol service endpoint URL by fetching the DID document. Supports both `did:plc:*` (via the PLC directory) and `did:web:*` (via `.well-known/did.json`). 346 - 347 - | Parameter | Type | Description | 348 - | --------- | ------ | ------------------------ | 349 - | `did` | string | The DID to resolve | 350 - 351 - **Returns:** The service endpoint URL as a string, or `nil` if resolution fails (DID not found, no PDS service in document, network error). 352 - 353 - ### Examples 354 - 355 - ```lua 356 - -- Resolve a did:plc DID 357 - local endpoint = atproto.resolve_service_endpoint("did:plc:abc123") 358 - -- endpoint = "https://pds.example.com" 359 - 360 - -- Resolve a did:web DID 361 - local endpoint = atproto.resolve_service_endpoint("did:web:example.com") 362 - -- endpoint = "https://example.com" 363 - 364 - -- Handle resolution failure 365 - local endpoint = atproto.resolve_service_endpoint("did:plc:unknown") 366 - if not endpoint then 367 - return { error = "Could not resolve DID" } 368 - end 369 - 370 - -- Use with HTTP API to call a remote XRPC endpoint 371 - local endpoint = atproto.resolve_service_endpoint(did) 372 - if endpoint then 373 - local resp = http.get(endpoint .. "/xrpc/com.example.method") 374 - local data = json.decode(resp.body) 375 - end 376 - ``` 377 - 378 - ### atproto.get_labels 379 - 380 - ```lua 381 - local labels = atproto.get_labels(uri) 382 - ``` 383 - 384 - Returns an array of labels for a single AT URI. Merges external labels (from subscribed labelers) with self-labels (from the record's `labels.values[]` field). 385 - 386 - | Parameter | Type | Description | 387 - | --------- | ------ | ------------------------------ | 388 - | `uri` | string | AT URI of the record to query | 132 + The `atproto` table provides AT Protocol utility functions like DID resolution and label queries. 389 133 390 - Each label in the array is a table with: 391 - 392 - | Field | Type | Description | 393 - | ----- | ------ | ---------------------------------------- | 394 - | `src` | string | DID of the labeler (or record author) | 395 - | `uri` | string | AT URI this label applies to | 396 - | `val` | string | Label value (e.g. "nsfw", "!hide") | 397 - | `cts` | string | Timestamp when the label was created | 398 - 399 - Expired labels are automatically filtered out. Returns an empty array if no labels exist. 400 - 401 - ### atproto.get_labels_batch 402 - 403 - ```lua 404 - local labels_by_uri = atproto.get_labels_batch(uris) 405 - ``` 406 - 407 - Batch version of `get_labels`. Takes an array of AT URIs and returns a table keyed by URI, where each value is an array of labels. 408 - 409 - | Parameter | Type | Description | 410 - | --------- | ----- | ------------------------ | 411 - | `uris` | table | Array of AT URI strings | 412 - 413 - **Returns:** A table keyed by URI. Each value is an array of label tables (same shape as `get_labels`). URIs with no labels have an empty array. 414 - 415 - ### Label Examples 416 - 417 - ```lua 418 - -- Get labels for a single game 419 - local labels = atproto.get_labels("at://did:plc:abc/games.gamesgamesgamesgames.game/rkey1") 420 - for _, label in ipairs(labels) do 421 - if label.val == "!hide" then 422 - -- skip this game in feed results 423 - end 424 - end 425 - 426 - -- Batch fetch labels for multiple games (efficient for feed hydration) 427 - local uris = {} 428 - for _, item in ipairs(skeleton) do 429 - uris[#uris + 1] = item.game 430 - end 431 - 432 - local labels_by_uri = atproto.get_labels_batch(uris) 433 - for _, uri in ipairs(uris) do 434 - local labels = labels_by_uri[uri] 435 - for _, label in ipairs(labels) do 436 - if label.val == "!hide" then 437 - -- filter out this game 438 - end 439 - end 440 - end 441 - ``` 134 + See the full [AT Protocol API reference](../reference/lua/atproto-api.md) for `atproto.resolve_service_endpoint`, `atproto.get_labels`, and `atproto.get_labels_batch`. 442 135 443 136 ## JSON API 444 137 445 - The `json` global provides JSON serialization and deserialization. Available in queries, procedures, and [index hooks](index-hooks.md). 446 - 447 - ### json.encode 448 - 449 - ```lua 450 - local str = json.encode({ key = "value", items = { 1, 2, 3 } }) 451 - -- '{"key":"value","items":[1,2,3]}' 452 - ``` 138 + The `json` global provides JSON serialization and deserialization. 453 139 454 - Converts a Lua table to a JSON string. 455 - 456 - ### json.decode 457 - 458 - ```lua 459 - local tbl = json.decode('{"key": "value"}') 460 - -- tbl.key == "value" 461 - ``` 462 - 463 - Parses a JSON string into a Lua table. Returns an error if the input is not valid JSON. 464 - 465 - ## Standard libraries 466 - 467 - The following Lua 5.4 standard library modules are available: 468 - 469 - <details> 470 - <summary> 471 - `string` 472 - </summary> 473 - - [`byte`](https://lua.org/manual/5.4/manual.html#pdf-string.byte) 474 - - [`char`](https://lua.org/manual/5.4/manual.html#pdf-string.char) 475 - - [`find`](https://lua.org/manual/5.4/manual.html#pdf-string.find) 476 - - [`format`](https://lua.org/manual/5.4/manual.html#pdf-string.format) 477 - - [`gmatch`](https://lua.org/manual/5.4/manual.html#pdf-string.gmatch) 478 - - [`gsub`](https://lua.org/manual/5.4/manual.html#pdf-string.gsub) 479 - - [`len`](https://lua.org/manual/5.4/manual.html#pdf-string.len) 480 - - [`lower`](https://lua.org/manual/5.4/manual.html#pdf-string.lower) 481 - - [`match`](https://lua.org/manual/5.4/manual.html#pdf-string.match) 482 - - [`rep`](https://lua.org/manual/5.4/manual.html#pdf-string.rep) 483 - - [`reverse`](https://lua.org/manual/5.4/manual.html#pdf-string.reverse) 484 - - [`sub`](https://lua.org/manual/5.4/manual.html#pdf-string.sub) 485 - - [`upper`](https://lua.org/manual/5.4/manual.html#pdf-string.upper) 486 - </details> 487 - 488 - <details> 489 - <summary> 490 - `table` 491 - </summary> 492 - - [`concat`](https://lua.org/manual/5.4/manual.html#pdf-table.concat) 493 - - [`insert`](https://lua.org/manual/5.4/manual.html#pdf-table.insert) 494 - - [`remove`](https://lua.org/manual/5.4/manual.html#pdf-table.remove) 495 - - [`sort`](https://lua.org/manual/5.4/manual.html#pdf-table.sort) 496 - - [`unpack`](https://lua.org/manual/5.4/manual.html#pdf-table.unpack) 497 - </details> 498 - 499 - <details> 500 - <summary> 501 - `math` 502 - </summary> 503 - - [`abs`](https://lua.org/manual/5.4/manual.html#pdf-math.abs) 504 - - [`ceil`](https://lua.org/manual/5.4/manual.html#pdf-math.ceil) 505 - - [`floor`](https://lua.org/manual/5.4/manual.html#pdf-math.floor) 506 - - [`max`](https://lua.org/manual/5.4/manual.html#pdf-math.max) 507 - - [`min`](https://lua.org/manual/5.4/manual.html#pdf-math.min) 508 - - [`random`](https://lua.org/manual/5.4/manual.html#pdf-math.random) 509 - - [`sqrt`](https://lua.org/manual/5.4/manual.html#pdf-math.sqrt) 510 - - [`huge`](https://lua.org/manual/5.4/manual.html#pdf-math.huge) 511 - - [`pi`](https://lua.org/manual/5.4/manual.html#pdf-math.pi) 512 - </details> 513 - 514 - <details> 515 - <summary> 516 - Standard builtins 517 - </summary> 518 - - [`print`](https://lua.org/manual/5.4/manual.html#pdf-print) 519 - - [`tostring`](https://lua.org/manual/5.4/manual.html#pdf-tostring) 520 - - [`tonumber`](https://lua.org/manual/5.4/manual.html#pdf-tonumber) 521 - - [`type`](https://lua.org/manual/5.4/manual.html#pdf-type) 522 - - [`pairs`](https://lua.org/manual/5.4/manual.html#pdf-pairs) 523 - - [`ipairs`](https://lua.org/manual/5.4/manual.html#pdf-ipairs) 524 - - [`next`](https://lua.org/manual/5.4/manual.html#pdf-next) 525 - - [`select`](https://lua.org/manual/5.4/manual.html#pdf-select) 526 - - [`unpack`](https://lua.org/manual/5.4/manual.html#pdf-table.unpack) 527 - - [`error`](https://lua.org/manual/5.4/manual.html#pdf-error) 528 - - [`pcall`](https://lua.org/manual/5.4/manual.html#pdf-pcall) 529 - - [`xpcall`](https://lua.org/manual/5.4/manual.html#pdf-xpcall) 530 - - [`assert`](https://lua.org/manual/5.4/manual.html#pdf-assert) 531 - - [`setmetatable`](https://lua.org/manual/5.4/manual.html#pdf-setmetatable) 532 - - [`getmetatable`](https://lua.org/manual/5.4/manual.html#pdf-getmetatable) 533 - - [`rawget`](https://lua.org/manual/5.4/manual.html#pdf-rawget) 534 - - [`rawset`](https://lua.org/manual/5.4/manual.html#pdf-rawset) 535 - - [`rawequal`](https://lua.org/manual/5.4/manual.html#pdf-rawequal) 536 - </details> 140 + See the full [JSON API reference](../reference/lua/json-api.md) for `json.encode` and `json.decode`. 537 141 538 142 ## Debugging 539 143
+88
packages/docs/docs/guides/sqlite-to-postgres-migration.md
··· 1 + # Migrating from SQLite to Postgres 2 + 3 + This guide covers migrating an existing HappyView deployment from SQLite to Postgres. If you are staying on SQLite, no action is required. 4 + 5 + ## Overview 6 + 7 + HappyView writes all internal SQL in SQLite syntax and translates to Postgres automatically at runtime. This means your **Lua scripts do not need any changes** when switching from SQLite to Postgres — they continue to work as-is. 8 + 9 + The main steps are: set up the Postgres database, update your environment variables, and re-index your data. 10 + 11 + ## Step 1: Set up Postgres 12 + 13 + Create a Postgres database for HappyView: 14 + 15 + ```sh 16 + createdb happyview 17 + ``` 18 + 19 + If you are using Docker Compose, uncomment the `postgres` service and `pgdata` volume in your `docker-compose.yml`. See the [database setup guide](database-setup.md#docker-compose) for details. 20 + 21 + ## Step 2: Back up your SQLite database 22 + 23 + Copy your SQLite database file before making any changes: 24 + 25 + ```sh 26 + cp data/happyview.db data/happyview.db.backup 27 + ``` 28 + 29 + ## Step 3: Update environment variables 30 + 31 + Change your `.env` to use Postgres: 32 + 33 + ```sh 34 + # Before 35 + DATABASE_URL=sqlite://data/happyview.db?mode=rwc 36 + 37 + # After 38 + DATABASE_URL=postgres://happyview:happyview@localhost/happyview 39 + ``` 40 + 41 + If you had `DATABASE_BACKEND` set, update it as well: 42 + 43 + ```sh 44 + DATABASE_BACKEND=postgres 45 + ``` 46 + 47 + ## Step 4: Start HappyView 48 + 49 + Start HappyView with the new `DATABASE_URL`. It will connect to Postgres and run migrations automatically, creating all necessary tables. 50 + 51 + ## Step 5: Re-index your data 52 + 53 + Since HappyView indexes records from the AT Protocol network, the simplest way to populate your new Postgres database is to re-run the backfill: 54 + 55 + 1. Upload your lexicons via the dashboard or admin API (or they will already be there if you exported and re-imported them) 56 + 2. Run a backfill for each collection (dashboard or `POST /admin/backfill`) 57 + 58 + Backfill fetches all records fresh from the network, so no data transfer between databases is needed. 59 + 60 + :::tip 61 + If you have many lexicons, you can export them from the old instance before switching. Use `GET /admin/lexicons` to list them and `POST /admin/lexicons` to re-upload after switching to Postgres. 62 + ::: 63 + 64 + ## Step 6: Re-create admin settings 65 + 66 + Instance settings (app name, logo, TOS/privacy URIs), API keys, users, and labeler subscriptions are stored in the database and are not carried over automatically. Re-create these via the dashboard or admin API after switching. 67 + 68 + ## Lua scripts 69 + 70 + No changes needed. Lua scripts use SQLite syntax by default, and HappyView translates to Postgres automatically at runtime. This includes: 71 + 72 + - `?` placeholders (translated to `$1`, `$2`, etc.) 73 + - `json_extract()` calls (translated to Postgres JSON operators) 74 + - `datetime('now')` (translated to `NOW()`) 75 + - Boolean literals `1`/`0` (work in both backends) 76 + 77 + If you have scripts that already use Postgres-native syntax (e.g., from direct `db.raw()` calls), they will **not** work after switching — HappyView expects SQLite syntax. Use the [codemod tool](postgres-to-sqlite-migration.md#run-the-codemod-tool) to convert them. 78 + 79 + ## Rollback 80 + 81 + To switch back to SQLite, revert your `DATABASE_URL` to the SQLite connection string. Your SQLite database file remains unchanged — HappyView does not modify it during the migration to Postgres. 82 + 83 + ## Next steps 84 + 85 + - [Postgres → SQLite migration](postgres-to-sqlite-migration.md) — migrate in the opposite direction 86 + - [Database setup](database-setup.md) — choose between SQLite and Postgres for new instances 87 + - [Backfill](backfill.md) — re-index records from the network after switching backends 88 + - [Lua scripting](scripting.md) — write SQL that works against either backend
+78 -1199
packages/docs/docs/reference/admin-api.md
··· 4 4 5 5 ## Auth 6 6 7 - The admin API supports three authentication methods: 7 + The admin API supports two authentication methods: 8 8 9 - 1. **Session cookie** (web UI) — Set during the OAuth login flow. The signed cookie contains the user's DID. 10 - 2. **API keys** — read/write tokens starting with `hv_`, passed as `Authorization: Bearer hv_...`. See the [API Keys guide](../guides/api-keys.md) for details. 11 - 3. **Service auth JWT** — AT Protocol inter-service authentication via signed JWTs. 9 + 1. **API keys** — read/write tokens starting with `hv_`, passed as `Authorization: Bearer hv_...`. See the [API Keys guide](../guides/api-keys.md) for details. 10 + 2. **Service auth JWT** — AT Protocol inter-service authentication via signed JWTs. 12 11 13 12 In all cases the resolved DID is checked against the `users` table, and the user's permissions are loaded to authorize the request. 14 13 ··· 16 15 17 16 Non-user DIDs receive a `403 Forbidden` response. Users without the required permission for a specific endpoint also receive `403 Forbidden`. 18 17 18 + ## Errors 19 + 19 20 All error responses return JSON with an `error` field: 20 21 21 22 ```json ··· 24 25 } 25 26 ``` 26 27 27 - | Status | Meaning | 28 - | ------------------ | -------------------------------------------------------------------------------------------------------------- | 29 - | `400 Bad Request` | Invalid input (missing required fields, malformed lexicon JSON) | 30 - | `401 Unauthorized` | Missing or invalid session cookie, API key, or service auth JWT | 31 - | `403 Forbidden` | Authenticated DID is not in the users table, or user lacks the required permission | 32 - | `404 Not Found` | Lexicon, user, or backfill job not found | 28 + | Status | Meaning | 29 + | ------------------ | -------------------------------------------------------------------------------------- | 30 + | `400 Bad Request` | Invalid input (missing required fields, malformed lexicon JSON) | 31 + | `401 Unauthorized` | Missing or invalid API key or service auth JWT | 32 + | `403 Forbidden` | Authenticated DID is not in the users table, or user lacks the required permission | 33 + | `404 Not Found` | Lexicon, user, or backfill job not found | 33 34 34 35 ```sh 35 36 # All examples assume $TOKEN is an API key (hv_...) 36 37 AUTH="Authorization: Bearer $TOKEN" 37 38 ``` 38 39 39 - ## Lexicons 40 - 41 - ### Upload / upsert a lexicon 42 - 43 - ``` 44 - POST /admin/lexicons 45 - ``` 46 - 47 - ```sh 48 - curl -X POST http://localhost:3000/admin/lexicons \ 49 - -H "$AUTH" \ 50 - -H "Content-Type: application/json" \ 51 - -d '{ 52 - "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" } } } } } }, 53 - "backfill": true, 54 - "target_collection": null 55 - }' 56 - ``` 57 - 58 - | Field | Type | Required | Description | 59 - | ------------------- | ------- | -------- | --------------------------------------------------------------------- | 60 - | `lexicon_json` | object | yes | Raw lexicon JSON (must have `lexicon: 1` and `id`) | 61 - | `backfill` | boolean | no | Whether uploading triggers historical backfill (default `true`) | 62 - | `target_collection` | string | no | For query/procedure lexicons, the record collection they operate on | 63 - | `script` | string | no | Lua script for query/procedure endpoints | 64 - | `index_hook` | string | no | [Index hook](../guides/index-hooks.md) Lua script for record lexicons | 65 - 66 - **Response**: `201 Created` (new) or `200 OK` (upsert) 67 - 68 - ```json 69 - { 70 - "id": "xyz.statusphere.status", 71 - "revision": 1 72 - } 73 - ``` 74 - 75 - ### List lexicons 76 - 77 - ``` 78 - GET /admin/lexicons 79 - ``` 80 - 81 - ```sh 82 - curl http://localhost:3000/admin/lexicons -H "$AUTH" 83 - ``` 84 - 85 - **Response**: `200 OK` 86 - 87 - ```json 88 - [ 89 - { 90 - "id": "xyz.statusphere.status", 91 - "revision": 1, 92 - "lexicon_type": "record", 93 - "backfill": true, 94 - "created_at": "2025-01-01T00:00:00Z", 95 - "updated_at": "2025-01-01T00:00:00Z" 96 - } 97 - ] 98 - ``` 99 - 100 - ### Get a lexicon 101 - 102 - ``` 103 - GET /admin/lexicons/{id} 104 - ``` 105 - 106 - ```sh 107 - curl http://localhost:3000/admin/lexicons/xyz.statusphere.status -H "$AUTH" 108 - ``` 109 - 110 - **Response**: `200 OK` with full lexicon details including raw JSON. 111 - 112 - ### Delete a lexicon 113 - 114 - ``` 115 - DELETE /admin/lexicons/{id} 116 - ``` 117 - 118 - ```sh 119 - curl -X DELETE http://localhost:3000/admin/lexicons/xyz.statusphere.status -H "$AUTH" 120 - ``` 121 - 122 - **Response**: `204 No Content` 123 - 124 - ## Network Lexicons 125 - 126 - Network lexicons are fetched from the AT Protocol network via DNS TXT resolution and kept updated via the Jetstream subscription. See [Lexicons - Network lexicons](../guides/lexicons.md#network-lexicons) for background. 127 - 128 - ### Add a network lexicon 129 - 130 - ``` 131 - POST /admin/network-lexicons 132 - ``` 133 - 134 - ```sh 135 - curl -X POST http://localhost:3000/admin/network-lexicons \ 136 - -H "$AUTH" \ 137 - -H "Content-Type: application/json" \ 138 - -d '{ 139 - "nsid": "xyz.statusphere.status", 140 - "target_collection": null 141 - }' 142 - ``` 143 - 144 - | Field | Type | Required | Description | 145 - | ------------------- | ------ | -------- | ------------------------------------------------------------------- | 146 - | `nsid` | string | yes | The NSID of the lexicon to watch | 147 - | `target_collection` | string | no | For query/procedure lexicons, the record collection they operate on | 148 - 149 - HappyView resolves the NSID authority via DNS TXT, fetches the lexicon from the authority's PDS, parses it, and stores it. 150 - 151 - **Response**: `201 Created` 152 - 153 - ```json 154 - { 155 - "nsid": "xyz.statusphere.status", 156 - "authority_did": "did:plc:authority", 157 - "revision": 1 158 - } 159 - ``` 160 - 161 - ### List network lexicons 162 - 163 - ``` 164 - GET /admin/network-lexicons 165 - ``` 166 - 167 - ```sh 168 - curl http://localhost:3000/admin/network-lexicons -H "$AUTH" 169 - ``` 170 - 171 - **Response**: `200 OK` 172 - 173 - ```json 174 - [ 175 - { 176 - "nsid": "xyz.statusphere.status", 177 - "authority_did": "did:plc:authority", 178 - "target_collection": null, 179 - "last_fetched_at": "2025-01-01T00:00:00Z", 180 - "created_at": "2025-01-01T00:00:00Z" 181 - } 182 - ] 183 - ``` 184 - 185 - ### Remove a network lexicon 186 - 187 - ``` 188 - DELETE /admin/network-lexicons/{nsid} 189 - ``` 190 - 191 - ```sh 192 - curl -X DELETE http://localhost:3000/admin/network-lexicons/xyz.statusphere.status \ 193 - -H "$AUTH" 194 - ``` 195 - 196 - Removes the network lexicon tracking and also deletes the lexicon from the `lexicons` table and in-memory registry. 197 - 198 - **Response**: `204 No Content` 199 - 200 - ## Stats 201 - 202 - ### Record counts 203 - 204 - ``` 205 - GET /admin/stats 206 - ``` 207 - 208 - ```sh 209 - curl http://localhost:3000/admin/stats -H "$AUTH" 210 - ``` 211 - 212 - **Response**: `200 OK` 213 - 214 - ```json 215 - { 216 - "total_records": 12345, 217 - "collections": [{ "collection": "xyz.statusphere.status", "count": 500 }] 218 - } 219 - ``` 220 - 221 - ## Backfill 222 - 223 - ### Create a backfill job 224 - 225 - ``` 226 - POST /admin/backfill 227 - ``` 228 - 229 - ```sh 230 - curl -X POST http://localhost:3000/admin/backfill \ 231 - -H "$AUTH" \ 232 - -H "Content-Type: application/json" \ 233 - -d '{ "collection": "xyz.statusphere.status" }' 234 - ``` 235 - 236 - | Field | Type | Required | Description | 237 - | ------------ | ------ | -------- | ---------------------------------------------------------- | 238 - | `collection` | string | no | Limit to a single collection (backfills all if omitted) | 239 - | `did` | string | no | Limit to a single DID (discovers all via relay if omitted) | 240 - 241 - **Response**: `201 Created` 242 - 243 - ```json 244 - { 245 - "id": "550e8400-e29b-41d4-a716-446655440000", 246 - "status": "pending" 247 - } 248 - ``` 249 - 250 - ### List backfill jobs 251 - 252 - ``` 253 - GET /admin/backfill/status 254 - ``` 255 - 256 - ```sh 257 - curl http://localhost:3000/admin/backfill/status -H "$AUTH" 258 - ``` 259 - 260 - **Response**: `200 OK` 261 - 262 - ```json 263 - [ 264 - { 265 - "id": "550e8400-e29b-41d4-a716-446655440000", 266 - "collection": "xyz.statusphere.status", 267 - "did": null, 268 - "status": "completed", 269 - "total_repos": 42, 270 - "processed_repos": 42, 271 - "total_records": 1000, 272 - "error": null, 273 - "started_at": "2025-01-01T00:01:00Z", 274 - "completed_at": "2025-01-01T00:05:00Z", 275 - "created_at": "2025-01-01T00:00:00Z" 276 - } 277 - ] 278 - ``` 279 - 280 - ## Event Logs 281 - 282 - HappyView records an audit trail of system events: lexicon changes, record operations, Lua script executions and errors, user actions, backfill jobs, and Jetstream connectivity. See the [Event Logs guide](../guides/event-logs.md) for details on event types and retention. 283 - 284 - ### List event logs 285 - 286 - ``` 287 - GET /admin/events 288 - ``` 289 - 290 - ```sh 291 - curl "http://localhost:3000/admin/events?severity=error&limit=10" -H "$AUTH" 292 - ``` 293 - 294 - | Param | Type | Required | Description | 295 - | ------------ | ------ | -------- | --------------------------------------------------------------------- | 296 - | `event_type` | string | no | Filter by exact event type (e.g. `script.error`) | 297 - | `category` | string | no | Filter by category prefix (e.g. `lexicon` matches all lexicon events) | 298 - | `severity` | string | no | Filter by severity: `info`, `warn`, or `error` | 299 - | `subject` | string | no | Filter by subject (lexicon ID, record URI, admin DID, etc.) | 300 - | `cursor` | string | no | Pagination cursor (ISO 8601 timestamp from previous response) | 301 - | `limit` | number | no | Results per page (default `50`, max `100`) | 40 + ## Endpoint groups 302 41 303 - **Response**: `200 OK` 304 - 305 - ```json 306 - { 307 - "events": [ 308 - { 309 - "id": "550e8400-e29b-41d4-a716-446655440000", 310 - "event_type": "script.error", 311 - "severity": "error", 312 - "actor_did": "did:plc:abc123", 313 - "subject": "com.example.feed.like", 314 - "detail": { 315 - "error": "attempt to index nil value", 316 - "script_source": "function handle() ... end", 317 - "input": { "status": "hello" }, 318 - "caller_did": "did:plc:abc123", 319 - "method": "com.example.feed.like" 320 - }, 321 - "created_at": "2026-03-01T12:00:00Z" 322 - } 323 - ], 324 - "cursor": "2026-03-01T11:59:00Z" 325 - } 326 - ``` 327 - 328 - Events are returned in reverse chronological order (newest first). Pass the `cursor` value from the response to fetch the next page. 329 - 330 - ## API Keys 331 - 332 - Manage API keys for programmatic access. See the [API Keys guide](../guides/api-keys.md) for usage details. 333 - 334 - ### Create an API key 335 - 336 - ``` 337 - POST /admin/api-keys 338 - ``` 339 - 340 - Requires `api-keys:create` permission. 341 - 342 - ```sh 343 - curl -X POST http://localhost:3000/admin/api-keys \ 344 - -H "$AUTH" \ 345 - -H "Content-Type: application/json" \ 346 - -d '{ 347 - "name": "CI Deploy", 348 - "permissions": ["lexicons:read", "lexicons:create", "backfill:create"] 349 - }' 350 - ``` 351 - 352 - | Field | Type | Required | Description | 353 - | ------------- | -------- | -------- | -------------------------------------------------------------------------------------------- | 354 - | `name` | string | yes | A label to identify this key's usage | 355 - | `permissions` | string[] | yes | Permissions to grant the key (must be a subset of the creating user's own permissions) | 356 - 357 - **Response**: `201 Created` 358 - 359 - ```json 360 - { 361 - "id": "550e8400-e29b-41d4-a716-446655440000", 362 - "name": "CI Deploy", 363 - "key": "hv_a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4", 364 - "key_prefix": "hv_a1b2c3d4", 365 - "permissions": ["lexicons:read", "lexicons:create", "backfill:create"] 366 - } 367 - ``` 368 - 369 - The `key` field contains the full API key. It is only returned in this response — store it securely. The key's effective permissions are the **intersection** of the permissions specified here and the creating user's permissions at the time of each request. 370 - 371 - ### List API keys 372 - 373 - ``` 374 - GET /admin/api-keys 375 - ``` 376 - 377 - Requires `api-keys:read` permission. 378 - 379 - ```sh 380 - curl http://localhost:3000/admin/api-keys -H "$AUTH" 381 - ``` 382 - 383 - **Response**: `200 OK` 384 - 385 - ```json 386 - [ 387 - { 388 - "id": "550e8400-e29b-41d4-a716-446655440000", 389 - "name": "CI Deploy", 390 - "key_prefix": "hv_a1b2c3d4", 391 - "permissions": ["lexicons:read", "lexicons:create", "backfill:create"], 392 - "created_at": "2026-03-01T00:00:00Z", 393 - "last_used_at": "2026-03-06T12:00:00Z", 394 - "revoked_at": null 395 - } 396 - ] 397 - ``` 398 - 399 - Only returns keys belonging to the authenticated user. The full key is never included — only the prefix. 400 - 401 - ### Revoke an API key 402 - 403 - ``` 404 - DELETE /admin/api-keys/{id} 405 - ``` 406 - 407 - Requires `api-keys:delete` permission. 408 - 409 - ```sh 410 - curl -X DELETE http://localhost:3000/admin/api-keys/550e8400-e29b-41d4-a716-446655440000 \ 411 - -H "$AUTH" 412 - ``` 413 - 414 - Sets `revoked_at` on the key. The key remains in the database for audit purposes but can no longer authenticate. 415 - 416 - **Response**: `204 No Content` 417 - 418 - ## User Management 419 - 420 - ### Create a user 421 - 422 - ``` 423 - POST /admin/users 424 - ``` 425 - 426 - Requires `users:create` permission. You cannot grant permissions you don't have yourself (escalation guard). 427 - 428 - ```sh 429 - curl -X POST http://localhost:3000/admin/users \ 430 - -H "$AUTH" \ 431 - -H "Content-Type: application/json" \ 432 - -d '{ 433 - "did": "did:plc:newuser", 434 - "template": "operator" 435 - }' 436 - ``` 437 - 438 - | Field | Type | Required | Description | 439 - | ------------- | -------- | -------- | ------------------------------------------------------------------------------------------------- | 440 - | `did` | string | yes | The AT Protocol DID of the user to add | 441 - | `template` | string | no | Permission template: `viewer`, `operator`, `manager`, or `full_access` | 442 - | `permissions` | string[] | no | Explicit list of permissions to grant (used instead of or in addition to `template`) | 443 - 444 - If neither `template` nor `permissions` is provided, the user is created with no permissions. 445 - 446 - **Response**: `201 Created` 447 - 448 - ```json 449 - { 450 - "id": "550e8400-e29b-41d4-a716-446655440000", 451 - "did": "did:plc:newuser", 452 - "is_super": false, 453 - "permissions": ["lexicons:read", "records:read", "script-variables:read", "users:read", "api-keys:read", "api-keys:create", "api-keys:delete", "backfill:read", "backfill:create", "stats:read", "events:read"] 454 - } 455 - ``` 456 - 457 - ### List users 458 - 459 - ``` 460 - GET /admin/users 461 - ``` 462 - 463 - Requires `users:read` permission. 464 - 465 - ```sh 466 - curl http://localhost:3000/admin/users -H "$AUTH" 467 - ``` 468 - 469 - **Response**: `200 OK` 470 - 471 - ```json 472 - [ 473 - { 474 - "id": "550e8400-e29b-41d4-a716-446655440000", 475 - "did": "did:plc:admin", 476 - "is_super": true, 477 - "permissions": ["lexicons:create", "lexicons:read", "lexicons:delete", "records:read", "records:delete", "records:delete-collection", "script-variables:create", "script-variables:read", "script-variables:delete", "users:create", "users:read", "users:update", "users:delete", "api-keys:create", "api-keys:read", "api-keys:delete", "backfill:create", "backfill:read", "stats:read", "events:read"], 478 - "created_at": "2025-01-01T00:00:00Z", 479 - "last_used_at": "2025-01-02T12:00:00Z" 480 - } 481 - ] 482 - ``` 483 - 484 - ### Get a user 485 - 486 - ``` 487 - GET /admin/users/{id} 488 - ``` 489 - 490 - Requires `users:read` permission. 491 - 492 - ```sh 493 - curl http://localhost:3000/admin/users/550e8400-e29b-41d4-a716-446655440000 -H "$AUTH" 494 - ``` 495 - 496 - **Response**: `200 OK` with the same shape as a single item from the list response. 497 - 498 - ### Update user permissions 499 - 500 - ``` 501 - PATCH /admin/users/{id}/permissions 502 - ``` 503 - 504 - Requires `users:update` permission. You cannot grant permissions you don't have yourself, and you cannot modify the super user's permissions. 505 - 506 - ```sh 507 - curl -X PATCH http://localhost:3000/admin/users/550e8400-e29b-41d4-a716-446655440000/permissions \ 508 - -H "$AUTH" \ 509 - -H "Content-Type: application/json" \ 510 - -d '{ 511 - "grant": ["lexicons:create", "lexicons:delete"], 512 - "revoke": ["records:delete"] 513 - }' 514 - ``` 515 - 516 - | Field | Type | Required | Description | 517 - | -------- | -------- | -------- | ------------------------------ | 518 - | `grant` | string[] | no | Permissions to add | 519 - | `revoke` | string[] | no | Permissions to remove | 520 - 521 - **Response**: `200 OK` with the updated user object. 522 - 523 - ### Transfer super user 524 - 525 - ``` 526 - POST /admin/users/transfer-super 527 - ``` 528 - 529 - Only the current super user can call this endpoint. Transfers super user status to another existing user. 530 - 531 - ```sh 532 - curl -X POST http://localhost:3000/admin/users/transfer-super \ 533 - -H "$AUTH" \ 534 - -H "Content-Type: application/json" \ 535 - -d '{ "target_user_id": "550e8400-e29b-41d4-a716-446655440000" }' 536 - ``` 537 - 538 - | Field | Type | Required | Description | 539 - | ---------------- | ------ | -------- | ---------------------------------------- | 540 - | `target_user_id` | string | yes | The ID of the user to receive super status | 541 - 542 - **Response**: `200 OK` 543 - 544 - ### Delete a user 545 - 546 - ``` 547 - DELETE /admin/users/{id} 548 - ``` 549 - 550 - Requires `users:delete` permission. You cannot delete the super user or yourself. 551 - 552 - ```sh 553 - curl -X DELETE http://localhost:3000/admin/users/550e8400-e29b-41d4-a716-446655440000 \ 554 - -H "$AUTH" 555 - ``` 556 - 557 - **Response**: `204 No Content` 558 - 559 - ## Labelers 560 - 561 - Manage external labeler subscriptions. See the [Labelers guide](../guides/labelers.md) for background. 562 - 563 - ### Add a labeler 564 - 565 - ``` 566 - POST /admin/labelers 567 - ``` 568 - 569 - Requires `labelers:create` permission. 570 - 571 - ```sh 572 - curl -X POST http://localhost:3000/admin/labelers \ 573 - -H "$AUTH" \ 574 - -H "Content-Type: application/json" \ 575 - -d '{ "did": "did:plc:ar7c4by46qjdydhdevvrndac" }' 576 - ``` 577 - 578 - | Field | Type | Required | Description | 579 - | ----- | ------ | -------- | ---------------------- | 580 - | `did` | string | yes | The labeler's AT Protocol DID | 581 - 582 - **Response**: `201 Created` (empty body) 583 - 584 - ### List labelers 585 - 586 - ``` 587 - GET /admin/labelers 588 - ``` 589 - 590 - Requires `labelers:read` permission. 591 - 592 - ```sh 593 - curl http://localhost:3000/admin/labelers -H "$AUTH" 594 - ``` 595 - 596 - **Response**: `200 OK` 597 - 598 - ```json 599 - [ 600 - { 601 - "did": "did:plc:ar7c4by46qjdydhdevvrndac", 602 - "status": "active", 603 - "cursor": 1234, 604 - "created_at": "2026-03-15T00:00:00Z", 605 - "updated_at": "2026-03-15T00:00:00Z" 606 - } 607 - ] 608 - ``` 609 - 610 - | Field | Type | Description | 611 - | ------------ | ------------ | ------------------------------------------------ | 612 - | `did` | string | The labeler's DID | 613 - | `status` | string | `active` or `paused` | 614 - | `cursor` | number\|null | Last processed event cursor (null if never synced) | 615 - | `created_at` | string | ISO 8601 creation timestamp | 616 - | `updated_at` | string | ISO 8601 last-updated timestamp | 617 - 618 - ### Update a labeler 619 - 620 - ``` 621 - PATCH /admin/labelers/{did} 622 - ``` 623 - 624 - Requires `labelers:create` permission. 625 - 626 - ```sh 627 - curl -X PATCH http://localhost:3000/admin/labelers/did:plc:ar7c4by46qjdydhdevvrndac \ 628 - -H "$AUTH" \ 629 - -H "Content-Type: application/json" \ 630 - -d '{ "status": "paused" }' 631 - ``` 632 - 633 - | Field | Type | Required | Description | 634 - | -------- | ------ | -------- | ---------------------------- | 635 - | `status` | string | yes | New status: `active` or `paused` | 636 - 637 - **Response**: `200 OK` 638 - 639 - ### Delete a labeler 640 - 641 - ``` 642 - DELETE /admin/labelers/{did} 643 - ``` 644 - 645 - Requires `labelers:delete` permission. Removes the subscription and all labels emitted by this labeler. 646 - 647 - ```sh 648 - curl -X DELETE http://localhost:3000/admin/labelers/did:plc:ar7c4by46qjdydhdevvrndac \ 649 - -H "$AUTH" 650 - ``` 651 - 652 - **Response**: `204 No Content` 653 - 654 - ## Instance Settings 655 - 656 - Instance settings are key/value entries used to override environment-variable defaults at runtime (for example, the application name, terms-of-service URL, privacy policy URL, and uploaded logo). Settings stored here take precedence over the corresponding environment variables. All endpoints require the `settings:manage` permission. 657 - 658 - ### List settings 659 - 660 - ``` 661 - GET /admin/settings 662 - ``` 663 - 664 - ```sh 665 - curl http://localhost:3000/admin/settings -H "$AUTH" 666 - ``` 667 - 668 - Returns all key/value pairs stored in the `instance_settings` table. 669 - 670 - ### Upsert a setting 671 - 672 - ``` 673 - PUT /admin/settings/{key} 674 - ``` 675 - 676 - ```sh 677 - curl -X PUT http://localhost:3000/admin/settings/app_name \ 678 - -H "$AUTH" \ 679 - -H "Content-Type: application/json" \ 680 - -d '{ "value": "My HappyView" }' 681 - ``` 682 - 683 - ### Delete a setting 684 - 685 - ``` 686 - DELETE /admin/settings/{key} 687 - ``` 688 - 689 - Removes the override; the corresponding environment variable (if any) takes effect again. 690 - 691 - ### Upload / delete logo 692 - 693 - ``` 694 - PUT /admin/settings/logo 695 - DELETE /admin/settings/logo 696 - ``` 697 - 698 - `PUT` accepts a binary image body and stores it as the instance logo (served via the public dashboard). `DELETE` removes the stored logo. 699 - 700 - ## Domain Management 701 - 702 - Manage the domains a HappyView instance serves. Each domain gets its own AT Protocol OAuth client identity. The primary domain is auto-seeded from `PUBLIC_URL` on first boot. All endpoints require the `settings:manage` permission. 703 - 704 - ### List domains 705 - 706 - ``` 707 - GET /admin/domains 708 - ``` 709 - 710 - ```sh 711 - curl http://localhost:3000/admin/domains -H "$AUTH" 712 - ``` 713 - 714 - **Response**: `200 OK` 715 - 716 - ```json 717 - [ 718 - { 719 - "id": "550e8400-e29b-41d4-a716-446655440000", 720 - "url": "https://gamesgamesgamesgames.games", 721 - "is_primary": true, 722 - "created_at": "2026-04-16T00:00:00Z", 723 - "updated_at": "2026-04-16T00:00:00Z" 724 - } 725 - ] 726 - ``` 727 - 728 - ### Add a domain 729 - 730 - ``` 731 - POST /admin/domains 732 - ``` 733 - 734 - ```sh 735 - curl -X POST http://localhost:3000/admin/domains \ 736 - -H "$AUTH" \ 737 - -H "Content-Type: application/json" \ 738 - -d '{ "url": "https://api.cartridge.dev" }' 739 - ``` 740 - 741 - | Field | Type | Required | Description | 742 - | ----- | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------ | 743 - | `url` | string | yes | Valid origin (scheme + host, no path or trailing slash). Must be `https` unless `PUBLIC_URL` is a loopback address. | 744 - 745 - Returns `400 Bad Request` if the URL is invalid or already registered. 746 - 747 - **Response**: `201 Created` 748 - 749 - ```json 750 - { 751 - "id": "550e8400-e29b-41d4-a716-446655440001", 752 - "url": "https://api.cartridge.dev", 753 - "is_primary": false, 754 - "created_at": "2026-04-16T00:00:00Z", 755 - "updated_at": "2026-04-16T00:00:00Z" 756 - } 757 - ``` 758 - 759 - Side effects: builds an OAuth client for the domain, updates the in-memory domain cache. 760 - 761 - ### Remove a domain 762 - 763 - ``` 764 - DELETE /admin/domains/{id} 765 - ``` 766 - 767 - ```sh 768 - curl -X DELETE http://localhost:3000/admin/domains/550e8400-e29b-41d4-a716-446655440001 \ 769 - -H "$AUTH" 770 - ``` 771 - 772 - Returns `400 Bad Request` if the domain is primary — set a different domain as primary first. Returns `404 Not Found` if the domain doesn't exist. 773 - 774 - **Response**: `204 No Content` 775 - 776 - Side effects: removes the domain's OAuth client and cache entry. 777 - 778 - ### Set primary domain 779 - 780 - ``` 781 - POST /admin/domains/{id}/primary 782 - ``` 783 - 784 - ```sh 785 - curl -X POST http://localhost:3000/admin/domains/550e8400-e29b-41d4-a716-446655440001/primary \ 786 - -H "$AUTH" 787 - ``` 788 - 789 - Sets the target domain as the primary. Unsets the current primary in a single operation. Returns `404 Not Found` if the domain doesn't exist. 790 - 791 - **Response**: `204 No Content` 792 - 793 - Side effects: updates the in-memory cache and the OAuth client registry's primary client reference. 794 - 795 - ## Script Variables 796 - 797 - Script variables are encrypted key/value pairs available to Lua scripts via the `vars` global. Use them for secrets like API tokens. 798 - 799 - ### List script variables 800 - 801 - ``` 802 - GET /admin/script-variables 803 - ``` 804 - 805 - Requires `script-variables:read`. Returns a list of variable keys (values are not returned). 806 - 807 - ### Upsert a script variable 808 - 809 - ``` 810 - POST /admin/script-variables 811 - ``` 812 - 813 - Requires `script-variables:create`. 814 - 815 - ```sh 816 - curl -X POST http://localhost:3000/admin/script-variables \ 817 - -H "$AUTH" \ 818 - -H "Content-Type: application/json" \ 819 - -d '{ "key": "ALGOLIA_API_KEY", "value": "..." }' 820 - ``` 821 - 822 - The value is encrypted at rest using `TOKEN_ENCRYPTION_KEY`. 823 - 824 - ### Delete a script variable 825 - 826 - ``` 827 - DELETE /admin/script-variables/{key} 828 - ``` 829 - 830 - Requires `script-variables:delete`. 831 - 832 - ## API Clients 833 - 834 - API clients represent third-party applications that call HappyView's XRPC endpoints. **Every XRPC request** — including unauthenticated queries — must identify itself with a registered client via the `X-Client-Key` header (or session cookie, or `client_key` query param). The client key is HappyView's rate-limit bucket and caller identity; a request without one gets `401 Unauthorized`. 835 - 836 - Each client has an `hvc_`-prefixed client key and an `hvs_`-prefixed client secret. The secret is only returned once (at creation) and is sha256-hashed in the database. Server-to-server callers pass the secret as `X-Client-Secret`; browser callers rely on the `Origin` header matching the client's registered `client_uri`. Both checks currently log warnings on mismatch rather than rejecting the request, but the rate-limit bucket is applied either way. See [Authentication — XRPC](../getting-started/authentication.md#xrpc-api-client-identification) for the client-side view, and the [API Keys guide](../guides/api-keys.md) for how admin API keys differ from API clients. 837 - 838 - ### List API clients 839 - 840 - ``` 841 - GET /admin/api-clients 842 - ``` 843 - 844 - Requires `api-clients:view`. Returns clients ordered by `created_at` descending. Secrets are never returned. 845 - 846 - ```sh 847 - curl http://localhost:3000/admin/api-clients -H "$AUTH" 848 - ``` 849 - 850 - **Response**: `200 OK` 851 - 852 - ```json 853 - [ 854 - { 855 - "id": "01J9...", 856 - "client_key": "hvc_a1b2c3...", 857 - "name": "My Game Client", 858 - "client_id_url": "https://example.com/client-metadata.json", 859 - "client_uri": "https://example.com", 860 - "redirect_uris": ["https://example.com/callback"], 861 - "scopes": "atproto", 862 - "rate_limit_capacity": 200, 863 - "rate_limit_refill_rate": 5.0, 864 - "is_active": true, 865 - "created_by": "did:plc:...", 866 - "created_at": "2026-04-13T12:00:00Z", 867 - "updated_at": "2026-04-13T12:00:00Z" 868 - } 869 - ] 870 - ``` 871 - 872 - ### Create an API client 873 - 874 - ``` 875 - POST /admin/api-clients 876 - ``` 877 - 878 - Requires `api-clients:create`. Generates a fresh `client_key` and `client_secret`. **The secret is only returned in this response** — store it immediately. 879 - 880 - ```sh 881 - curl -X POST http://localhost:3000/admin/api-clients \ 882 - -H "$AUTH" \ 883 - -H "Content-Type: application/json" \ 884 - -d '{ 885 - "name": "My Game Client", 886 - "client_id_url": "https://example.com/client-metadata.json", 887 - "client_uri": "https://example.com", 888 - "redirect_uris": ["https://example.com/callback"], 889 - "scopes": "atproto", 890 - "rate_limit_capacity": 200, 891 - "rate_limit_refill_rate": 5.0 892 - }' 893 - ``` 894 - 895 - | Field | Type | Required | Description | 896 - | ------------------------ | -------- | -------- | -------------------------------------------------------------------------------------- | 897 - | `name` | string | yes | Human-readable display name | 898 - | `client_id_url` | string | yes | URL to the client's published OAuth client metadata document | 899 - | `client_uri` | string | yes | The client's home/landing URL | 900 - | `redirect_uris` | string[] | yes | Allowed OAuth redirect URIs | 901 - | `scopes` | string | no | Space-separated OAuth scopes (default `"atproto"`) | 902 - | `rate_limit_capacity` | integer | no | Per-client token bucket capacity. Falls back to `DEFAULT_RATE_LIMIT_CAPACITY` if unset | 903 - | `rate_limit_refill_rate` | number | no | Tokens added per second. Falls back to `DEFAULT_RATE_LIMIT_REFILL_RATE` if unset | 904 - 905 - **Response**: `201 Created` 906 - 907 - ```json 908 - { 909 - "id": "01J9...", 910 - "client_key": "hvc_a1b2c3...", 911 - "client_secret": "hvs_d4e5f6...", 912 - "name": "My Game Client", 913 - "client_id_url": "https://example.com/client-metadata.json" 914 - } 915 - ``` 916 - 917 - The new client is immediately registered with the OAuth registry and rate limiter, so it can authenticate without restarting HappyView. 918 - 919 - ### Get an API client 920 - 921 - ``` 922 - GET /admin/api-clients/{id} 923 - ``` 924 - 925 - Requires `api-clients:view`. Returns the same `ApiClientSummary` shape as the list endpoint, or `404 Not Found`. 926 - 927 - ### Update an API client 928 - 929 - ``` 930 - PUT /admin/api-clients/{id} 931 - ``` 932 - 933 - Requires `api-clients:edit`. All fields are optional — only provided fields are changed. Updating either rate-limit field re-registers the client with the rate limiter using the new values. 934 - 935 - | Field | Type | Description | 936 - | ------------------------ | -------- | ------------------------------------------------------------------------ | 937 - | `name` | string | New display name | 938 - | `client_uri` | string | New home URL | 939 - | `redirect_uris` | string[] | Replace the allowed redirect URIs | 940 - | `scopes` | string | Replace the OAuth scopes | 941 - | `rate_limit_capacity` | integer | New bucket capacity. Pass `null` to clear the override | 942 - | `rate_limit_refill_rate` | number | New refill rate. Pass `null` to clear the override | 943 - | `is_active` | boolean | Disable (`false`) or re-enable (`true`) the client without deleting it | 944 - 945 - **Response**: `204 No Content` 946 - 947 - The OAuth registry is updated in place. The `client_id_url` is immutable — to change it, delete and recreate the client. 948 - 949 - ### Delete an API client 950 - 951 - ``` 952 - DELETE /admin/api-clients/{id} 953 - ``` 954 - 955 - Requires `api-clients:delete`. Removes the client from the OAuth registry, the rate limiter, and the client identity store. 956 - 957 - **Response**: `204 No Content` 958 - 959 - ## Plugins 960 - 961 - Plugins extend HappyView with WebAssembly modules sourced from the [official plugin registry](../guides/plugins.md) or any URL serving a `manifest.json`. Most endpoints take a plugin manifest URL and load (or reload) the plugin in place — no restart needed. Encrypted plugin secrets require `TOKEN_ENCRYPTION_KEY` to be configured. 962 - 963 - ### List installed plugins 964 - 965 - ``` 966 - GET /admin/plugins 967 - ``` 968 - 969 - Requires `plugins:read`. Returns every loaded plugin with its source, required secrets, configuration status, and any pending updates from the official registry cache. 970 - 971 - ```sh 972 - curl http://localhost:3000/admin/plugins -H "$AUTH" 973 - ``` 974 - 975 - **Response**: `200 OK` 976 - 977 - ```json 978 - { 979 - "encryption_configured": true, 980 - "plugins": [ 981 - { 982 - "id": "steam", 983 - "name": "Steam", 984 - "version": "1.2.0", 985 - "source": "url", 986 - "url": "https://example.com/plugins/steam/manifest.json", 987 - "sha256": null, 988 - "enabled": true, 989 - "auth_type": "openid", 990 - "required_secrets": [ 991 - { 992 - "key": "PLUGIN_STEAM_API_KEY", 993 - "name": "Steam Web API Key", 994 - "description": "Get your API key at steamcommunity.com/dev/apikey" 995 - } 996 - ], 997 - "secrets_configured": true, 998 - "loaded_at": null, 999 - "update_available": false, 1000 - "latest_version": "1.2.0", 1001 - "pending_releases": [] 1002 - } 1003 - ] 1004 - } 1005 - ``` 1006 - 1007 - `secrets_configured` is `true` if the plugin has no required secrets, or if a row exists for it in `plugin_configs`. `update_available` and `pending_releases` are populated from the cached official registry — call `POST /admin/plugins/{id}/check-update` to refresh them. 1008 - 1009 - ### Preview a plugin before installing 1010 - 1011 - ``` 1012 - POST /admin/plugins/preview 1013 - ``` 1014 - 1015 - Requires `plugins:create`. Fetches and parses a manifest without installing the plugin, so the dashboard can show what it would register. 1016 - 1017 - ```sh 1018 - curl -X POST http://localhost:3000/admin/plugins/preview \ 1019 - -H "$AUTH" \ 1020 - -H "Content-Type: application/json" \ 1021 - -d '{ "url": "https://example.com/plugins/steam/manifest.json" }' 1022 - ``` 1023 - 1024 - **Response**: `200 OK` 1025 - 1026 - ```json 1027 - { 1028 - "id": "steam", 1029 - "name": "Steam", 1030 - "version": "1.2.0", 1031 - "description": "Import your Steam game library and playtime data.", 1032 - "icon_url": "https://example.com/steam-icon.png", 1033 - "auth_type": "openid", 1034 - "required_secrets": [ 1035 - { "key": "PLUGIN_STEAM_API_KEY", "name": "Steam Web API Key", "description": "..." } 1036 - ], 1037 - "manifest_url": "https://example.com/plugins/steam/manifest.json", 1038 - "wasm_url": "https://example.com/plugins/steam/steam.wasm" 1039 - } 1040 - ``` 1041 - 1042 - Returns `400 Bad Request` if the manifest can't be fetched or parsed. 1043 - 1044 - ### Install a plugin 1045 - 1046 - ``` 1047 - POST /admin/plugins 1048 - ``` 1049 - 1050 - Requires `plugins:create`. Fetches the manifest, downloads the WASM, registers the plugin, and persists it. 1051 - 1052 - ```sh 1053 - curl -X POST http://localhost:3000/admin/plugins \ 1054 - -H "$AUTH" \ 1055 - -H "Content-Type: application/json" \ 1056 - -d '{ 1057 - "url": "https://example.com/plugins/steam/manifest.json", 1058 - "sha256": "abc123..." 1059 - }' 1060 - ``` 1061 - 1062 - | Field | Type | Required | Description | 1063 - | -------- | ------ | -------- | -------------------------------------------------------------------------------------------- | 1064 - | `url` | string | yes | URL to the plugin's `manifest.json` | 1065 - | `sha256` | string | no | Optional sha256 of the WASM binary. If provided, install fails when the downloaded hash mismatches | 1066 - 1067 - **Response**: `200 OK` returning the same `PluginSummary` shape as the list endpoint. `secrets_configured` will be `false` if the plugin requires any secrets — call `PUT /admin/plugins/{id}/secrets` to configure them before the plugin can run. 1068 - 1069 - ### List official plugins 1070 - 1071 - ``` 1072 - GET /admin/plugins/official 1073 - ``` 1074 - 1075 - Requires `plugins:read`. Returns the cached catalog of plugins from the official registry. The cache is refreshed periodically by the server; use `POST /admin/plugins/{id}/check-update` to force-refresh a single entry. 1076 - 1077 - **Response**: `200 OK` 1078 - 1079 - ```json 1080 - { 1081 - "last_refreshed_at": "2026-04-13T11:00:00Z", 1082 - "plugins": [ 1083 - { 1084 - "id": "steam", 1085 - "name": "Steam", 1086 - "description": "Import your Steam game library and playtime data.", 1087 - "icon_url": "https://example.com/steam-icon.png", 1088 - "latest_version": "1.2.0", 1089 - "manifest_url": "https://example.com/plugins/steam/manifest.json" 1090 - } 1091 - ] 1092 - } 1093 - ``` 1094 - 1095 - ### Remove a plugin 1096 - 1097 - ``` 1098 - DELETE /admin/plugins/{id} 1099 - ``` 1100 - 1101 - Requires `plugins:delete`. Unregisters the plugin from the runtime and deletes its row from the `plugins` table. Plugin secrets in `plugin_configs` are not removed automatically — they're available again if you reinstall the same plugin. 1102 - 1103 - **Response**: `204 No Content`. Returns `404 Not Found` if no plugin with that id is loaded. 1104 - 1105 - ### Reload a plugin 1106 - 1107 - ``` 1108 - POST /admin/plugins/{id}/reload 1109 - ``` 1110 - 1111 - Requires `plugins:create`. Re-fetches the plugin from its current source URL and re-registers it. Useful after publishing a new version of a plugin you host yourself. 1112 - 1113 - The body is optional. To point the plugin at a new URL, pass: 1114 - 1115 - ```json 1116 - { "url": "https://example.com/plugins/steam/manifest.json" } 1117 - ``` 1118 - 1119 - When a new URL is provided, the stored `sha256` is cleared (the new version has its own hash). File-based plugins cannot be reloaded via this endpoint and return `400 Bad Request`. 1120 - 1121 - **Response**: `200 OK` with the refreshed `PluginSummary`. 1122 - 1123 - ### Check for plugin updates 1124 - 1125 - ``` 1126 - POST /admin/plugins/{id}/check-update 1127 - ``` 1128 - 1129 - Requires `plugins:create`. Forces a cache refresh for one plugin from the official registry, then returns the updated `PluginSummary` with `update_available`, `latest_version`, and `pending_releases` reflecting the latest catalog state. 1130 - 1131 - **Response**: `200 OK` with a `PluginSummary`. 1132 - 1133 - ### Get plugin secrets 1134 - 1135 - ``` 1136 - GET /admin/plugins/{id}/secrets 1137 - ``` 1138 - 1139 - Requires `plugins:read`. Returns the plugin's configured secrets with values masked (last 4 characters shown for values longer than 8 characters, otherwise fully masked). Requires `TOKEN_ENCRYPTION_KEY` to be configured. 1140 - 1141 - **Response**: `200 OK` 1142 - 1143 - ```json 1144 - { 1145 - "plugin_id": "steam", 1146 - "secrets": { 1147 - "PLUGIN_STEAM_API_KEY": "********ABCD" 1148 - } 1149 - } 1150 - ``` 1151 - 1152 - ### Update plugin secrets 1153 - 1154 - ``` 1155 - PUT /admin/plugins/{id}/secrets 1156 - ``` 1157 - 1158 - Requires `plugins:create`. Encrypts the provided secret values with `TOKEN_ENCRYPTION_KEY` (AES-256-GCM) and upserts them into `plugin_configs`. 1159 - 1160 - ```sh 1161 - curl -X PUT http://localhost:3000/admin/plugins/steam/secrets \ 1162 - -H "$AUTH" \ 1163 - -H "Content-Type: application/json" \ 1164 - -d '{ 1165 - "secrets": { 1166 - "PLUGIN_STEAM_API_KEY": "your-new-api-key" 1167 - } 1168 - }' 1169 - ``` 1170 - 1171 - Special handling: 1172 - 1173 - - Values starting with `********` are treated as masked placeholders and the existing encrypted value is preserved (so you can `GET` then `PUT` without re-typing every secret). 1174 - - Empty string values are not stored — use them to clear a secret. 1175 - 1176 - **Response**: `204 No Content` 42 + | Group | Description | 43 + | ----- | ----------- | 44 + | [Lexicons](admin/lexicons.md) | Upload, list, get, and delete lexicons and network lexicons | 45 + | [Stats](admin/stats.md) | Record counts by collection | 46 + | [Backfill](admin/backfill.md) | Create and monitor historical backfill jobs | 47 + | [Event Logs](admin/events.md) | Query the audit trail of system events | 48 + | [API Keys](admin/api-keys.md) | Create, list, and revoke API keys | 49 + | [Users](admin/users.md) | Create, list, update, and delete admin users | 50 + | [Labelers](admin/labelers.md) | Manage external labeler subscriptions | 51 + | [Instance Settings](admin/settings.md) | Configure app name, logo, and policy URLs | 52 + | [Domains](admin/domains.md) | Manage domains and their OAuth client identities | 53 + | [Script Variables](admin/script-variables.md) | Encrypted key/value pairs for Lua scripts | 54 + | [API Clients](admin/api-clients.md) | Register and manage third-party XRPC clients | 55 + | [Plugins](admin/plugins.md) | Install, configure, and manage WASM plugins | 1177 56 1178 57 ## Permissions 1179 58 1180 59 Each admin API endpoint requires a specific permission. See the [Permissions guide](../guides/permissions.md) for the full list of permissions and templates. 1181 60 1182 - | Endpoint | Required Permission | 1183 - | ------------------------------------- | ---------------------------- | 1184 - | `POST /admin/lexicons` | `lexicons:create` | 1185 - | `GET /admin/lexicons` | `lexicons:read` | 1186 - | `GET /admin/lexicons/{id}` | `lexicons:read` | 1187 - | `DELETE /admin/lexicons/{id}` | `lexicons:delete` | 1188 - | `POST /admin/network-lexicons` | `lexicons:create` | 1189 - | `GET /admin/network-lexicons` | `lexicons:read` | 1190 - | `DELETE /admin/network-lexicons/{id}` | `lexicons:delete` | 1191 - | `GET /admin/stats` | `stats:read` | 1192 - | `POST /admin/backfill` | `backfill:create` | 1193 - | `GET /admin/backfill/status` | `backfill:read` | 1194 - | `GET /admin/events` | `events:read` | 1195 - | `POST /admin/api-keys` | `api-keys:create` | 1196 - | `GET /admin/api-keys` | `api-keys:read` | 1197 - | `DELETE /admin/api-keys/{id}` | `api-keys:delete` | 1198 - | `POST /admin/users` | `users:create` | 1199 - | `GET /admin/users` | `users:read` | 1200 - | `GET /admin/users/{id}` | `users:read` | 1201 - | `PATCH /admin/users/{id}/permissions`| `users:update` | 1202 - | `DELETE /admin/users/{id}` | `users:delete` | 1203 - | `POST /admin/users/transfer-super` | Super user only | 1204 - | `GET /admin/script-variables` | `script-variables:read` | 1205 - | `POST /admin/script-variables` | `script-variables:create` | 1206 - | `DELETE /admin/script-variables/{key}`| `script-variables:delete` | 1207 - | `POST /admin/labelers` | `labelers:create` | 1208 - | `GET /admin/labelers` | `labelers:read` | 1209 - | `PATCH /admin/labelers/{did}` | `labelers:create` | 1210 - | `DELETE /admin/labelers/{did}` | `labelers:delete` | 1211 - | `GET /admin/settings` | `settings:manage` | 1212 - | `PUT /admin/settings/{key}` | `settings:manage` | 1213 - | `DELETE /admin/settings/{key}` | `settings:manage` | 1214 - | `PUT /admin/settings/logo` | `settings:manage` | 1215 - | `DELETE /admin/settings/logo` | `settings:manage` | 1216 - | `GET /admin/plugins` | `plugins:read` | 1217 - | `POST /admin/plugins` | `plugins:create` | 1218 - | `POST /admin/plugins/preview` | `plugins:read` | 1219 - | `GET /admin/plugins/official` | `plugins:read` | 1220 - | `DELETE /admin/plugins/{id}` | `plugins:delete` | 1221 - | `POST /admin/plugins/{id}/reload` | `plugins:create` | 1222 - | `POST /admin/plugins/{id}/check-update` | `plugins:read` | 1223 - | `GET /admin/plugins/{id}/secrets` | `plugins:read` | 1224 - | `PUT /admin/plugins/{id}/secrets` | `plugins:create` | 1225 - | `GET /admin/domains` | `settings:manage` | 1226 - | `POST /admin/domains` | `settings:manage` | 1227 - | `DELETE /admin/domains/{id}` | `settings:manage` | 1228 - | `POST /admin/domains/{id}/primary` | `settings:manage` | 1229 - | `GET /admin/api-clients` | `api-clients:view` | 1230 - | `POST /admin/api-clients` | `api-clients:create` | 1231 - | `GET /admin/api-clients/{id}` | `api-clients:view` | 1232 - | `PUT /admin/api-clients/{id}` | `api-clients:edit` | 1233 - | `DELETE /admin/api-clients/{id}` | `api-clients:delete` | 61 + | Endpoint | Required Permission | 62 + | ---------------------------------------- | -------------------------- | 63 + | `POST /admin/lexicons` | `lexicons:create` | 64 + | `GET /admin/lexicons` | `lexicons:read` | 65 + | `GET /admin/lexicons/{id}` | `lexicons:read` | 66 + | `DELETE /admin/lexicons/{id}` | `lexicons:delete` | 67 + | `POST /admin/network-lexicons` | `lexicons:create` | 68 + | `GET /admin/network-lexicons` | `lexicons:read` | 69 + | `DELETE /admin/network-lexicons/{id}` | `lexicons:delete` | 70 + | `GET /admin/stats` | `stats:read` | 71 + | `POST /admin/backfill` | `backfill:create` | 72 + | `GET /admin/backfill/status` | `backfill:read` | 73 + | `GET /admin/events` | `events:read` | 74 + | `POST /admin/api-keys` | `api-keys:create` | 75 + | `GET /admin/api-keys` | `api-keys:read` | 76 + | `DELETE /admin/api-keys/{id}` | `api-keys:delete` | 77 + | `POST /admin/users` | `users:create` | 78 + | `GET /admin/users` | `users:read` | 79 + | `GET /admin/users/{id}` | `users:read` | 80 + | `PATCH /admin/users/{id}/permissions` | `users:update` | 81 + | `DELETE /admin/users/{id}` | `users:delete` | 82 + | `POST /admin/users/transfer-super` | Super user only | 83 + | `GET /admin/script-variables` | `script-variables:read` | 84 + | `POST /admin/script-variables` | `script-variables:create` | 85 + | `DELETE /admin/script-variables/{key}` | `script-variables:delete` | 86 + | `POST /admin/labelers` | `labelers:create` | 87 + | `GET /admin/labelers` | `labelers:read` | 88 + | `PATCH /admin/labelers/{did}` | `labelers:create` | 89 + | `DELETE /admin/labelers/{did}` | `labelers:delete` | 90 + | `GET /admin/settings` | `settings:manage` | 91 + | `PUT /admin/settings/{key}` | `settings:manage` | 92 + | `DELETE /admin/settings/{key}` | `settings:manage` | 93 + | `PUT /admin/settings/logo` | `settings:manage` | 94 + | `DELETE /admin/settings/logo` | `settings:manage` | 95 + | `GET /admin/plugins` | `plugins:read` | 96 + | `POST /admin/plugins` | `plugins:create` | 97 + | `POST /admin/plugins/preview` | `plugins:read` | 98 + | `GET /admin/plugins/official` | `plugins:read` | 99 + | `DELETE /admin/plugins/{id}` | `plugins:delete` | 100 + | `POST /admin/plugins/{id}/reload` | `plugins:create` | 101 + | `POST /admin/plugins/{id}/check-update` | `plugins:read` | 102 + | `GET /admin/plugins/{id}/secrets` | `plugins:read` | 103 + | `PUT /admin/plugins/{id}/secrets` | `plugins:create` | 104 + | `GET /admin/domains` | `settings:manage` | 105 + | `POST /admin/domains` | `settings:manage` | 106 + | `DELETE /admin/domains/{id}` | `settings:manage` | 107 + | `POST /admin/domains/{id}/primary` | `settings:manage` | 108 + | `GET /admin/api-clients` | `api-clients:view` | 109 + | `POST /admin/api-clients` | `api-clients:create` | 110 + | `GET /admin/api-clients/{id}` | `api-clients:view` | 111 + | `PUT /admin/api-clients/{id}` | `api-clients:edit` | 112 + | `DELETE /admin/api-clients/{id}` | `api-clients:delete` |
+131
packages/docs/docs/reference/admin/api-clients.md
··· 1 + # Admin API: API Clients 2 + 3 + API clients represent third-party applications that call HappyView's XRPC endpoints. **Every XRPC request** — including unauthenticated queries — must identify itself with a registered client via the `X-Client-Key` header (or `client_key` query param). The client key is HappyView's rate-limit bucket and caller identity; a request without one gets `401 Unauthorized`. 4 + 5 + Each client has an `hvc_`-prefixed client key and an `hvs_`-prefixed client secret. The secret is only returned once (at creation) and is sha256-hashed in the database. Server-to-server callers pass the secret as `X-Client-Secret`; browser callers rely on the `Origin` header matching the client's registered `client_uri`. Both checks currently log warnings on mismatch rather than rejecting the request, but the rate-limit bucket is applied either way. See [Authentication — XRPC](../../getting-started/authentication.md#xrpc-api-client-identification) for the client-side view, and the [API Keys guide](../../guides/api-keys.md) for how admin API keys differ from API clients. 6 + 7 + ```sh 8 + # All examples assume $TOKEN is an API key (hv_...) 9 + AUTH="Authorization: Bearer $TOKEN" 10 + ``` 11 + 12 + ## List API clients 13 + 14 + ``` 15 + GET /admin/api-clients 16 + ``` 17 + 18 + Requires `api-clients:view`. Returns clients ordered by `created_at` descending. Secrets are never returned. 19 + 20 + ```sh 21 + curl http://localhost:3000/admin/api-clients -H "$AUTH" 22 + ``` 23 + 24 + **Response**: `200 OK` 25 + 26 + ```json 27 + [ 28 + { 29 + "id": "01J9...", 30 + "client_key": "hvc_a1b2c3...", 31 + "name": "My Game Client", 32 + "client_id_url": "https://example.com/client-metadata.json", 33 + "client_uri": "https://example.com", 34 + "redirect_uris": ["https://example.com/callback"], 35 + "scopes": "atproto", 36 + "rate_limit_capacity": 200, 37 + "rate_limit_refill_rate": 5.0, 38 + "is_active": true, 39 + "created_by": "did:plc:...", 40 + "created_at": "2026-04-13T12:00:00Z", 41 + "updated_at": "2026-04-13T12:00:00Z" 42 + } 43 + ] 44 + ``` 45 + 46 + ## Create an API client 47 + 48 + ``` 49 + POST /admin/api-clients 50 + ``` 51 + 52 + Requires `api-clients:create`. Generates a fresh `client_key` and `client_secret`. **The secret is only returned in this response** — store it immediately. 53 + 54 + ```sh 55 + curl -X POST http://localhost:3000/admin/api-clients \ 56 + -H "$AUTH" \ 57 + -H "Content-Type: application/json" \ 58 + -d '{ 59 + "name": "My Game Client", 60 + "client_id_url": "https://example.com/client-metadata.json", 61 + "client_uri": "https://example.com", 62 + "redirect_uris": ["https://example.com/callback"], 63 + "scopes": "atproto", 64 + "rate_limit_capacity": 200, 65 + "rate_limit_refill_rate": 5.0 66 + }' 67 + ``` 68 + 69 + | Field | Type | Required | Description | 70 + | ------------------------ | -------- | -------- | -------------------------------------------------------------------------------------- | 71 + | `name` | string | yes | Human-readable display name | 72 + | `client_id_url` | string | yes | URL to the client's published OAuth client metadata document | 73 + | `client_uri` | string | yes | The client's home/landing URL | 74 + | `redirect_uris` | string[] | yes | Allowed OAuth redirect URIs | 75 + | `scopes` | string | no | Space-separated OAuth scopes (default `"atproto"`) | 76 + | `rate_limit_capacity` | integer | no | Per-client token bucket capacity. Falls back to `DEFAULT_RATE_LIMIT_CAPACITY` if unset | 77 + | `rate_limit_refill_rate` | number | no | Tokens added per second. Falls back to `DEFAULT_RATE_LIMIT_REFILL_RATE` if unset | 78 + 79 + **Response**: `201 Created` 80 + 81 + ```json 82 + { 83 + "id": "01J9...", 84 + "client_key": "hvc_a1b2c3...", 85 + "client_secret": "hvs_d4e5f6...", 86 + "name": "My Game Client", 87 + "client_id_url": "https://example.com/client-metadata.json" 88 + } 89 + ``` 90 + 91 + The new client is immediately registered with the OAuth registry and rate limiter, so it can authenticate without restarting HappyView. 92 + 93 + ## Get an API client 94 + 95 + ``` 96 + GET /admin/api-clients/{id} 97 + ``` 98 + 99 + Requires `api-clients:view`. Returns the same shape as the list endpoint, or `404 Not Found`. 100 + 101 + ## Update an API client 102 + 103 + ``` 104 + PUT /admin/api-clients/{id} 105 + ``` 106 + 107 + Requires `api-clients:edit`. All fields are optional — only provided fields are changed. Updating either rate-limit field re-registers the client with the rate limiter using the new values. 108 + 109 + | Field | Type | Description | 110 + | ------------------------ | -------- | ---------------------------------------------------------------------- | 111 + | `name` | string | New display name | 112 + | `client_uri` | string | New home URL | 113 + | `redirect_uris` | string[] | Replace the allowed redirect URIs | 114 + | `scopes` | string | Replace the OAuth scopes | 115 + | `rate_limit_capacity` | integer | New bucket capacity. Pass `null` to clear the override | 116 + | `rate_limit_refill_rate` | number | New refill rate. Pass `null` to clear the override | 117 + | `is_active` | boolean | Disable (`false`) or re-enable (`true`) the client without deleting it | 118 + 119 + **Response**: `204 No Content` 120 + 121 + The OAuth registry is updated in place. The `client_id_url` is immutable — to change it, delete and recreate the client. 122 + 123 + ## Delete an API client 124 + 125 + ``` 126 + DELETE /admin/api-clients/{id} 127 + ``` 128 + 129 + Requires `api-clients:delete`. Removes the client from the OAuth registry, the rate limiter, and the client identity store. 130 + 131 + **Response**: `204 No Content`
+92
packages/docs/docs/reference/admin/api-keys.md
··· 1 + # Admin API: API Keys 2 + 3 + Manage API keys for programmatic access. See the [API Keys guide](../../guides/api-keys.md) for usage details. 4 + 5 + ```sh 6 + # All examples assume $TOKEN is an API key (hv_...) 7 + AUTH="Authorization: Bearer $TOKEN" 8 + ``` 9 + 10 + ## Create an API key 11 + 12 + ``` 13 + POST /admin/api-keys 14 + ``` 15 + 16 + Requires `api-keys:create` permission. 17 + 18 + ```sh 19 + curl -X POST http://localhost:3000/admin/api-keys \ 20 + -H "$AUTH" \ 21 + -H "Content-Type: application/json" \ 22 + -d '{ 23 + "name": "CI Deploy", 24 + "permissions": ["lexicons:read", "lexicons:create", "backfill:create"] 25 + }' 26 + ``` 27 + 28 + | Field | Type | Required | Description | 29 + | ------------- | -------- | -------- | ------------------------------------------------------------------------------------- | 30 + | `name` | string | yes | A label to identify this key's usage | 31 + | `permissions` | string[] | yes | Permissions to grant the key (must be a subset of the creating user's own permissions) | 32 + 33 + **Response**: `201 Created` 34 + 35 + ```json 36 + { 37 + "id": "550e8400-e29b-41d4-a716-446655440000", 38 + "name": "CI Deploy", 39 + "key": "hv_a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4", 40 + "key_prefix": "hv_a1b2c3d4", 41 + "permissions": ["lexicons:read", "lexicons:create", "backfill:create"] 42 + } 43 + ``` 44 + 45 + The `key` field contains the full API key. It is only returned in this response — store it securely. The key's effective permissions are the **intersection** of the permissions specified here and the creating user's permissions at the time of each request. 46 + 47 + ## List API keys 48 + 49 + ``` 50 + GET /admin/api-keys 51 + ``` 52 + 53 + Requires `api-keys:read` permission. 54 + 55 + ```sh 56 + curl http://localhost:3000/admin/api-keys -H "$AUTH" 57 + ``` 58 + 59 + **Response**: `200 OK` 60 + 61 + ```json 62 + [ 63 + { 64 + "id": "550e8400-e29b-41d4-a716-446655440000", 65 + "name": "CI Deploy", 66 + "key_prefix": "hv_a1b2c3d4", 67 + "permissions": ["lexicons:read", "lexicons:create", "backfill:create"], 68 + "created_at": "2026-03-01T00:00:00Z", 69 + "last_used_at": "2026-03-06T12:00:00Z", 70 + "revoked_at": null 71 + } 72 + ] 73 + ``` 74 + 75 + Only returns keys belonging to the authenticated user. The full key is never included — only the prefix. 76 + 77 + ## Revoke an API key 78 + 79 + ``` 80 + DELETE /admin/api-keys/{id} 81 + ``` 82 + 83 + Requires `api-keys:delete` permission. 84 + 85 + ```sh 86 + curl -X DELETE http://localhost:3000/admin/api-keys/550e8400-e29b-41d4-a716-446655440000 \ 87 + -H "$AUTH" 88 + ``` 89 + 90 + Sets `revoked_at` on the key. The key remains in the database for audit purposes but can no longer authenticate. 91 + 92 + **Response**: `204 No Content`
+65
packages/docs/docs/reference/admin/backfill.md
··· 1 + # Admin API: Backfill 2 + 3 + Create and monitor historical backfill jobs. See the [Backfill guide](../../guides/backfill.md) for background. 4 + 5 + ```sh 6 + # All examples assume $TOKEN is an API key (hv_...) 7 + AUTH="Authorization: Bearer $TOKEN" 8 + ``` 9 + 10 + ## Create a backfill job 11 + 12 + ``` 13 + POST /admin/backfill 14 + ``` 15 + 16 + ```sh 17 + curl -X POST http://localhost:3000/admin/backfill \ 18 + -H "$AUTH" \ 19 + -H "Content-Type: application/json" \ 20 + -d '{ "collection": "xyz.statusphere.status" }' 21 + ``` 22 + 23 + | Field | Type | Required | Description | 24 + | ------------ | ------ | -------- | ---------------------------------------------------------- | 25 + | `collection` | string | no | Limit to a single collection (backfills all if omitted) | 26 + | `did` | string | no | Limit to a single DID (discovers all via relay if omitted) | 27 + 28 + **Response**: `201 Created` 29 + 30 + ```json 31 + { 32 + "id": "550e8400-e29b-41d4-a716-446655440000", 33 + "status": "pending" 34 + } 35 + ``` 36 + 37 + ## List backfill jobs 38 + 39 + ``` 40 + GET /admin/backfill/status 41 + ``` 42 + 43 + ```sh 44 + curl http://localhost:3000/admin/backfill/status -H "$AUTH" 45 + ``` 46 + 47 + **Response**: `200 OK` 48 + 49 + ```json 50 + [ 51 + { 52 + "id": "550e8400-e29b-41d4-a716-446655440000", 53 + "collection": "xyz.statusphere.status", 54 + "did": null, 55 + "status": "completed", 56 + "total_repos": 42, 57 + "processed_repos": 42, 58 + "total_records": 1000, 59 + "error": null, 60 + "started_at": "2025-01-01T00:01:00Z", 61 + "completed_at": "2025-01-01T00:05:00Z", 62 + "created_at": "2025-01-01T00:00:00Z" 63 + } 64 + ] 65 + ```
+99
packages/docs/docs/reference/admin/domains.md
··· 1 + # Admin API: Domains 2 + 3 + Manage the domains a HappyView instance serves. Each domain gets its own AT Protocol OAuth client identity. The primary domain is auto-seeded from `PUBLIC_URL` on first boot. All endpoints require the `settings:manage` permission. 4 + 5 + ```sh 6 + # All examples assume $TOKEN is an API key (hv_...) 7 + AUTH="Authorization: Bearer $TOKEN" 8 + ``` 9 + 10 + ## List domains 11 + 12 + ``` 13 + GET /admin/domains 14 + ``` 15 + 16 + ```sh 17 + curl http://localhost:3000/admin/domains -H "$AUTH" 18 + ``` 19 + 20 + **Response**: `200 OK` 21 + 22 + ```json 23 + [ 24 + { 25 + "id": "550e8400-e29b-41d4-a716-446655440000", 26 + "url": "https://gamesgamesgamesgames.games", 27 + "is_primary": true, 28 + "created_at": "2026-04-16T00:00:00Z", 29 + "updated_at": "2026-04-16T00:00:00Z" 30 + } 31 + ] 32 + ``` 33 + 34 + ## Add a domain 35 + 36 + ``` 37 + POST /admin/domains 38 + ``` 39 + 40 + ```sh 41 + curl -X POST http://localhost:3000/admin/domains \ 42 + -H "$AUTH" \ 43 + -H "Content-Type: application/json" \ 44 + -d '{ "url": "https://api.cartridge.dev" }' 45 + ``` 46 + 47 + | Field | Type | Required | Description | 48 + | ----- | ------ | -------- | ------------------------------------------------------------------------------------------------------------------ | 49 + | `url` | string | yes | Valid origin (scheme + host, no path or trailing slash). Must be `https` unless `PUBLIC_URL` is a loopback address. | 50 + 51 + Returns `400 Bad Request` if the URL is invalid or already registered. 52 + 53 + **Response**: `201 Created` 54 + 55 + ```json 56 + { 57 + "id": "550e8400-e29b-41d4-a716-446655440001", 58 + "url": "https://api.cartridge.dev", 59 + "is_primary": false, 60 + "created_at": "2026-04-16T00:00:00Z", 61 + "updated_at": "2026-04-16T00:00:00Z" 62 + } 63 + ``` 64 + 65 + Side effects: builds an OAuth client for the domain, updates the in-memory domain cache. 66 + 67 + ## Remove a domain 68 + 69 + ``` 70 + DELETE /admin/domains/{id} 71 + ``` 72 + 73 + ```sh 74 + curl -X DELETE http://localhost:3000/admin/domains/550e8400-e29b-41d4-a716-446655440001 \ 75 + -H "$AUTH" 76 + ``` 77 + 78 + Returns `400 Bad Request` if the domain is primary — set a different domain as primary first. Returns `404 Not Found` if the domain doesn't exist. 79 + 80 + **Response**: `204 No Content` 81 + 82 + Side effects: removes the domain's OAuth client and cache entry. 83 + 84 + ## Set primary domain 85 + 86 + ``` 87 + POST /admin/domains/{id}/primary 88 + ``` 89 + 90 + ```sh 91 + curl -X POST http://localhost:3000/admin/domains/550e8400-e29b-41d4-a716-446655440001/primary \ 92 + -H "$AUTH" 93 + ``` 94 + 95 + Sets the target domain as the primary. Unsets the current primary in a single operation. Returns `404 Not Found` if the domain doesn't exist. 96 + 97 + **Response**: `204 No Content` 98 + 99 + Side effects: updates the in-memory cache and the OAuth client registry's primary client reference.
+54
packages/docs/docs/reference/admin/events.md
··· 1 + # Admin API: Event Logs 2 + 3 + HappyView records an audit trail of system events: lexicon changes, record operations, Lua script executions and errors, user actions, backfill jobs, and Jetstream connectivity. See the [Event Logs guide](../../guides/event-logs.md) for details on event types and retention. 4 + 5 + ```sh 6 + # All examples assume $TOKEN is an API key (hv_...) 7 + AUTH="Authorization: Bearer $TOKEN" 8 + ``` 9 + 10 + ## List event logs 11 + 12 + ``` 13 + GET /admin/events 14 + ``` 15 + 16 + ```sh 17 + curl "http://localhost:3000/admin/events?severity=error&limit=10" -H "$AUTH" 18 + ``` 19 + 20 + | Param | Type | Required | Description | 21 + | ------------ | ------ | -------- | --------------------------------------------------------------------- | 22 + | `event_type` | string | no | Filter by exact event type (e.g. `script.error`) | 23 + | `category` | string | no | Filter by category prefix (e.g. `lexicon` matches all lexicon events) | 24 + | `severity` | string | no | Filter by severity: `info`, `warn`, or `error` | 25 + | `subject` | string | no | Filter by subject (lexicon ID, record URI, admin DID, etc.) | 26 + | `cursor` | string | no | Pagination cursor (ISO 8601 timestamp from previous response) | 27 + | `limit` | number | no | Results per page (default `50`, max `100`) | 28 + 29 + **Response**: `200 OK` 30 + 31 + ```json 32 + { 33 + "events": [ 34 + { 35 + "id": "550e8400-e29b-41d4-a716-446655440000", 36 + "event_type": "script.error", 37 + "severity": "error", 38 + "actor_did": "did:plc:abc123", 39 + "subject": "com.example.feed.like", 40 + "detail": { 41 + "error": "attempt to index nil value", 42 + "script_source": "function handle() ... end", 43 + "input": { "status": "hello" }, 44 + "caller_did": "did:plc:abc123", 45 + "method": "com.example.feed.like" 46 + }, 47 + "created_at": "2026-03-01T12:00:00Z" 48 + } 49 + ], 50 + "cursor": "2026-03-01T11:59:00Z" 51 + } 52 + ``` 53 + 54 + Events are returned in reverse chronological order (newest first). Pass the `cursor` value from the response to fetch the next page.
+99
packages/docs/docs/reference/admin/labelers.md
··· 1 + # Admin API: Labelers 2 + 3 + Manage external labeler subscriptions. See the [Labelers guide](../../guides/labelers.md) for background. 4 + 5 + ```sh 6 + # All examples assume $TOKEN is an API key (hv_...) 7 + AUTH="Authorization: Bearer $TOKEN" 8 + ``` 9 + 10 + ## Add a labeler 11 + 12 + ``` 13 + POST /admin/labelers 14 + ``` 15 + 16 + Requires `labelers:create` permission. 17 + 18 + ```sh 19 + curl -X POST http://localhost:3000/admin/labelers \ 20 + -H "$AUTH" \ 21 + -H "Content-Type: application/json" \ 22 + -d '{ "did": "did:plc:ar7c4by46qjdydhdevvrndac" }' 23 + ``` 24 + 25 + | Field | Type | Required | Description | 26 + | ----- | ------ | -------- | ----------------------------- | 27 + | `did` | string | yes | The labeler's AT Protocol DID | 28 + 29 + **Response**: `201 Created` (empty body) 30 + 31 + ## List labelers 32 + 33 + ``` 34 + GET /admin/labelers 35 + ``` 36 + 37 + Requires `labelers:read` permission. 38 + 39 + ```sh 40 + curl http://localhost:3000/admin/labelers -H "$AUTH" 41 + ``` 42 + 43 + **Response**: `200 OK` 44 + 45 + ```json 46 + [ 47 + { 48 + "did": "did:plc:ar7c4by46qjdydhdevvrndac", 49 + "status": "active", 50 + "cursor": 1234, 51 + "created_at": "2026-03-15T00:00:00Z", 52 + "updated_at": "2026-03-15T00:00:00Z" 53 + } 54 + ] 55 + ``` 56 + 57 + | Field | Type | Description | 58 + | ------------ | ------------ | -------------------------------------------------- | 59 + | `did` | string | The labeler's DID | 60 + | `status` | string | `active` or `paused` | 61 + | `cursor` | number\|null | Last processed event cursor (null if never synced) | 62 + | `created_at` | string | ISO 8601 creation timestamp | 63 + | `updated_at` | string | ISO 8601 last-updated timestamp | 64 + 65 + ## Update a labeler 66 + 67 + ``` 68 + PATCH /admin/labelers/{did} 69 + ``` 70 + 71 + Requires `labelers:create` permission. 72 + 73 + ```sh 74 + curl -X PATCH http://localhost:3000/admin/labelers/did:plc:ar7c4by46qjdydhdevvrndac \ 75 + -H "$AUTH" \ 76 + -H "Content-Type: application/json" \ 77 + -d '{ "status": "paused" }' 78 + ``` 79 + 80 + | Field | Type | Required | Description | 81 + | -------- | ------ | -------- | -------------------------------- | 82 + | `status` | string | yes | New status: `active` or `paused` | 83 + 84 + **Response**: `200 OK` 85 + 86 + ## Delete a labeler 87 + 88 + ``` 89 + DELETE /admin/labelers/{did} 90 + ``` 91 + 92 + Requires `labelers:delete` permission. Removes the subscription and all labels emitted by this labeler. 93 + 94 + ```sh 95 + curl -X DELETE http://localhost:3000/admin/labelers/did:plc:ar7c4by46qjdydhdevvrndac \ 96 + -H "$AUTH" 97 + ``` 98 + 99 + **Response**: `204 No Content`
+167
packages/docs/docs/reference/admin/lexicons.md
··· 1 + # Admin API: Lexicons 2 + 3 + Manage lexicons and network lexicons. See the [Lexicons guide](../../guides/lexicons.md) for background on how lexicons drive indexing and XRPC routing. 4 + 5 + ```sh 6 + # All examples assume $TOKEN is an API key (hv_...) 7 + AUTH="Authorization: Bearer $TOKEN" 8 + ``` 9 + 10 + ## Upload / upsert a lexicon 11 + 12 + ``` 13 + POST /admin/lexicons 14 + ``` 15 + 16 + ```sh 17 + curl -X POST http://localhost:3000/admin/lexicons \ 18 + -H "$AUTH" \ 19 + -H "Content-Type: application/json" \ 20 + -d '{ 21 + "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" } } } } } }, 22 + "backfill": true, 23 + "target_collection": null 24 + }' 25 + ``` 26 + 27 + | Field | Type | Required | Description | 28 + | ------------------- | ------- | -------- | --------------------------------------------------------------------- | 29 + | `lexicon_json` | object | yes | Raw lexicon JSON (must have `lexicon: 1` and `id`) | 30 + | `backfill` | boolean | no | Whether uploading triggers historical backfill (default `true`) | 31 + | `target_collection` | string | no | For query/procedure lexicons, the record collection they operate on | 32 + | `script` | string | no | Lua script for query/procedure endpoints | 33 + | `index_hook` | string | no | [Index hook](../../guides/index-hooks.md) Lua script for record lexicons | 34 + 35 + **Response**: `201 Created` (new) or `200 OK` (upsert) 36 + 37 + ```json 38 + { 39 + "id": "xyz.statusphere.status", 40 + "revision": 1 41 + } 42 + ``` 43 + 44 + ## List lexicons 45 + 46 + ``` 47 + GET /admin/lexicons 48 + ``` 49 + 50 + ```sh 51 + curl http://localhost:3000/admin/lexicons -H "$AUTH" 52 + ``` 53 + 54 + **Response**: `200 OK` 55 + 56 + ```json 57 + [ 58 + { 59 + "id": "xyz.statusphere.status", 60 + "revision": 1, 61 + "lexicon_type": "record", 62 + "backfill": true, 63 + "created_at": "2025-01-01T00:00:00Z", 64 + "updated_at": "2025-01-01T00:00:00Z" 65 + } 66 + ] 67 + ``` 68 + 69 + ## Get a lexicon 70 + 71 + ``` 72 + GET /admin/lexicons/{id} 73 + ``` 74 + 75 + ```sh 76 + curl http://localhost:3000/admin/lexicons/xyz.statusphere.status -H "$AUTH" 77 + ``` 78 + 79 + **Response**: `200 OK` with full lexicon details including raw JSON. 80 + 81 + ## Delete a lexicon 82 + 83 + ``` 84 + DELETE /admin/lexicons/{id} 85 + ``` 86 + 87 + ```sh 88 + curl -X DELETE http://localhost:3000/admin/lexicons/xyz.statusphere.status -H "$AUTH" 89 + ``` 90 + 91 + **Response**: `204 No Content` 92 + 93 + ## Network Lexicons 94 + 95 + Network lexicons are fetched from the AT Protocol network via DNS TXT resolution and kept updated via the Jetstream subscription. See [Lexicons - Network lexicons](../../guides/lexicons.md#network-lexicons) for background. 96 + 97 + ### Add a network lexicon 98 + 99 + ``` 100 + POST /admin/network-lexicons 101 + ``` 102 + 103 + ```sh 104 + curl -X POST http://localhost:3000/admin/network-lexicons \ 105 + -H "$AUTH" \ 106 + -H "Content-Type: application/json" \ 107 + -d '{ 108 + "nsid": "xyz.statusphere.status", 109 + "target_collection": null 110 + }' 111 + ``` 112 + 113 + | Field | Type | Required | Description | 114 + | ------------------- | ------ | -------- | ------------------------------------------------------------------- | 115 + | `nsid` | string | yes | The NSID of the lexicon to watch | 116 + | `target_collection` | string | no | For query/procedure lexicons, the record collection they operate on | 117 + 118 + HappyView resolves the NSID authority via DNS TXT, fetches the lexicon from the authority's PDS, parses it, and stores it. 119 + 120 + **Response**: `201 Created` 121 + 122 + ```json 123 + { 124 + "nsid": "xyz.statusphere.status", 125 + "authority_did": "did:plc:authority", 126 + "revision": 1 127 + } 128 + ``` 129 + 130 + ### List network lexicons 131 + 132 + ``` 133 + GET /admin/network-lexicons 134 + ``` 135 + 136 + ```sh 137 + curl http://localhost:3000/admin/network-lexicons -H "$AUTH" 138 + ``` 139 + 140 + **Response**: `200 OK` 141 + 142 + ```json 143 + [ 144 + { 145 + "nsid": "xyz.statusphere.status", 146 + "authority_did": "did:plc:authority", 147 + "target_collection": null, 148 + "last_fetched_at": "2025-01-01T00:00:00Z", 149 + "created_at": "2025-01-01T00:00:00Z" 150 + } 151 + ] 152 + ``` 153 + 154 + ### Remove a network lexicon 155 + 156 + ``` 157 + DELETE /admin/network-lexicons/{nsid} 158 + ``` 159 + 160 + ```sh 161 + curl -X DELETE http://localhost:3000/admin/network-lexicons/xyz.statusphere.status \ 162 + -H "$AUTH" 163 + ``` 164 + 165 + Removes the network lexicon tracking and also deletes the lexicon from the `lexicons` table and in-memory registry. 166 + 167 + **Response**: `204 No Content`
+223
packages/docs/docs/reference/admin/plugins.md
··· 1 + # Admin API: Plugins 2 + 3 + Plugins extend HappyView with WebAssembly modules sourced from the [official plugin registry](../../guides/plugins.md) or any URL serving a `manifest.json`. Most endpoints take a plugin manifest URL and load (or reload) the plugin in place — no restart needed. Encrypted plugin secrets require `TOKEN_ENCRYPTION_KEY` to be configured. 4 + 5 + ```sh 6 + # All examples assume $TOKEN is an API key (hv_...) 7 + AUTH="Authorization: Bearer $TOKEN" 8 + ``` 9 + 10 + ## List installed plugins 11 + 12 + ``` 13 + GET /admin/plugins 14 + ``` 15 + 16 + Requires `plugins:read`. Returns every loaded plugin with its source, required secrets, configuration status, and any pending updates from the official registry cache. 17 + 18 + ```sh 19 + curl http://localhost:3000/admin/plugins -H "$AUTH" 20 + ``` 21 + 22 + **Response**: `200 OK` 23 + 24 + ```json 25 + { 26 + "encryption_configured": true, 27 + "plugins": [ 28 + { 29 + "id": "steam", 30 + "name": "Steam", 31 + "version": "1.2.0", 32 + "source": "url", 33 + "url": "https://example.com/plugins/steam/manifest.json", 34 + "sha256": null, 35 + "enabled": true, 36 + "auth_type": "openid", 37 + "required_secrets": [ 38 + { 39 + "key": "PLUGIN_STEAM_API_KEY", 40 + "name": "Steam Web API Key", 41 + "description": "Get your API key at steamcommunity.com/dev/apikey" 42 + } 43 + ], 44 + "secrets_configured": true, 45 + "loaded_at": null, 46 + "update_available": false, 47 + "latest_version": "1.2.0", 48 + "pending_releases": [] 49 + } 50 + ] 51 + } 52 + ``` 53 + 54 + `secrets_configured` is `true` if the plugin has no required secrets, or if a row exists for it in `plugin_configs`. `update_available` and `pending_releases` are populated from the cached official registry — call `POST /admin/plugins/{id}/check-update` to refresh them. 55 + 56 + ## Preview a plugin before installing 57 + 58 + ``` 59 + POST /admin/plugins/preview 60 + ``` 61 + 62 + Requires `plugins:create`. Fetches and parses a manifest without installing the plugin, so the dashboard can show what it would register. 63 + 64 + ```sh 65 + curl -X POST http://localhost:3000/admin/plugins/preview \ 66 + -H "$AUTH" \ 67 + -H "Content-Type: application/json" \ 68 + -d '{ "url": "https://example.com/plugins/steam/manifest.json" }' 69 + ``` 70 + 71 + **Response**: `200 OK` 72 + 73 + ```json 74 + { 75 + "id": "steam", 76 + "name": "Steam", 77 + "version": "1.2.0", 78 + "description": "Import your Steam game library and playtime data.", 79 + "icon_url": "https://example.com/steam-icon.png", 80 + "auth_type": "openid", 81 + "required_secrets": [ 82 + { "key": "PLUGIN_STEAM_API_KEY", "name": "Steam Web API Key", "description": "..." } 83 + ], 84 + "manifest_url": "https://example.com/plugins/steam/manifest.json", 85 + "wasm_url": "https://example.com/plugins/steam/steam.wasm" 86 + } 87 + ``` 88 + 89 + Returns `400 Bad Request` if the manifest can't be fetched or parsed. 90 + 91 + ## Install a plugin 92 + 93 + ``` 94 + POST /admin/plugins 95 + ``` 96 + 97 + Requires `plugins:create`. Fetches the manifest, downloads the WASM, registers the plugin, and persists it. 98 + 99 + ```sh 100 + curl -X POST http://localhost:3000/admin/plugins \ 101 + -H "$AUTH" \ 102 + -H "Content-Type: application/json" \ 103 + -d '{ 104 + "url": "https://example.com/plugins/steam/manifest.json", 105 + "sha256": "abc123..." 106 + }' 107 + ``` 108 + 109 + | Field | Type | Required | Description | 110 + | -------- | ------ | -------- | ---------------------------------------------------------------------------------------------------- | 111 + | `url` | string | yes | URL to the plugin's `manifest.json` | 112 + | `sha256` | string | no | Optional sha256 of the WASM binary. If provided, install fails when the downloaded hash mismatches | 113 + 114 + **Response**: `200 OK` returning the same `PluginSummary` shape as the list endpoint. `secrets_configured` will be `false` if the plugin requires any secrets — call `PUT /admin/plugins/{id}/secrets` to configure them before the plugin can run. 115 + 116 + ## List official plugins 117 + 118 + ``` 119 + GET /admin/plugins/official 120 + ``` 121 + 122 + Requires `plugins:read`. Returns the cached catalog of plugins from the official registry. The cache is refreshed periodically by the server; use `POST /admin/plugins/{id}/check-update` to force-refresh a single entry. 123 + 124 + **Response**: `200 OK` 125 + 126 + ```json 127 + { 128 + "last_refreshed_at": "2026-04-13T11:00:00Z", 129 + "plugins": [ 130 + { 131 + "id": "steam", 132 + "name": "Steam", 133 + "description": "Import your Steam game library and playtime data.", 134 + "icon_url": "https://example.com/steam-icon.png", 135 + "latest_version": "1.2.0", 136 + "manifest_url": "https://example.com/plugins/steam/manifest.json" 137 + } 138 + ] 139 + } 140 + ``` 141 + 142 + ## Remove a plugin 143 + 144 + ``` 145 + DELETE /admin/plugins/{id} 146 + ``` 147 + 148 + Requires `plugins:delete`. Unregisters the plugin from the runtime and deletes its row from the `plugins` table. Plugin secrets in `plugin_configs` are not removed automatically — they're available again if you reinstall the same plugin. 149 + 150 + **Response**: `204 No Content`. Returns `404 Not Found` if no plugin with that id is loaded. 151 + 152 + ## Reload a plugin 153 + 154 + ``` 155 + POST /admin/plugins/{id}/reload 156 + ``` 157 + 158 + Requires `plugins:create`. Re-fetches the plugin from its current source URL and re-registers it. Useful after publishing a new version of a plugin you host yourself. 159 + 160 + The body is optional. To point the plugin at a new URL, pass: 161 + 162 + ```json 163 + { "url": "https://example.com/plugins/steam/manifest.json" } 164 + ``` 165 + 166 + When a new URL is provided, the stored `sha256` is cleared (the new version has its own hash). File-based plugins cannot be reloaded via this endpoint and return `400 Bad Request`. 167 + 168 + **Response**: `200 OK` with the refreshed `PluginSummary`. 169 + 170 + ## Check for plugin updates 171 + 172 + ``` 173 + POST /admin/plugins/{id}/check-update 174 + ``` 175 + 176 + Requires `plugins:create`. Forces a cache refresh for one plugin from the official registry, then returns the updated `PluginSummary` with `update_available`, `latest_version`, and `pending_releases` reflecting the latest catalog state. 177 + 178 + **Response**: `200 OK` with a `PluginSummary`. 179 + 180 + ## Get plugin secrets 181 + 182 + ``` 183 + GET /admin/plugins/{id}/secrets 184 + ``` 185 + 186 + Requires `plugins:read`. Returns the plugin's configured secrets with values masked (last 4 characters shown for values longer than 8 characters, otherwise fully masked). Requires `TOKEN_ENCRYPTION_KEY` to be configured. 187 + 188 + **Response**: `200 OK` 189 + 190 + ```json 191 + { 192 + "plugin_id": "steam", 193 + "secrets": { 194 + "PLUGIN_STEAM_API_KEY": "********ABCD" 195 + } 196 + } 197 + ``` 198 + 199 + ## Update plugin secrets 200 + 201 + ``` 202 + PUT /admin/plugins/{id}/secrets 203 + ``` 204 + 205 + Requires `plugins:create`. Encrypts the provided secret values with `TOKEN_ENCRYPTION_KEY` (AES-256-GCM) and upserts them into `plugin_configs`. 206 + 207 + ```sh 208 + curl -X PUT http://localhost:3000/admin/plugins/steam/secrets \ 209 + -H "$AUTH" \ 210 + -H "Content-Type: application/json" \ 211 + -d '{ 212 + "secrets": { 213 + "PLUGIN_STEAM_API_KEY": "your-new-api-key" 214 + } 215 + }' 216 + ``` 217 + 218 + Special handling: 219 + 220 + - Values starting with `********` are treated as masked placeholders and the existing encrypted value is preserved (so you can `GET` then `PUT` without re-typing every secret). 221 + - Empty string values are not stored — use them to clear a secret. 222 + 223 + **Response**: `204 No Content`
+41
packages/docs/docs/reference/admin/script-variables.md
··· 1 + # Admin API: Script Variables 2 + 3 + Script variables are encrypted key/value pairs available to Lua scripts via the `vars` global. Use them for secrets like API tokens. 4 + 5 + ```sh 6 + # All examples assume $TOKEN is an API key (hv_...) 7 + AUTH="Authorization: Bearer $TOKEN" 8 + ``` 9 + 10 + ## List script variables 11 + 12 + ``` 13 + GET /admin/script-variables 14 + ``` 15 + 16 + Requires `script-variables:read`. Returns a list of variable keys (values are not returned). 17 + 18 + ## Upsert a script variable 19 + 20 + ``` 21 + POST /admin/script-variables 22 + ``` 23 + 24 + Requires `script-variables:create`. 25 + 26 + ```sh 27 + curl -X POST http://localhost:3000/admin/script-variables \ 28 + -H "$AUTH" \ 29 + -H "Content-Type: application/json" \ 30 + -d '{ "key": "ALGOLIA_API_KEY", "value": "..." }' 31 + ``` 32 + 33 + The value is encrypted at rest using `TOKEN_ENCRYPTION_KEY`. 34 + 35 + ## Delete a script variable 36 + 37 + ``` 38 + DELETE /admin/script-variables/{key} 39 + ``` 40 + 41 + Requires `script-variables:delete`.
+50
packages/docs/docs/reference/admin/settings.md
··· 1 + # Admin API: Instance Settings 2 + 3 + Instance settings are key/value entries used to override environment-variable defaults at runtime (for example, the application name, terms-of-service URL, privacy policy URL, and uploaded logo). Settings stored here take precedence over the corresponding environment variables. All endpoints require the `settings:manage` permission. 4 + 5 + ```sh 6 + # All examples assume $TOKEN is an API key (hv_...) 7 + AUTH="Authorization: Bearer $TOKEN" 8 + ``` 9 + 10 + ## List settings 11 + 12 + ``` 13 + GET /admin/settings 14 + ``` 15 + 16 + ```sh 17 + curl http://localhost:3000/admin/settings -H "$AUTH" 18 + ``` 19 + 20 + Returns all key/value pairs stored in the `instance_settings` table. 21 + 22 + ## Upsert a setting 23 + 24 + ``` 25 + PUT /admin/settings/{key} 26 + ``` 27 + 28 + ```sh 29 + curl -X PUT http://localhost:3000/admin/settings/app_name \ 30 + -H "$AUTH" \ 31 + -H "Content-Type: application/json" \ 32 + -d '{ "value": "My HappyView" }' 33 + ``` 34 + 35 + ## Delete a setting 36 + 37 + ``` 38 + DELETE /admin/settings/{key} 39 + ``` 40 + 41 + Removes the override; the corresponding environment variable (if any) takes effect again. 42 + 43 + ## Upload / delete logo 44 + 45 + ``` 46 + PUT /admin/settings/logo 47 + DELETE /admin/settings/logo 48 + ``` 49 + 50 + `PUT` accepts a binary image body and stores it as the instance logo (served via the public dashboard). `DELETE` removes the stored logo.
+25
packages/docs/docs/reference/admin/stats.md
··· 1 + # Admin API: Stats 2 + 3 + ```sh 4 + # All examples assume $TOKEN is an API key (hv_...) 5 + AUTH="Authorization: Bearer $TOKEN" 6 + ``` 7 + 8 + ## Record counts 9 + 10 + ``` 11 + GET /admin/stats 12 + ``` 13 + 14 + ```sh 15 + curl http://localhost:3000/admin/stats -H "$AUTH" 16 + ``` 17 + 18 + **Response**: `200 OK` 19 + 20 + ```json 21 + { 22 + "total_records": 12345, 23 + "collections": [{ "collection": "xyz.statusphere.status", "count": 500 }] 24 + } 25 + ```
+147
packages/docs/docs/reference/admin/users.md
··· 1 + # Admin API: Users 2 + 3 + Manage admin users and their permissions. See the [Permissions guide](../../guides/permissions.md) for available permissions and templates. 4 + 5 + ```sh 6 + # All examples assume $TOKEN is an API key (hv_...) 7 + AUTH="Authorization: Bearer $TOKEN" 8 + ``` 9 + 10 + ## Create a user 11 + 12 + ``` 13 + POST /admin/users 14 + ``` 15 + 16 + Requires `users:create` permission. You cannot grant permissions you don't have yourself (escalation guard). 17 + 18 + ```sh 19 + curl -X POST http://localhost:3000/admin/users \ 20 + -H "$AUTH" \ 21 + -H "Content-Type: application/json" \ 22 + -d '{ 23 + "did": "did:plc:newuser", 24 + "template": "operator" 25 + }' 26 + ``` 27 + 28 + | Field | Type | Required | Description | 29 + | ------------- | -------- | -------- | ---------------------------------------------------------------------------------- | 30 + | `did` | string | yes | The AT Protocol DID of the user to add | 31 + | `template` | string | no | Permission template: `viewer`, `operator`, `manager`, or `full_access` | 32 + | `permissions` | string[] | no | Explicit list of permissions to grant (used instead of or in addition to `template`) | 33 + 34 + If neither `template` nor `permissions` is provided, the user is created with no permissions. 35 + 36 + **Response**: `201 Created` 37 + 38 + ```json 39 + { 40 + "id": "550e8400-e29b-41d4-a716-446655440000", 41 + "did": "did:plc:newuser", 42 + "is_super": false, 43 + "permissions": ["lexicons:read", "records:read", "script-variables:read", "users:read", "api-keys:read", "api-keys:create", "api-keys:delete", "backfill:read", "backfill:create", "stats:read", "events:read"] 44 + } 45 + ``` 46 + 47 + ## List users 48 + 49 + ``` 50 + GET /admin/users 51 + ``` 52 + 53 + Requires `users:read` permission. 54 + 55 + ```sh 56 + curl http://localhost:3000/admin/users -H "$AUTH" 57 + ``` 58 + 59 + **Response**: `200 OK` 60 + 61 + ```json 62 + [ 63 + { 64 + "id": "550e8400-e29b-41d4-a716-446655440000", 65 + "did": "did:plc:admin", 66 + "is_super": true, 67 + "permissions": ["lexicons:create", "lexicons:read", "lexicons:delete", "records:read", "records:delete", "records:delete-collection", "script-variables:create", "script-variables:read", "script-variables:delete", "users:create", "users:read", "users:update", "users:delete", "api-keys:create", "api-keys:read", "api-keys:delete", "backfill:create", "backfill:read", "stats:read", "events:read"], 68 + "created_at": "2025-01-01T00:00:00Z", 69 + "last_used_at": "2025-01-02T12:00:00Z" 70 + } 71 + ] 72 + ``` 73 + 74 + ## Get a user 75 + 76 + ``` 77 + GET /admin/users/{id} 78 + ``` 79 + 80 + Requires `users:read` permission. 81 + 82 + ```sh 83 + curl http://localhost:3000/admin/users/550e8400-e29b-41d4-a716-446655440000 -H "$AUTH" 84 + ``` 85 + 86 + **Response**: `200 OK` with the same shape as a single item from the list response. 87 + 88 + ## Update user permissions 89 + 90 + ``` 91 + PATCH /admin/users/{id}/permissions 92 + ``` 93 + 94 + Requires `users:update` permission. You cannot grant permissions you don't have yourself, and you cannot modify the super user's permissions. 95 + 96 + ```sh 97 + curl -X PATCH http://localhost:3000/admin/users/550e8400-e29b-41d4-a716-446655440000/permissions \ 98 + -H "$AUTH" \ 99 + -H "Content-Type: application/json" \ 100 + -d '{ 101 + "grant": ["lexicons:create", "lexicons:delete"], 102 + "revoke": ["records:delete"] 103 + }' 104 + ``` 105 + 106 + | Field | Type | Required | Description | 107 + | -------- | -------- | -------- | --------------------- | 108 + | `grant` | string[] | no | Permissions to add | 109 + | `revoke` | string[] | no | Permissions to remove | 110 + 111 + **Response**: `200 OK` with the updated user object. 112 + 113 + ## Transfer super user 114 + 115 + ``` 116 + POST /admin/users/transfer-super 117 + ``` 118 + 119 + Only the current super user can call this endpoint. Transfers super user status to another existing user. 120 + 121 + ```sh 122 + curl -X POST http://localhost:3000/admin/users/transfer-super \ 123 + -H "$AUTH" \ 124 + -H "Content-Type: application/json" \ 125 + -d '{ "target_user_id": "550e8400-e29b-41d4-a716-446655440000" }' 126 + ``` 127 + 128 + | Field | Type | Required | Description | 129 + | ---------------- | ------ | -------- | ------------------------------------------ | 130 + | `target_user_id` | string | yes | The ID of the user to receive super status | 131 + 132 + **Response**: `200 OK` 133 + 134 + ## Delete a user 135 + 136 + ``` 137 + DELETE /admin/users/{id} 138 + ``` 139 + 140 + Requires `users:delete` permission. You cannot delete the super user or yourself. 141 + 142 + ```sh 143 + curl -X DELETE http://localhost:3000/admin/users/550e8400-e29b-41d4-a716-446655440000 \ 144 + -H "$AUTH" 145 + ``` 146 + 147 + **Response**: `204 No Content`
+108 -99
packages/docs/docs/reference/architecture.md
··· 31 31 32 32 Reads flow top-down through the query handler to the database (SQLite by default, or Postgres). Writes flow through the procedure handler to the user's PDS, then HappyView indexes the record locally. Real-time record events stream in via [Jetstream](https://github.com/bluesky-social/jetstream); historical records are backfilled in-process by discovering repos via the relay's `listReposByCollection` and fetching records directly from each PDS. 33 33 34 - ## Module overview 35 - 36 - ``` 37 - src/ 38 - main.rs Startup: config, DB, migrations, build OAuth client, spawn Jetstream worker, start server 39 - lib.rs AppState struct (incl. OAuth client + cookie key), module declarations 40 - config.rs Environment variable loading 41 - dns.rs DNS TXT resolver for atrium handle resolution 42 - error.rs AppError enum (Auth, BadRequest, Forbidden, Internal, NotFound, PdsError) 43 - server.rs Axum router: fixed routes + admin nest + auth routes + XRPC catch-all + static files 44 - lexicon.rs ParsedLexicon, LexiconRegistry (Arc<RwLock<HashMap>>) 45 - profile.rs DID document resolution, PDS discovery, profile fetching 46 - jetstream.rs Jetstream WebSocket listener, collection filter sync, cursor persistence 47 - resolve.rs NSID authority resolution (DNS TXT → DID → PDS) 48 - auth/ 49 - mod.rs Re-exports, COOKIE_NAME constant 50 - middleware.rs Claims extractor (cookie auth, API key, or service auth JWT) 51 - routes.rs OAuth endpoints (/auth/login, /auth/callback, /auth/logout, /auth/me) 52 - oauth_store.rs Database-backed session and state stores for atrium-oauth 53 - service_auth.rs XRPC service-to-service JWT validation (ES256/ES256K) 54 - admin/ 55 - mod.rs Admin route definitions 56 - auth.rs UserAuth extractor (Claims + DID lookup + permission check + auto-bootstrap) 57 - users.rs User CRUD handlers (create, list, get, delete, update permissions, transfer super) 58 - permissions.rs Permission enum (20 permissions), templates (Viewer, Operator, Manager, FullAccess) 59 - api_keys.rs API key CRUD handlers (create, list, revoke) with scoped permissions 60 - events.rs Event log query handler 61 - settings.rs Instance settings CRUD handlers (list, upsert, delete, logo upload/serve) 62 - script_variables.rs Script variable CRUD handlers (list, upsert, delete) 63 - lexicons.rs Lexicon CRUD handlers 64 - network_lexicons.rs Network lexicon tracking (add, list, remove) 65 - records.rs Record listing handler 66 - stats.rs Record count stats 67 - backfill.rs Backfill job runner (relay discovery + per-PDS listRecords) 68 - types.rs Request/response structs for admin endpoints 69 - lua/ 70 - mod.rs Re-exports 71 - context.rs Lua context globals (method, params, input, caller_did, collection) 72 - db_api.rs Lua database API (db.query, db.get, db.count) 73 - execute.rs Script execution and sandbox setup 74 - record.rs Lua Record API (constructor, save, delete, load) 75 - sandbox.rs Restricted Lua environment (removed modules, instruction limit) 76 - tid.rs TID generation for Lua scripts 77 - repo/ 78 - mod.rs Re-exports 79 - pds.rs PDS proxy helpers (JSON POST, blob POST, response forwarding via OAuth session) 80 - session.rs OAuth session restoration from atrium store 81 - upload_blob.rs Blob upload handler 82 - xrpc/ 83 - mod.rs Re-exports 84 - query.rs Dynamic GET handler (Lua script or default: single record + list) 85 - procedure.rs Dynamic POST handler (Lua script or default: create vs put) 86 - ``` 87 - 88 34 ## Request flow 89 35 90 36 ### Reads (queries) 91 37 92 - ``` 93 - Client GET /xrpc/{method}?params 94 - -> xrpc::xrpc_get() 95 - -> LexiconRegistry lookup (must be Query type) 96 - -> If Lua script attached: execute script (has access to db API) 97 - -> Else: default SQL query on records table (collection from target_collection) 98 - -> JSON response 38 + ```mermaid 39 + sequenceDiagram 40 + participant C as Client 41 + participant X as xrpc_get() 42 + participant R as LexiconRegistry 43 + participant L as Lua Script 44 + participant D as Database 45 + 46 + C->>X: GET /xrpc/{method}?params 47 + X->>R: Lookup (must be Query type) 48 + alt Lua script attached 49 + R->>L: Execute script 50 + L->>D: db.query / db.get / db.raw 51 + D-->>L: Results 52 + L-->>X: Response table 53 + else No script 54 + R->>D: Default SQL query (collection from target_collection) 55 + D-->>X: Results 56 + end 57 + X-->>C: JSON response 99 58 ``` 100 59 101 60 ### Writes (procedures) 102 61 103 - ``` 104 - Client POST /xrpc/{method} + session cookie or Bearer token 105 - -> Claims extractor (cookie, API key, or service auth JWT) 106 - -> xrpc::xrpc_post() 107 - -> LexiconRegistry lookup (must be Procedure type) 108 - -> If Lua script attached: execute script (has access to Record API) 109 - -> Else: default create/update (auto-detect based on uri field) 110 - -> Restore OAuth session from atrium store (by DID) 111 - -> atrium handles DPoP proof generation and token refresh 112 - -> Proxy to user's PDS (createRecord or putRecord) 113 - -> Upsert record locally 114 - -> Forward PDS response 62 + ```mermaid 63 + sequenceDiagram 64 + participant C as Client 65 + participant A as Claims Extractor 66 + participant X as xrpc_post() 67 + participant R as LexiconRegistry 68 + participant L as Lua Script 69 + participant S as OAuth Session 70 + participant P as User PDS 71 + participant D as Database 72 + 73 + C->>A: POST /xrpc/{method} + DPoP auth + X-Client-Key 74 + A->>X: Validated claims 75 + X->>R: Lookup (must be Procedure type) 76 + alt Lua script attached 77 + R->>L: Execute script (Record API) 78 + L->>S: Record:save() 79 + else No script 80 + R->>S: Default create/update (auto-detect from uri field) 81 + end 82 + S->>P: Proxy write (createRecord or putRecord) 83 + P-->>S: PDS response 84 + S->>D: Upsert record locally 85 + S-->>C: Forward PDS response 115 86 ``` 116 87 117 88 ### Admin endpoints 118 89 119 - ``` 120 - Client request + session cookie or Bearer token 121 - -> AdminAuth extractor: 122 - 1. Claims validation (cookie, API key, or service auth JWT) 123 - 2. DID lookup in users table (auto-bootstrap super user if empty) 124 - 3. Permission check (403 if missing required permission) 125 - -> Admin handler 126 - -> JSON response 90 + ```mermaid 91 + sequenceDiagram 92 + participant C as Client 93 + participant A as AdminAuth Extractor 94 + participant U as Users Table 95 + participant H as Admin Handler 96 + participant D as Database 97 + 98 + C->>A: Request + Bearer token 99 + A->>A: Validate claims (API key or service auth JWT) 100 + A->>U: DID lookup 101 + alt Users table empty 102 + U-->>A: Auto-bootstrap as super user 103 + else User found 104 + U-->>A: Load permissions 105 + end 106 + A->>A: Permission check (403 if missing) 107 + A->>H: Authorized request 108 + H->>D: Database operation 109 + D-->>H: Result 110 + H-->>C: JSON response 127 111 ``` 128 112 129 113 ## Data flow 130 114 131 115 ### Real-time indexing 132 116 133 - ``` 134 - Jetstream WebSocket connection (jetstream::spawn) 135 - -> Collection filters built from indexed lexicons and applied to subscription URL 136 - -> Reconnects on collection filter changes (lexicon add/remove) 137 - -> Record commit events: 138 - create/update -> UPSERT into records table 139 - delete -> DELETE from records table 140 - -> Lexicon schema events (com.atproto.lexicon.schema): 141 - -> Update tracked network lexicons in DB and registry 142 - -> Cursor persisted to instance_settings for resume on reconnect 117 + ```mermaid 118 + sequenceDiagram 119 + participant J as Jetstream WebSocket 120 + participant H as HappyView 121 + participant D as Database 122 + participant R as LexiconRegistry 123 + 124 + H->>J: Connect (collection filters from indexed lexicons) 125 + loop Stream events 126 + J->>H: Record commit event 127 + alt create / update 128 + H->>D: UPSERT into records table 129 + else delete 130 + H->>D: DELETE from records table 131 + end 132 + end 133 + J->>H: Lexicon schema event (com.atproto.lexicon.schema) 134 + H->>D: Update tracked network lexicons 135 + H->>R: Update in-memory registry 136 + Note over H,D: Cursor persisted to instance_settings for resume on reconnect 137 + Note over H,J: Reconnects on collection filter changes (lexicon add/remove) 143 138 ``` 144 139 145 140 ### Backfill 146 141 147 - ``` 148 - POST /admin/backfill 149 - -> Create backfill_jobs record (status = running) 150 - -> Relay listReposByCollection -> list of DIDs (paginated) 151 - -> For each DID: resolve PDS via PLC, listRecords from that PDS (paginated) 152 - -> UPSERT each record into records table 153 - -> Update processed_repos / total_records counters 154 - -> Mark job as completed (or failed with error message) 142 + ```mermaid 143 + sequenceDiagram 144 + participant A as Admin 145 + participant H as HappyView 146 + participant D as Database 147 + participant Relay as Relay 148 + participant PLC as PLC Directory 149 + participant PDS as User PDS 150 + 151 + A->>H: POST /admin/backfill 152 + H->>D: Create backfill_jobs record (status = running) 153 + H->>Relay: listReposByCollection (paginated) 154 + Relay-->>H: List of DIDs 155 + loop For each DID 156 + H->>PLC: Resolve DID document 157 + PLC-->>H: PDS endpoint 158 + H->>PDS: listRecords (paginated) 159 + PDS-->>H: Records 160 + H->>D: UPSERT each record 161 + H->>D: Update processed_repos / total_records 162 + end 163 + H->>D: Mark job completed (or failed) 155 164 ``` 156 165 157 166 ## Database schema
+1 -1
packages/docs/docs/reference/changelog.md
··· 22 22 23 23 ## v1.9.0 — Event Logs 24 24 25 - - **Event logging** — system-wide audit trail for lexicon changes, record operations, Lua script executions/errors, admin actions, backfill jobs, and firehose connectivity 25 + - **Event logging** — system-wide audit trail for lexicon changes, record operations, Lua script executions/errors, admin actions, backfill jobs, and Jetstream connectivity 26 26 - **`GET /admin/events`** — query event logs with filtering by event type, category, severity, and subject, with cursor pagination 27 27 - **Lua error context** — script errors capture full debugging context: error message, script source, input payload, and caller DID 28 28 - **Automatic retention cleanup** — configurable via `EVENT_LOG_RETENTION_DAYS` (default 30 days)
+107
packages/docs/docs/reference/lua/atproto-api.md
··· 1 + # AT Protocol API 2 + 3 + The `atproto` table provides AT Protocol utility functions. Available in queries, procedures, and [index hooks](../../guides/index-hooks.md). 4 + 5 + ## atproto.resolve_service_endpoint 6 + 7 + ```lua 8 + local endpoint = atproto.resolve_service_endpoint(did) 9 + ``` 10 + 11 + Resolves a DID to its AT Protocol service endpoint URL by fetching the DID document. Supports both `did:plc:*` (via the PLC directory) and `did:web:*` (via `.well-known/did.json`). 12 + 13 + | Parameter | Type | Description | 14 + | --------- | ------ | ------------------------ | 15 + | `did` | string | The DID to resolve | 16 + 17 + **Returns:** The service endpoint URL as a string, or `nil` if resolution fails (DID not found, no PDS service in document, network error). 18 + 19 + ### Examples 20 + 21 + ```lua 22 + -- Resolve a did:plc DID 23 + local endpoint = atproto.resolve_service_endpoint("did:plc:abc123") 24 + -- endpoint = "https://pds.example.com" 25 + 26 + -- Resolve a did:web DID 27 + local endpoint = atproto.resolve_service_endpoint("did:web:example.com") 28 + -- endpoint = "https://example.com" 29 + 30 + -- Handle resolution failure 31 + local endpoint = atproto.resolve_service_endpoint("did:plc:unknown") 32 + if not endpoint then 33 + return { error = "Could not resolve DID" } 34 + end 35 + 36 + -- Use with HTTP API to call a remote XRPC endpoint 37 + local endpoint = atproto.resolve_service_endpoint(did) 38 + if endpoint then 39 + local resp = http.get(endpoint .. "/xrpc/com.example.method") 40 + local data = json.decode(resp.body) 41 + end 42 + ``` 43 + 44 + ## atproto.get_labels 45 + 46 + ```lua 47 + local labels = atproto.get_labels(uri) 48 + ``` 49 + 50 + Returns an array of labels for a single AT URI. Merges external labels (from subscribed labelers) with self-labels (from the record's `labels.values[]` field). 51 + 52 + | Parameter | Type | Description | 53 + | --------- | ------ | ------------------------------ | 54 + | `uri` | string | AT URI of the record to query | 55 + 56 + Each label in the array is a table with: 57 + 58 + | Field | Type | Description | 59 + | ----- | ------ | ---------------------------------------- | 60 + | `src` | string | DID of the labeler (or record author) | 61 + | `uri` | string | AT URI this label applies to | 62 + | `val` | string | Label value (e.g. "nsfw", "!hide") | 63 + | `cts` | string | Timestamp when the label was created | 64 + 65 + Expired labels are automatically filtered out. Returns an empty array if no labels exist. 66 + 67 + ## atproto.get_labels_batch 68 + 69 + ```lua 70 + local labels_by_uri = atproto.get_labels_batch(uris) 71 + ``` 72 + 73 + Batch version of `get_labels`. Takes an array of AT URIs and returns a table keyed by URI, where each value is an array of labels. 74 + 75 + | Parameter | Type | Description | 76 + | --------- | ----- | ------------------------ | 77 + | `uris` | table | Array of AT URI strings | 78 + 79 + **Returns:** A table keyed by URI. Each value is an array of label tables (same shape as `get_labels`). URIs with no labels have an empty array. 80 + 81 + ### Label examples 82 + 83 + ```lua 84 + -- Get labels for a single game 85 + local labels = atproto.get_labels("at://did:plc:abc/games.gamesgamesgamesgames.game/rkey1") 86 + for _, label in ipairs(labels) do 87 + if label.val == "!hide" then 88 + -- skip this game in feed results 89 + end 90 + end 91 + 92 + -- Batch fetch labels for multiple games (efficient for feed hydration) 93 + local uris = {} 94 + for _, item in ipairs(skeleton) do 95 + uris[#uris + 1] = item.game 96 + end 97 + 98 + local labels_by_uri = atproto.get_labels_batch(uris) 99 + for _, uri in ipairs(uris) do 100 + local labels = labels_by_uri[uri] 101 + for _, label in ipairs(labels) do 102 + if label.val == "!hide" then 103 + -- filter out this game 104 + end 105 + end 106 + end 107 + ```
+106
packages/docs/docs/reference/lua/database-api.md
··· 1 + # Database API 2 + 3 + The `db` table provides access to the database. Available in queries, procedures, and [index hooks](../../guides/index-hooks.md). 4 + 5 + ## db.query 6 + 7 + ```lua 8 + local result = db.query({ 9 + collection = "xyz.statusphere.status", -- required 10 + did = "did:plc:abc", -- optional: filter by DID 11 + limit = 20, -- optional: max 100, default 20 12 + cursor = params.cursor, -- optional: opaque cursor from a previous response 13 + sort = "name", -- optional: field to sort by, default "indexed_at" 14 + sortDirection = "asc", -- optional: "asc" or "desc", default "desc" 15 + }) 16 + 17 + -- result.records — array of record tables (each includes a "uri" field) 18 + -- result.cursor — present when more records exist (opaque string, pass back as-is) 19 + ``` 20 + 21 + The `cursor` is an opaque string returned in a previous response. Pass it through directly — don't parse or modify it. When no `sort` field is specified, `db.query` uses keyset pagination (based on `created_at` and `uri`), which is stable even when records are inserted between pages. When a custom `sort` field is specified, offset-based pagination is used instead. 22 + 23 + The `sort` field can be a top-level column (`indexed_at`, `did`, `uri`) or any field inside the record's `value` object (e.g. `name`, `createdAt`). Field names must contain only alphanumeric characters and underscores. 24 + 25 + ## db.get 26 + 27 + ```lua 28 + local record = db.get("at://did:plc:abc/xyz.statusphere.status/abc123") 29 + -- Returns the record table or nil 30 + -- The returned table includes a "uri" field 31 + ``` 32 + 33 + ## db.search 34 + 35 + ```lua 36 + local result = db.search({ 37 + collection = "xyz.statusphere.status", -- required 38 + field = "displayName", -- required: record field to search 39 + query = "alice", -- required: search term 40 + limit = 10, -- optional: max 100, default 10 41 + }) 42 + 43 + -- result.records — array of matching records, ranked by relevance: 44 + -- exact match > prefix match > contains match, then alphabetical 45 + ``` 46 + 47 + ## db.backlinks 48 + 49 + Find records that reference a given AT URI anywhere in their data. Useful for finding likes on a post, replies to a thread, or any record that links to another. 50 + 51 + ```lua 52 + local result = db.backlinks({ 53 + collection = "xyz.statusphere.status", -- required 54 + uri = "at://did:plc:abc/xyz.statusphere.status/foo", -- required: the URI to find references to 55 + did = "did:plc:abc", -- optional: filter by DID 56 + limit = 20, -- optional: max 100, default 20 57 + cursor = params.cursor, -- optional: opaque cursor from a previous response 58 + }) 59 + 60 + -- result.records — array of records whose data contains the given URI 61 + -- result.cursor — present when more records exist (opaque string, pass back as-is) 62 + ``` 63 + 64 + The search checks the full record data, so it works regardless of which field holds the reference (`subject`, `parent`, `reply.root`, etc.). 65 + 66 + ## db.count 67 + 68 + ```lua 69 + local n = db.count("xyz.statusphere.status") 70 + local n = db.count("xyz.statusphere.status", "did:plc:abc") -- filter by DID 71 + ``` 72 + 73 + ## db.raw 74 + 75 + Run a raw SQL query against the database. Supports `SELECT`, `INSERT`, `UPDATE`, `DELETE`, and `CREATE TABLE` statements. 76 + 77 + ```lua 78 + -- Read query 79 + local rows = db.raw( 80 + "SELECT uri, did, record FROM records WHERE collection = $1 AND did = $2 LIMIT $3", 81 + { "xyz.statusphere.status", "did:plc:abc", 10 } 82 + ) 83 + 84 + for _, row in ipairs(rows) do 85 + -- row.uri, row.did, row.record (JSONB is returned as a Lua table) 86 + end 87 + 88 + -- Write query (returns affected rows, if any) 89 + db.raw("CREATE TABLE IF NOT EXISTS my_table (id TEXT PRIMARY KEY, value TEXT NOT NULL)") 90 + db.raw("INSERT INTO my_table (id, value) VALUES ($1, $2) ON CONFLICT (id) DO UPDATE SET value = $2", 91 + { "key1", "hello" }) 92 + ``` 93 + 94 + Parameters are passed as an array and bound to `$1`, `$2`, etc. Supported parameter types: strings, integers, numbers, booleans, and nil. 95 + 96 + ### Column type mapping 97 + 98 + | Postgres type | Lua type | 99 + | ---------------------- | -------- | 100 + | `TEXT`, `VARCHAR` | string | 101 + | `INT4`, `INT8` | integer | 102 + | `FLOAT4`, `FLOAT8` | number | 103 + | `BOOL` | boolean | 104 + | `JSON`, `JSONB` | table | 105 + | `TIMESTAMPTZ` | string (ISO 8601) | 106 + | Other | string (fallback) |
+60
packages/docs/docs/reference/lua/http-api.md
··· 1 + # HTTP API 2 + 3 + The `http` table provides async HTTP client functions. Available in queries, procedures, and [index hooks](../../guides/index-hooks.md). 4 + 5 + ## Methods 6 + 7 + All methods take a URL and an optional options table, and return a [response table](#response). 8 + 9 + ```lua 10 + http.get(url, opts?) 11 + http.post(url, opts?) 12 + http.put(url, opts?) 13 + http.patch(url, opts?) 14 + http.delete(url, opts?) 15 + http.head(url, opts?) 16 + ``` 17 + 18 + ## Options 19 + 20 + The optional second argument is a table with: 21 + 22 + | Field | Type | Description | 23 + | --------- | ------ | ---------------------------------------------- | 24 + | `headers` | table | Request headers as key-value string pairs | 25 + | `body` | string | Request body (ignored for GET and HEAD) | 26 + 27 + ## Response 28 + 29 + Every method returns a table with: 30 + 31 + | Field | Type | Description | 32 + | --------- | ------- | ---------------------------------------------------- | 33 + | `status` | integer | HTTP status code | 34 + | `body` | string | Response body text (empty string for HEAD) | 35 + | `headers` | table | Response headers as key-value pairs (lowercase keys) | 36 + 37 + ## Examples 38 + 39 + ```lua 40 + -- Simple GET 41 + local resp = http.get("https://api.example.com/data") 42 + -- resp.status = 200, resp.body = "...", resp.headers["content-type"] = "application/json" 43 + 44 + -- GET with custom headers 45 + local resp = http.get("https://api.example.com/data", { 46 + headers = { ["authorization"] = "Bearer token123" } 47 + }) 48 + 49 + -- POST with JSON body 50 + local resp = http.post("https://api.example.com/hook", { 51 + body = '{"key": "value"}', 52 + headers = { ["content-type"] = "application/json" } 53 + }) 54 + 55 + -- PUT, PATCH, DELETE, HEAD follow the same pattern 56 + local resp = http.put(url, { body = data, headers = { ... } }) 57 + local resp = http.patch(url, { body = data, headers = { ... } }) 58 + local resp = http.delete(url, { headers = { ... } }) 59 + local resp = http.head(url) 60 + ```
+21
packages/docs/docs/reference/lua/json-api.md
··· 1 + # JSON API 2 + 3 + The `json` global provides JSON serialization and deserialization. Available in queries, procedures, and [index hooks](../../guides/index-hooks.md). 4 + 5 + ## json.encode 6 + 7 + ```lua 8 + local str = json.encode({ key = "value", items = { 1, 2, 3 } }) 9 + -- '{"key":"value","items":[1,2,3]}' 10 + ``` 11 + 12 + Converts a Lua table to a JSON string. 13 + 14 + ## json.decode 15 + 16 + ```lua 17 + local tbl = json.decode('{"key": "value"}') 18 + -- tbl.key == "value" 19 + ``` 20 + 21 + Parses a JSON string into a Lua table. Returns an error if the input is not valid JSON.
+84
packages/docs/docs/reference/lua/record-api.md
··· 1 + # Record API 2 + 3 + The `Record` API is only available in **procedure** scripts. It handles creating, updating, loading, and deleting AT Protocol records. Writes are proxied to the caller's PDS and indexed locally. 4 + 5 + ## Constructor 6 + 7 + ```lua 8 + local r = Record("xyz.statusphere.status", { status = "\ud83d\ude0a", createdAt = now() }) 9 + ``` 10 + 11 + Creates a new record instance for the given collection. The optional second argument sets initial field values. The record's `_key_type` is automatically set from the lexicon's `key` definition. Default values from the schema are populated for any missing fields. 12 + 13 + ## Static methods 14 + 15 + ```lua 16 + -- Save multiple records in parallel 17 + Record.save_all({ record1, record2, record3 }) 18 + 19 + -- Load a record from the local database by AT URI 20 + local r = Record.load("at://did:plc:abc/xyz.statusphere.status/abc123") 21 + -- Returns nil if not found 22 + 23 + -- Load multiple records in parallel 24 + local records = Record.load_all({ uri1, uri2 }) 25 + -- Returns nil entries for URIs not found 26 + ``` 27 + 28 + ## Instance methods 29 + 30 + ```lua 31 + -- Save (creates or updates depending on whether _uri is set) 32 + r:save() 33 + 34 + -- Delete from PDS and local database 35 + r:delete() 36 + 37 + -- Set the record key type (tid, any, nsid, or literal:*) 38 + r:set_key_type("tid") 39 + 40 + -- Set a specific record key 41 + r:set_rkey("my-key") 42 + 43 + -- Auto-generate a record key based on _key_type 44 + local key = r:generate_rkey() 45 + ``` 46 + 47 + **Key type behavior for `generate_rkey()`:** 48 + 49 + | Key type | Generated rkey | 50 + | --------------- | --------------------------------- | 51 + | `tid` | Sortable timestamp-based ID | 52 + | `any` | Same as `tid` | 53 + | `literal:value` | The literal value after the colon | 54 + | `nsid` | Error — use `set_rkey()` instead | 55 + 56 + ## Instance fields 57 + 58 + These fields are set automatically and are read-only (writes raise an error): 59 + 60 + | Field | Type | Description | 61 + | ------------- | ------- | ----------------------------------------------------------- | 62 + | `_uri` | string? | AT URI — set after `save()`, cleared after `delete()` | 63 + | `_cid` | string? | Content hash — set after `save()`, cleared after `delete()` | 64 + | `_key_type` | string? | Record key type from the lexicon definition | 65 + | `_rkey` | string? | Record key — set via `set_rkey()` or `generate_rkey()` | 66 + | `_collection` | string | Collection NSID (always set) | 67 + | `_schema` | table? | Schema definition from the lexicon (used for validation) | 68 + 69 + ## Schema validation 70 + 71 + When a record has a schema (loaded from the lexicon): 72 + 73 + - **On save:** required fields are checked, and missing required fields raise an error 74 + - **On construction:** default values from schema properties are auto-populated 75 + - **On save:** only fields defined in the schema's `properties` are sent to the PDS 76 + 77 + ## Save behavior 78 + 79 + `r:save()` auto-detects create vs update: 80 + 81 + - If `_uri` is nil → calls `createRecord` on the PDS 82 + - If `_uri` is set → calls `putRecord` on the PDS 83 + 84 + After a successful save, `_uri` and `_cid` are updated on the record instance.
+77
packages/docs/docs/reference/lua/standard-libraries.md
··· 1 + # Standard Libraries 2 + 3 + The following Lua 5.4 standard library modules and builtins are available in the HappyView sandbox. 4 + 5 + ## string 6 + 7 + - [`byte`](https://lua.org/manual/5.4/manual.html#pdf-string.byte) 8 + - [`char`](https://lua.org/manual/5.4/manual.html#pdf-string.char) 9 + - [`find`](https://lua.org/manual/5.4/manual.html#pdf-string.find) 10 + - [`format`](https://lua.org/manual/5.4/manual.html#pdf-string.format) 11 + - [`gmatch`](https://lua.org/manual/5.4/manual.html#pdf-string.gmatch) 12 + - [`gsub`](https://lua.org/manual/5.4/manual.html#pdf-string.gsub) 13 + - [`len`](https://lua.org/manual/5.4/manual.html#pdf-string.len) 14 + - [`lower`](https://lua.org/manual/5.4/manual.html#pdf-string.lower) 15 + - [`match`](https://lua.org/manual/5.4/manual.html#pdf-string.match) 16 + - [`rep`](https://lua.org/manual/5.4/manual.html#pdf-string.rep) 17 + - [`reverse`](https://lua.org/manual/5.4/manual.html#pdf-string.reverse) 18 + - [`sub`](https://lua.org/manual/5.4/manual.html#pdf-string.sub) 19 + - [`upper`](https://lua.org/manual/5.4/manual.html#pdf-string.upper) 20 + 21 + ## table 22 + 23 + - [`concat`](https://lua.org/manual/5.4/manual.html#pdf-table.concat) 24 + - [`insert`](https://lua.org/manual/5.4/manual.html#pdf-table.insert) 25 + - [`remove`](https://lua.org/manual/5.4/manual.html#pdf-table.remove) 26 + - [`sort`](https://lua.org/manual/5.4/manual.html#pdf-table.sort) 27 + - [`unpack`](https://lua.org/manual/5.4/manual.html#pdf-table.unpack) 28 + 29 + ## math 30 + 31 + - [`abs`](https://lua.org/manual/5.4/manual.html#pdf-math.abs) 32 + - [`ceil`](https://lua.org/manual/5.4/manual.html#pdf-math.ceil) 33 + - [`floor`](https://lua.org/manual/5.4/manual.html#pdf-math.floor) 34 + - [`max`](https://lua.org/manual/5.4/manual.html#pdf-math.max) 35 + - [`min`](https://lua.org/manual/5.4/manual.html#pdf-math.min) 36 + - [`random`](https://lua.org/manual/5.4/manual.html#pdf-math.random) 37 + - [`sqrt`](https://lua.org/manual/5.4/manual.html#pdf-math.sqrt) 38 + - [`huge`](https://lua.org/manual/5.4/manual.html#pdf-math.huge) 39 + - [`pi`](https://lua.org/manual/5.4/manual.html#pdf-math.pi) 40 + 41 + ## os (safe subset) 42 + 43 + Only the following safe functions are available from the `os` module: 44 + 45 + - [`time`](https://lua.org/manual/5.4/manual.html#pdf-os.time) 46 + - [`date`](https://lua.org/manual/5.4/manual.html#pdf-os.date) 47 + - [`difftime`](https://lua.org/manual/5.4/manual.html#pdf-os.difftime) 48 + - [`clock`](https://lua.org/manual/5.4/manual.html#pdf-os.clock) 49 + 50 + Dangerous functions like `os.execute`, `os.remove`, `os.rename`, and `os.exit` are not available. 51 + 52 + ## Builtins 53 + 54 + - [`print`](https://lua.org/manual/5.4/manual.html#pdf-print) 55 + - [`tostring`](https://lua.org/manual/5.4/manual.html#pdf-tostring) 56 + - [`tonumber`](https://lua.org/manual/5.4/manual.html#pdf-tonumber) 57 + - [`type`](https://lua.org/manual/5.4/manual.html#pdf-type) 58 + - [`pairs`](https://lua.org/manual/5.4/manual.html#pdf-pairs) 59 + - [`ipairs`](https://lua.org/manual/5.4/manual.html#pdf-ipairs) 60 + - [`next`](https://lua.org/manual/5.4/manual.html#pdf-next) 61 + - [`select`](https://lua.org/manual/5.4/manual.html#pdf-select) 62 + - [`unpack`](https://lua.org/manual/5.4/manual.html#pdf-table.unpack) 63 + - [`error`](https://lua.org/manual/5.4/manual.html#pdf-error) 64 + - [`pcall`](https://lua.org/manual/5.4/manual.html#pdf-pcall) 65 + - [`xpcall`](https://lua.org/manual/5.4/manual.html#pdf-xpcall) 66 + - [`assert`](https://lua.org/manual/5.4/manual.html#pdf-assert) 67 + - [`setmetatable`](https://lua.org/manual/5.4/manual.html#pdf-setmetatable) 68 + - [`getmetatable`](https://lua.org/manual/5.4/manual.html#pdf-getmetatable) 69 + - [`rawget`](https://lua.org/manual/5.4/manual.html#pdf-rawget) 70 + - [`rawset`](https://lua.org/manual/5.4/manual.html#pdf-rawset) 71 + - [`rawequal`](https://lua.org/manual/5.4/manual.html#pdf-rawequal) 72 + 73 + ## Removed modules 74 + 75 + The following standard Lua modules are **removed** and unavailable in the sandbox: 76 + 77 + `io`, `debug`, `package`, `require`, `dofile`, `loadfile`, `load`, `collectgarbage`
+2 -2
packages/docs/docs/reference/production-deployment.md
··· 4 4 5 5 ## Session secret 6 6 7 - Set `SESSION_SECRET` to a strong random value (at least 32 bytes). This signs the session cookies issued during OAuth login; rotating it invalidates every existing session. 7 + Set `SESSION_SECRET` to a random string of at least 64 characters. This signs the session cookies issued during OAuth login; rotating it invalidates every existing session. 8 8 9 9 ```sh 10 10 openssl rand -base64 48 ··· 46 46 47 47 HappyView has a per-client token-bucket rate limiter for XRPC endpoints. The defaults (set via `DEFAULT_RATE_LIMIT_CAPACITY` and `DEFAULT_RATE_LIMIT_REFILL_RATE`) apply to any [API client](../guides/api-keys.md) that doesn't have per-client overrides. Raise the defaults cautiously — they exist so one misbehaving integrator can't saturate the server. 48 48 49 - Per-client overrides are set at client creation or via `PUT /admin/api-clients/{id}` (see [Admin API — API Clients](admin-api.md#api-clients)). 49 + Per-client overrides are set at client creation or via `PUT /admin/api-clients/{id}` (see [Admin API — API Clients](admin/api-clients.md)). 50 50 51 51 ## Logging 52 52
+2 -2
packages/docs/docs/reference/scripts/batch-save.md
··· 23 23 24 24 ## How it works 25 25 26 - 1. Iterate over `input.items` and create a [`Record`](../../guides/scripting.md#record-api) instance for each item. 27 - 2. Call [`Record.save_all()`](../../guides/scripting.md#static-methods) to save all records in parallel, rather than one at a time. 26 + 1. Iterate over `input.items` and create a [`Record`](../lua/record-api.md) instance for each item. 27 + 2. Call [`Record.save_all()`](../lua/record-api.md#static-methods) to save all records in parallel, rather than one at a time. 28 28 3. Collect the resulting AT URIs and return them. 29 29 30 30 ## Usage
+1 -1
packages/docs/docs/reference/scripts/cascading-delete.md
··· 49 49 50 50 1. Load the primary record by URI. Return early if it doesn't exist. 51 51 2. Query for related records, in this example comments by the same user that reference the primary record's URI. 52 - 3. Load each related record with [`Record.load`](../../guides/scripting.md#static-methods) to get a deletable `Record` instance. 52 + 3. Load each related record with [`Record.load`](../lua/record-api.md#static-methods) to get a deletable `Record` instance. 53 53 4. Delete everything. Each `r:delete()` removes the record from the user's PDS and the local index. 54 54 55 55 ## Usage
+1 -1
packages/docs/docs/reference/scripts/complex-mutations.md
··· 56 56 57 57 ## How it works 58 58 59 - 1. Load the existing record with [`Record.load`](../../guides/scripting.md#static-methods). This gives you a mutable `Record` instance with all the current field values. 59 + 1. Load the existing record with [`Record.load`](../lua/record-api.md#static-methods). This gives you a mutable `Record` instance with all the current field values. 60 60 2. Apply transformations directly on the record's fields: 61 61 - **Increment a counter**: use `or 0` to handle the field being `nil` on first access. 62 62 - **Merge tags**: iterate over `input.tags`, skip duplicates already in `r.tags`, append new ones, then trim the list to 10.
+1 -1
packages/docs/docs/reference/scripts/create-record.md
··· 14 14 15 15 ## How it works 16 16 17 - 1. Create a new [`Record`](../../guides/scripting.md#record-api) instance from the target collection, populated with the fields from the request body. 17 + 1. Create a new [`Record`](../lua/record-api.md) instance from the target collection, populated with the fields from the request body. 18 18 2. Call `r:save()`, which creates the record on the caller's PDS and indexes it locally. 19 19 3. Return the AT URI and CID of the newly created record. 20 20
+4 -4
packages/docs/docs/reference/scripts/expanded-query.md
··· 13 13 collection = "xyz.statusphere.status", 14 14 did = params.did, 15 15 limit = limit, 16 - offset = tonumber(params.cursor) or 0, 16 + cursor = params.cursor, 17 17 }) 18 18 19 19 -- Collect unique DIDs from the statuses ··· 51 51 1. Query statuses from the target collection with pagination, same as a normal list query. 52 52 2. Extract the unique DIDs from the returned status URIs using `string.match`. 53 53 3. Build an AT URI for each DID's `app.bsky.actor.profile/self` record (this is where Bluesky profiles live). 54 - 4. Load all profiles in parallel with [`Record.load_all`](../../guides/scripting.md#static-methods). Profiles that aren't indexed locally return `nil` and are skipped. 54 + 4. Load all profiles in parallel with [`Record.load_all`](../lua/record-api.md#static-methods). Profiles that aren't indexed locally return `nil` and are skipped. 55 55 5. Return statuses and profiles as separate keys, with the cursor from the status query. 56 56 57 57 ## Usage ··· 59 59 ``` 60 60 GET /xrpc/xyz.statusphere.listStatusesWithProfiles?limit=10 61 61 GET /xrpc/xyz.statusphere.listStatusesWithProfiles?did=did:plc:abc 62 - GET /xrpc/xyz.statusphere.listStatusesWithProfiles?cursor=20&limit=20 62 + GET /xrpc/xyz.statusphere.listStatusesWithProfiles?cursor=<opaque>&limit=20 63 63 ``` 64 64 65 65 ```json ··· 72 72 { "uri": "at://did:plc:abc/app.bsky.actor.profile/self", "displayName": "Alice", "avatar": "..." }, 73 73 { "uri": "at://did:plc:def/app.bsky.actor.profile/self", "displayName": "Bob", "avatar": "..." } 74 74 ], 75 - "cursor": "10" 75 + "cursor": "MjAyNi0wMS0wMVQxMjowMDowMFp8YXQ6Ly9kaWQ6..." 76 76 } 77 77 ``` 78 78
+1 -1
packages/docs/docs/reference/scripts/get-record.md
··· 22 22 ## How it works 23 23 24 24 1. Check that the `uri` query parameter is present. Return a structured error if missing. 25 - 2. Look up the record with [`db.get`](../../guides/scripting.md#dbget), which returns the record table or `nil`. 25 + 2. Look up the record with [`db.get`](../lua/database-api.md#dbget), which returns the record table or `nil`. 26 26 3. Return the record wrapped in an object. 27 27 28 28 ## Usage
+3 -3
packages/docs/docs/reference/scripts/list-or-fetch.md
··· 18 18 collection = collection, 19 19 did = params.did, 20 20 limit = tonumber(params.limit) or 20, 21 - offset = tonumber(params.cursor) or 0, 21 + cursor = params.cursor, 22 22 }) 23 23 end 24 24 ``` 25 25 26 26 ## How it works 27 27 28 - 1. If a `uri` query parameter is provided, fetch that single record with [`db.get`](../../guides/scripting.md#dbget) and return it. If it doesn't exist, return a structured error (using `error()` would trigger a 500 response). 29 - 2. Otherwise, list records from the target collection using [`db.query`](../../guides/scripting.md#dbquery), with optional filtering by `did` and pagination via `limit`/`offset`. Since query parameters arrive as strings, `tonumber()` converts them to numbers. 28 + 1. If a `uri` query parameter is provided, fetch that single record with [`db.get`](../lua/database-api.md#dbget) and return it. If it doesn't exist, return a structured error (using `error()` would trigger a 500 response). 29 + 2. Otherwise, list records from the target collection using [`db.query`](../lua/database-api.md#dbquery), with optional filtering by `did` and cursor-based pagination. The `cursor` is an opaque string from a previous response — pass it through directly. Since `limit` arrives as a string, `tonumber()` converts it to a number. 30 30 31 31 ## Usage 32 32
+5 -5
packages/docs/docs/reference/scripts/paginated-list.md
··· 13 13 collection = collection, 14 14 did = params.did, 15 15 limit = limit, 16 - offset = tonumber(params.cursor) or 0, 16 + cursor = params.cursor, 17 17 }) 18 18 19 19 return result ··· 23 23 ## How it works 24 24 25 25 1. Parse `limit` from the query string, defaulting to 20 and capping at 100. 26 - 2. Call [`db.query`](../../guides/scripting.md#dbquery) with the target collection, optional DID filter, and offset-based pagination. 27 - 3. Return the result directly. `db.query` returns `{ records = [...], cursor = "..." }` where `cursor` is present when more records exist. 26 + 2. Call [`db.query`](../lua/database-api.md#dbquery) with the target collection, optional DID filter, and cursor for pagination. 27 + 3. Return the result directly. `db.query` returns `{ records = [...], cursor = "..." }` where `cursor` is an opaque string present when more records exist. 28 28 29 29 ## Usage 30 30 ··· 32 32 GET /xrpc/xyz.statusphere.listStatuses 33 33 GET /xrpc/xyz.statusphere.listStatuses?limit=50 34 34 GET /xrpc/xyz.statusphere.listStatuses?did=did:plc:abc&limit=10 35 - GET /xrpc/xyz.statusphere.listStatuses?cursor=20&limit=20 35 + GET /xrpc/xyz.statusphere.listStatuses?cursor=<opaque>&limit=20 36 36 ``` 37 37 38 38 ## Use case 39 39 40 - A straightforward list endpoint for feeds, timelines, or browsing records by collection. The `cursor` value returned by `db.query` is an offset. Clients pass it back as the `cursor` parameter to fetch the next page. Since all query parameters arrive as strings, use `tonumber()` to convert `limit` and `cursor` to numbers. 40 + A straightforward list endpoint for feeds, timelines, or browsing records by collection. The `cursor` value returned by `db.query` is an opaque string. Clients pass it back as the `cursor` parameter to fetch the next page — don't parse or modify it.
+1 -1
packages/docs/docs/reference/scripts/sidecar-records.md
··· 34 34 35 35 1. Generate a single [`TID()`](../../guides/scripting.md#utility-globals) to use as the rkey for both records. 36 36 2. Create a `Record` for each collection and call `r:set_rkey()` with the shared rkey. 37 - 3. Save both records in parallel with [`Record.save_all()`](../../guides/scripting.md#static-methods). 37 + 3. Save both records in parallel with [`Record.save_all()`](../lua/record-api.md#static-methods). 38 38 4. Return both URIs so the client knows the identity of each record. 39 39 40 40 ## Usage
+2 -2
packages/docs/docs/reference/scripts/update-or-delete.md
··· 30 30 31 31 ## How it works 32 32 33 - 1. If `input.delete` is truthy and `input.uri` is provided, load the record with [`Record.load`](../../guides/scripting.md#static-methods) and delete it. 34 - 2. If only `input.uri` is provided, load the existing record with [`Record.load`](../../guides/scripting.md#static-methods), update its fields, and save it back. Since `_uri` is already set, `r:save()` calls `putRecord` instead of `createRecord`. 33 + 1. If `input.delete` is truthy and `input.uri` is provided, load the record with [`Record.load`](../lua/record-api.md#static-methods) and delete it. 34 + 2. If only `input.uri` is provided, load the existing record with [`Record.load`](../lua/record-api.md#static-methods), update its fields, and save it back. Since `_uri` is already set, `r:save()` calls `putRecord` instead of `createRecord`. 35 35 3. If neither condition matches, create a new record from the input. 36 36 37 37 ## Usage
+1 -1
packages/docs/docs/reference/scripts/upsert-record.md
··· 33 33 ## How it works 34 34 35 35 1. Use the client-provided `input.rkey` if present, otherwise generate a new [`TID()`](../../guides/scripting.md#utility-globals). This means omitting `rkey` always creates, while providing one enables updates. 36 - 2. Build the AT URI from the caller's DID, the target collection, and the rkey, then try to load it with [`Record.load`](../../guides/scripting.md#static-methods). 36 + 2. Build the AT URI from the caller's DID, the target collection, and the rkey, then try to load it with [`Record.load`](../lua/record-api.md#static-methods). 37 37 3. If the record exists, update its fields and save. Since `_uri` is already set, `r:save()` calls `putRecord`. 38 38 4. If it doesn't exist, create a new record, set the rkey explicitly with `r:set_rkey()`, and save. This calls `createRecord` with the specified rkey. 39 39
+52 -4
packages/docs/docs/reference/troubleshooting.md
··· 28 28 29 29 **Causes**: 30 30 31 - - No session cookie or `Authorization: Bearer` header is present. 32 - - The session cookie has expired or was signed with a different `SESSION_SECRET`. 33 - - The API key has been revoked or is invalid. 31 + - No `Authorization: DPoP` header or `X-Client-Key` header is present. 32 + - The DPoP proof is invalid or expired. 33 + - The API client key is not registered or is inactive. 34 34 35 35 ## Admin endpoints return 403 Forbidden 36 36 ··· 86 86 - No record-type lexicon exists for the collection. HappyView only indexes collections that have a corresponding record-type lexicon. 87 87 - The Jetstream subscription hasn't reconnected with the new collection filter after a lexicon change. This should happen automatically. Check server logs for connection errors. 88 88 89 + ## Lua script can't find records 90 + 91 + **Symptom**: `db.query` or `db.get` returns empty results inside a Lua script, even though the admin dashboard shows records exist. 92 + 93 + **Causes**: 94 + 95 + - The `collection` global is only set when the lexicon has a `target_collection`. If you're using `db.raw` with a hardcoded collection name, double-check the spelling matches exactly. 96 + - `db.get` expects a full AT URI (`at://did:plc:abc/collection/rkey`), not just an rkey. 97 + - If querying by DID, make sure you're passing the full DID string including the `did:plc:` or `did:web:` prefix. 98 + 99 + ## Plugin secrets not working 100 + 101 + **Symptom**: A plugin fails with authentication errors even though you've configured its secrets. 102 + 103 + **Causes**: 104 + 105 + - `TOKEN_ENCRYPTION_KEY` is not set. Plugin secrets are encrypted at rest and cannot be read without this key. See [Plugins - Configuration](../guides/plugins.md#plugin-configuration). 106 + - If `TOKEN_ENCRYPTION_KEY` changed since the secrets were saved, the existing encrypted values are unreadable. Re-enter the secrets via the dashboard or `PUT /admin/plugins/{id}/secrets`. 107 + - Environment variable secrets (`PLUGIN_<ID>_<KEY>`) are overridden by dashboard-configured secrets. If you've set both, the dashboard values take precedence. 108 + 89 109 ## OAuth or login issues 90 110 91 111 HappyView handles AT Protocol OAuth internally via the `atrium-oauth` library. If users can't log in: 92 112 93 113 1. Verify `PUBLIC_URL` is set correctly and the URL is publicly accessible (required for OAuth callbacks). 94 114 2. Check that the user's PDS authorization server is reachable. 95 - 3. Verify `SESSION_SECRET` hasn't changed since sessions were created (changing it invalidates all existing session cookies). 115 + 3. Verify `SESSION_SECRET` hasn't changed since sessions were created (changing it invalidates all existing dashboard sessions). 96 116 4. Check server logs for OAuth-specific error messages. 97 117 118 + ## Third-party app can't authenticate 119 + 120 + **Symptom**: A third-party app using DPoP authentication gets 401 errors on XRPC endpoints. 121 + 122 + **Causes**: 123 + 124 + - The app hasn't registered an API client. Every XRPC request needs an `X-Client-Key` header with a valid `hvc_`-prefixed client key. Register one via **Settings > API Clients** or `POST /admin/api-clients`. 125 + - The DPoP proof is malformed or expired. Proofs include a timestamp and are valid for a short window. 126 + - The API client has been deactivated (`is_active: false`). Re-enable it via the dashboard or `PUT /admin/api-clients/{id}`. 127 + 98 128 ## Database connection errors 99 129 100 130 **Symptom**: HappyView fails to start or returns 500 errors. ··· 106 136 - Postgres version is too old. HappyView requires Postgres 17+. 107 137 108 138 See [Configuration](../getting-started/configuration.md) for environment variable details. 139 + 140 + ## Switching databases loses data 141 + 142 + **Symptom**: After changing `DATABASE_URL` from SQLite to Postgres (or vice versa), all records, lexicons, and users are gone. 143 + 144 + **Explanation**: Each database is independent. Switching `DATABASE_URL` points HappyView at a fresh database. Your old data is still in the previous database file or Postgres instance. 145 + 146 + **Recovery**: Re-upload your lexicons and run backfills to re-index records from the network. Admin settings, users, and API keys need to be re-created manually. See the [SQLite → Postgres](../guides/sqlite-to-postgres-migration.md) or [Postgres → SQLite](../guides/postgres-to-sqlite-migration.md) migration guides. 147 + 148 + ## Jetstream disconnects frequently 149 + 150 + **Symptom**: Server logs show repeated `jetstream.disconnected` / `jetstream.connected` events. 151 + 152 + **Causes**: 153 + 154 + - Network instability between HappyView and the Jetstream server. Verify `JETSTREAM_URL` is reachable. 155 + - The default Jetstream instance may be under heavy load. Consider pointing `JETSTREAM_URL` at a different instance if available. 156 + - HappyView reconnects automatically and resumes from its last cursor, so brief disconnections don't cause data loss. Prolonged outages may require a backfill to catch up on missed records.
+6 -6
packages/docs/docs/reference/xrpc-api.md
··· 7 7 ## Auth 8 8 9 9 - **Queries** (`GET /xrpc/{method}`): unauthenticated 10 - - **Procedures** (`POST /xrpc/{method}`): require authentication (session cookie, API key, or service auth JWT) 10 + - **Procedures** (`POST /xrpc/{method}`): require DPoP authentication (`Authorization: DPoP` + `DPoP` proof header + `X-Client-Key`) 11 11 - **getProfile**: requires auth 12 12 - **uploadBlob**: requires auth 13 13 ··· 101 101 ### List records 102 102 103 103 ``` 104 - GET /xrpc/{method}?limit=20&cursor=0&did=optional 104 + GET /xrpc/{method}?limit=20&cursor=<opaque>&did=optional 105 105 ``` 106 106 107 107 | Param | Type | Default | Description | 108 108 |-------|------|---------|-------------| 109 109 | `limit` | integer | 20 | Max records to return (max 100) | 110 - | `cursor` | string | `0` | Pagination cursor (opaque, pass from previous response) | 110 + | `cursor` | string | --- | Opaque pagination cursor from a previous response | 111 111 | `did` | string | --- | Filter records by DID | 112 112 113 113 ```sh ··· 125 125 "createdAt": "2025-01-01T12:00:00Z" 126 126 } 127 127 ], 128 - "cursor": "10" 128 + "cursor": "MjAyNS0wMS0wMVQxMjowMDowMFp8YXQ6Ly9kaWQ6..." 129 129 } 130 130 ``` 131 131 132 - The `cursor` field is present only when more records exist. 132 + The `cursor` field is an opaque string present only when more records exist. Pass it back as-is to fetch the next page. 133 133 134 134 ## Dynamic procedure endpoints 135 135 ··· 184 184 | Status | Meaning | Common causes | 185 185 |--------|---------|---------------| 186 186 | `400 Bad Request` | Invalid input | Missing required fields, malformed JSON, invalid AT URI | 187 - | `401 Unauthorized` | Authentication failed | Missing or invalid session cookie, API key, or service auth JWT | 187 + | `401 Unauthorized` | Authentication failed | Missing or invalid client identification or DPoP authentication | 188 188 | `404 Not Found` | Method or record not found | XRPC method has no matching lexicon, or the requested record doesn't exist | 189 189 | `500 Internal Server Error` | Server-side failure | Lua script error, database error, or upstream PDS failure | 190 190
+3 -3
packages/docs/docs/tutorials/statusphere.md
··· 125 125 "createdAt": "2025-01-01T11:30:00Z" 126 126 } 127 127 ], 128 - "cursor": "5" 128 + "cursor": "MjAyNS0wMS0wMVQxMjowMDowMFp8YXQ6Ly9kaWQ6..." 129 129 } 130 130 ``` 131 131 ··· 149 149 collection = collection, 150 150 did = params.did, 151 151 limit = tonumber(params.limit) or 20, 152 - offset = tonumber(params.cursor) or 0, 152 + cursor = params.cursor, 153 153 }) 154 154 end 155 155 ``` ··· 190 190 collection = collection, 191 191 did = params.did, 192 192 limit = tonumber(params.limit) or 20, 193 - offset = tonumber(params.cursor) or 0, 193 + cursor = params.cursor, 194 194 }) 195 195 end' 196 196
+114 -2
packages/docs/sidebars.ts
··· 191 191 id: "guides/plugins", 192 192 label: "Plugins", 193 193 }, 194 + { 195 + type: "doc", 196 + id: "guides/developing-plugins", 197 + label: "Developing Plugins", 198 + }, 194 199 ], 195 200 }, 196 201 { ··· 228 233 id: "guides/postgres-to-sqlite-migration", 229 234 label: "Postgres → SQLite Migration", 230 235 }, 236 + { 237 + type: "doc", 238 + id: "guides/sqlite-to-postgres-migration", 239 + label: "SQLite → Postgres Migration", 240 + }, 231 241 ], 232 242 }, 233 243 ], ··· 268 278 label: "XRPC API", 269 279 }, 270 280 { 271 - type: "doc", 272 - id: "reference/admin-api", 281 + type: "category", 273 282 label: "Admin API", 283 + items: [ 284 + { 285 + type: "doc", 286 + id: "reference/admin-api", 287 + label: "Overview", 288 + }, 289 + { 290 + type: "doc", 291 + id: "reference/admin/lexicons", 292 + label: "Lexicons", 293 + }, 294 + { 295 + type: "doc", 296 + id: "reference/admin/stats", 297 + label: "Stats", 298 + }, 299 + { 300 + type: "doc", 301 + id: "reference/admin/backfill", 302 + label: "Backfill", 303 + }, 304 + { 305 + type: "doc", 306 + id: "reference/admin/events", 307 + label: "Event Logs", 308 + }, 309 + { 310 + type: "doc", 311 + id: "reference/admin/api-keys", 312 + label: "API Keys", 313 + }, 314 + { 315 + type: "doc", 316 + id: "reference/admin/users", 317 + label: "Users", 318 + }, 319 + { 320 + type: "doc", 321 + id: "reference/admin/labelers", 322 + label: "Labelers", 323 + }, 324 + { 325 + type: "doc", 326 + id: "reference/admin/settings", 327 + label: "Instance Settings", 328 + }, 329 + { 330 + type: "doc", 331 + id: "reference/admin/domains", 332 + label: "Domains", 333 + }, 334 + { 335 + type: "doc", 336 + id: "reference/admin/script-variables", 337 + label: "Script Variables", 338 + }, 339 + { 340 + type: "doc", 341 + id: "reference/admin/api-clients", 342 + label: "API Clients", 343 + }, 344 + { 345 + type: "doc", 346 + id: "reference/admin/plugins", 347 + label: "Plugins", 348 + }, 349 + ], 350 + }, 351 + { 352 + type: "category", 353 + label: "Lua API", 354 + items: [ 355 + { 356 + type: "doc", 357 + id: "reference/lua/record-api", 358 + label: "Record API", 359 + }, 360 + { 361 + type: "doc", 362 + id: "reference/lua/database-api", 363 + label: "Database API", 364 + }, 365 + { 366 + type: "doc", 367 + id: "reference/lua/http-api", 368 + label: "HTTP API", 369 + }, 370 + { 371 + type: "doc", 372 + id: "reference/lua/atproto-api", 373 + label: "AT Protocol API", 374 + }, 375 + { 376 + type: "doc", 377 + id: "reference/lua/json-api", 378 + label: "JSON API", 379 + }, 380 + { 381 + type: "doc", 382 + id: "reference/lua/standard-libraries", 383 + label: "Standard Libraries", 384 + }, 385 + ], 274 386 }, 275 387 { 276 388 type: "doc",