···6677## Features
8899-- 📜 **Lexicon-Driven**: Upload your lexicon schemas and HappyView generates fully functional XRPC query and procedure endpoints automatically, no code required
1010-- 🔄 **Real-Time Sync**: Records stream in from the AT Protocol network in real-time via [Tap](https://github.com/bluesky-social/indigo/tree/main/cmd/tap), with cryptographic verification and backfill via the admin API
1111-- 🔐 **OAuth Built In**: AT Protocol OAuth is handled natively via `atrium-oauth`, and writes are proxied back to the user's PDS with automatic DPoP and token refresh
1212-- 🌙 **Lua Scripting**: Add custom query and procedure logic with Lua scripts that have full access to the record database
1313-- 🗄️ **Automatic Indexing**: HappyView indexes relevant records into PostgreSQL as they arrive, ready to query
1414-- 🪝 **Index Hooks**: Attach Lua scripts to record collections that fire on every create, update, or delete — sync to search engines, trigger webhooks, or build materialized views in real time
1515-- 🌐 **Network Lexicons**: Fetch lexicon schemas directly from the AT Protocol network via DNS authority resolution
1616-- ⚡ **Hot Reloading**: Upload or update lexicons at runtime, and new endpoints are available immediately with no restart
1717-- 🏷️ **Label Support**: Subscribe to external [labelers](guides/labelers.md) and surface content labels on records, with self-label detection and color-coded badges in the dashboard
1818-- 🔌 **Plugin System**: Extend HappyView with WASM [plugins](guides/plugins.md) that integrate with external platforms like Steam, Xbox, and itch.io
1919-- 🛠️ **Admin Dashboard**: Manage lexicons, monitor record stats, and run backfill jobs through a built-in admin API
99+- **Schema-driven endpoints.** Upload a [lexicon](guides/lexicons.md) and HappyView generates XRPC query and procedure routes, storage, and indexing from it — updatable at runtime with no restart.
1010+- **Network sync built in.** Real-time record streaming via [Jetstream](https://github.com/bluesky-social/jetstream), historical [backfill](guides/backfill.md) from each user's PDS, and AT Protocol OAuth with DPoP-bound proxy writes back to the PDS.
1111+- **Customize with Lua, hooks, and plugins.** [Lua scripts](guides/scripting.md) for query and procedure logic, [index hooks](guides/index-hooks.md) that fire on every record change, WASM [plugins](guides/plugins.md) for external platform integration, and [labeler](guides/labelers.md) subscriptions for content moderation.
1212+- **Protocol-native.** Works with any PDS, resolves DIDs through the directory, and fetches [network lexicons](guides/lexicons.md#network-lexicons) via DNS authority resolution.
1313+- **Full admin surface.** Built-in [dashboard](getting-started/dashboard.md) and [admin API](reference/admin-api.md) for managing lexicons, users, API keys, API clients, backfill jobs, and plugins.
20142115## Design Principles
2216
+87-27
docs/getting-started/authentication.md
···11# Authentication
2233-HappyView uses [AT Protocol OAuth](https://atproto.com/specs/oauth) for authentication, handled natively via the `atrium-oauth` library. HappyView manages the full OAuth flow internally — no external auth service is required.
33+HappyView has two distinct authentication surfaces:
44+55+- **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).
66+- **Admin API** (`/admin/*`) — user-level authentication via session cookies, admin API keys, or service auth JWTs, gated by [permissions](../guides/permissions.md).
77+88+## Which endpoints require what?
99+1010+| Endpoint type | Client identification | User authentication |
1111+| ----------------------------------- | ------------------------ | ------------------------------------------------------------------------------------ |
1212+| Queries (`GET /xrpc/{method}`) | `X-Client-Key` required | Optional — provide a session if the query needs to know who the user is |
1313+| Procedures (`POST /xrpc/{method}`) | `X-Client-Key` required | Required — a live OAuth session so HappyView can proxy writes to the user's PDS |
1414+| Admin API (`/admin/*`) | — | Required — must be a HappyView user with the right [permissions](../guides/permissions.md) |
1515+| Health check (`GET /health`) | — | — |
1616+1717+## XRPC: API client identification
41855-## Which endpoints require auth?
1919+Every XRPC request — including unauthenticated `GET` queries — must identify itself with a registered API client. The client key is HappyView's rate-limit bucket key and its way of knowing who is calling. A request without one returns `401 Unauthorized` with `Missing client identification`.
62077-| Endpoint type | Auth required? |
88-|---------------|---------------|
99-| Queries (`GET /xrpc/{method}`) | No |
1010-| Procedures (`POST /xrpc/{method}`) | Yes |
1111-| Admin API (`/admin/*`) | Yes (must be a user with appropriate [permissions](../guides/permissions.md)) |
1212-| Health check (`GET /health`) | No |
2121+Register a client in the dashboard (**Settings > API Clients > New client**) or via `POST /admin/api-clients`. You'll get back an `hvc_…` client key and an `hvs_…` client secret — **the secret is only shown once**, so capture it immediately.
2222+2323+HappyView resolves the client key from the first of:
2424+2525+1. The session cookie, if the user logged in through this client's OAuth flow (the cookie carries the `client_key` that minted it).
2626+2. The `X-Client-Key` request header.
2727+3. A `client_key` query-string parameter.
13281414-Authentication uses signed session cookies set during the OAuth login flow. For programmatic access, API keys (prefixed `hv_`) are also supported via the `Authorization: Bearer` header.
2929+On top of the client key, HappyView does best-effort validation that the caller actually controls the client:
15301616-## Logging in via the dashboard
3131+- If an `Origin` header is present (typical for browser apps), it must match the client's registered `client_uri`.
3232+- Otherwise, an `X-Client-Secret` header may be supplied and must match the stored secret (typical for server-to-server callers).
17331818-1. Open the dashboard and click **Log in**
1919-2. Enter your AT Protocol handle (e.g. `user.bsky.social`)
2020-3. You'll be redirected to your identity provider's authorization page
2121-4. After approving, you're redirected back to HappyView with a session cookie set
3434+Both checks currently log warnings on mismatch rather than rejecting the request, but the intent is clear: don't share client keys, and treat the secret like a password.
22352323-The session cookie is HttpOnly and signed. It persists across browser sessions until you log out or the OAuth session expires.
3636+### Calling a query
24372525-## Programmatic access
3838+```sh
3939+curl 'https://happyview.example.com/xrpc/com.example.feed.getHot' \
4040+ -H 'X-Client-Key: hvc_a1b2c3...'
4141+```
26422727-For scripts or CI/CD pipelines, use [API keys](../guides/api-keys.md) instead of OAuth:
4343+For a server-to-server integration, add the secret:
4444+4545+```sh
4646+curl 'https://happyview.example.com/xrpc/com.example.feed.getHot' \
4747+ -H 'X-Client-Key: hvc_a1b2c3...' \
4848+ -H 'X-Client-Secret: hvs_d4e5f6...'
4949+```
5050+5151+### Logging a user in so you can call procedures
5252+5353+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. The shape of the flow:
5454+5555+1. Publish a client metadata document at your API client's `client_id_url`.
5656+2. Redirect the user to HappyView's OAuth authorize endpoint with your `hvc_…` key as `client_id`.
5757+3. Exchange the authorization code at the token endpoint using your client key + `hvs_…` secret.
5858+4. HappyView sets a signed session cookie containing the user's DID and your client key. Subsequent XRPC requests made with that cookie are automatically attributed to your client — you don't need to also send `X-Client-Key`.
5959+6060+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).
6161+6262+## Admin API: user authentication
6363+6464+Admin endpoints don't use API clients. They require a real HappyView user, identified by one of three methods:
6565+6666+### Session cookie (dashboard)
6767+6868+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.
6969+7070+### Admin API key
7171+7272+For automation — CI/CD, monitoring, cron jobs — create an [admin API key](../guides/api-keys.md) at **Settings > API Keys** or via `POST /admin/api-keys` and pass it as a bearer token:
28732974```sh
3075export TOKEN="hv_your-api-key-here"
···3277 -H "Authorization: Bearer $TOKEN"
3378```
34793535-API keys are created via the dashboard or `POST /admin/api-keys`. See the [API Keys guide](../guides/api-keys.md) for details.
8080+A key only carries the permissions selected at creation time and can never exceed the permissions of the user who created it. Admin API keys are not valid for XRPC endpoints — they exist solely for admin API access.
8181+8282+### Service auth JWT
8383+8484+HappyView also accepts standard AT Protocol inter-service auth JWTs in the `Authorization` header. Another AppView, relay, or PDS can sign a short-lived ES256 or ES256K JWT with its DID's signing key; HappyView resolves the issuer's DID document, verifies the signature against the `#atproto` verification method, and treats the issuer DID as the caller identity.
8585+8686+For a service auth JWT to validate:
36873737-## How authentication works
8888+- `alg` must be `ES256` or `ES256K`.
8989+- `typ` must not be `at+jwt`, `refresh+jwt`, or `dpop+jwt` (those are other token types, not inter-service JWTs).
9090+- `exp` must be in the future.
9191+- The signature must verify against the issuer DID's atproto signing key.
38923939-HappyView supports three authentication methods:
9393+As with the other methods, the resolved DID still has to exist in the HappyView `users` table with the right permissions to hit admin endpoints — service auth gets you identified, not privileged.
40944141-1. **Session cookie** (web UI) — Set during the OAuth callback flow. The signed cookie contains the user's DID, which HappyView reads on each request.
4242-2. **API key** (programmatic) — Bearer tokens starting with `hv_`. HappyView looks up the key hash in the database to resolve the caller's DID and permissions.
4343-3. **Service auth JWT** (AT Protocol inter-service) — Standard AT Protocol service authentication via signed JWTs. HappyView validates the signature by resolving the issuer's DID document.
9595+### Admin access and the first user
44964545-For write operations (procedures), HappyView uses the stored OAuth session to proxy writes to the user's PDS. The `atrium-oauth` library handles DPoP proof generation and token refresh automatically.
9797+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.
46984747-## Admin access
9999+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.
100100+101101+## Proxying procedures to the user's PDS
481024949-Admin endpoints require the authenticated user's DID to exist in the `users` table with the appropriate [permissions](../guides/permissions.md). If the table is empty (fresh deployment), the first authenticated request to any admin endpoint auto-bootstraps that user as the **super user** with all permissions granted.
103103+When a client calls an XRPC procedure that writes a record, HappyView proxies the write to the user's PDS using the user's stored OAuth session. `atrium-oauth` attaches a DPoP proof and a DPoP-bound access token to the outbound request automatically — HappyView doesn't do any manual DPoP handling.
501045151-To add more users, 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.
105105+This only works if HappyView has a live OAuth session for the caller, which in practice means the caller logged in through the dashboard or through an API client's OAuth flow. A request that only carries an `X-Client-Key` header (no session cookie) can hit queries but can't be used to proxy writes — there's no user to write as. Service auth JWTs and admin API keys similarly don't carry a user OAuth session.
106106+107107+## Next steps
108108+109109+- [Permissions](../guides/permissions.md) — full list of permissions and what each one grants
110110+- [API Keys](../guides/api-keys.md) — create scoped admin API keys for automation
111111+- [Admin API — API Clients](../reference/admin-api.md#api-clients) — register API clients and configure rate limits
+16-4
docs/getting-started/configuration.md
···1212| `SESSION_SECRET` | no | dev default | Secret key for signing session cookies. **Must be set in production** |
1313| `HOST` | no | `0.0.0.0` | Bind host |
1414| `PORT` | no | `3000` | Bind port |
1515-| `TAP_URL` | no | `http://localhost:2480` | [Tap](https://github.com/bluesky-social/indigo/tree/main/cmd/tap) instance URL for real-time record streaming and backfill |
1616-| `TAP_ADMIN_PASSWORD` | no | --- | Shared secret for authenticating with Tap's admin endpoints |
1515+| `JETSTREAM_URL` | no | `wss://jetstream1.us-east.bsky.network` | Jetstream WebSocket URL for real-time record streaming |
1716| `RELAY_URL` | no | `https://bsky.network` | Relay URL for [backfill](../guides/backfill.md) repo discovery |
1817| `PLC_URL` | no | `https://plc.directory` | [PLC directory](https://github.com/did-method-plc/did-method-plc) URL for DID resolution |
1818+| `STATIC_DIR` | no | `./web/out` | Directory containing the built dashboard static assets |
1919| `EVENT_LOG_RETENTION_DAYS` | no | `30` | Number of days to keep event logs before automatic cleanup. Set to `0` to disable cleanup |
2020+| `TOKEN_ENCRYPTION_KEY` | no | --- | Base64-encoded 32-byte key for encrypting stored OAuth tokens. **Strongly recommended in production** |
2121+| `DEFAULT_RATE_LIMIT_CAPACITY` | no | `100` | Default token bucket capacity used when registering a new API client |
2222+| `DEFAULT_RATE_LIMIT_REFILL_RATE` | no | `2.0` | Default token bucket refill rate (tokens/second) for new API clients |
2023| `RUST_LOG` | no | `happyview=debug,tower_http=debug` | Log filter (uses `tracing_subscriber::EnvFilter`) |
2124| `APP_NAME` | no | --- | Application name shown on OAuth authorization screens. Overridden by database setting if set via admin API |
2225| `LOGO_URI` | no | --- | URL to application logo for OAuth screens. Overridden by database setting or logo upload |
···3740# Optional overrides
3841# HOST=0.0.0.0
3942# PORT=3000
4040-# TAP_URL=http://localhost:2480
4141-# TAP_ADMIN_PASSWORD=your-secret-here
4343+# JETSTREAM_URL=wss://jetstream1.us-east.bsky.network
4244# RELAY_URL=https://bsky.network
4345# PLC_URL=https://plc.directory
4646+# STATIC_DIR=./web/out
4447# EVENT_LOG_RETENTION_DAYS=30
4848+# TOKEN_ENCRYPTION_KEY=base64-encoded-32-byte-key
4949+# DEFAULT_RATE_LIMIT_CAPACITY=100
5050+# DEFAULT_RATE_LIMIT_REFILL_RATE=2.0
4551# RUST_LOG=happyview=debug,tower_http=debug
4652# APP_NAME=My App
4753# LOGO_URI=https://example.com/logo.png
4854# TOS_URI=https://example.com/tos
4955# POLICY_URI=https://example.com/privacy
5056```
5757+5858+## Next steps
5959+6060+- [Authentication](authentication.md) — set up OAuth and admin users
6161+- [Dashboard](dashboard.md) — explore the admin dashboard
6262+- [Production deployment](../reference/production-deployment.md) — deploy HappyView to production
+9-13
docs/getting-started/dashboard.md
···11# Dashboard
2233-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.
44-55-## Logging in for the first time
66-77-The dashboard uses AT Protocol OAuth. Click **Log in** and enter your handle to authenticate. If no users exist in the database yet, the first authenticated request to any admin endpoint automatically bootstraps that user as the super user with all permissions.
33+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.
8499-## Settings
1010-1111-The **Settings** page is organized into sub-pages accessible from the collapsible sidebar:
1212-1313-- **Users** — manage user accounts and permissions
1414-- **ENV Variables** — view and edit script variables
1515-- **API Keys** — create and revoke API keys
55+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.
166177## Adding a lexicon
188···29193020Toggle **Enable backfill** to index historical records when uploading a record-type lexicon.
31213232-**Network** lexicons are fetched from the AT Protocol network. Enter an NSID (e.g. `xyz.statusphere.status`) and HappyView resolves the schema automatically. If found, the lexicon JSON is displayed in a read-only editor. Click **Add** to track it. Network lexicons are kept up to date via Tap. See [Lexicons - Network lexicons](../guides/lexicons.md#network-lexicons) for how resolution works.
2222+**Network** lexicons are fetched from the AT Protocol network. Enter an NSID (e.g. `xyz.statusphere.status`) and HappyView resolves the schema automatically. If found, the lexicon JSON is displayed in a read-only editor. Click **Add** to track it. Network lexicons are kept up to date via the Jetstream subscription. See [Lexicons - Network lexicons](../guides/lexicons.md#network-lexicons) for how resolution works.
33233424### JSON editor
3525···4434The Lua editor provides context-aware code completions, including suggestions for the `Record`, `db`, `input`, and `params` APIs as well as Lua keywords, builtins, and standard library functions. It also offers snippet templates for common constructs like `if`, `for`, and `function`.
45354636See [Lua Scripting](../guides/scripting.md) for the full runtime reference and examples.
3737+3838+## Next steps
3939+4040+- [Lexicons](../guides/lexicons.md) — how lexicons drive HappyView's indexing and routing
4141+- [Lua Scripting](../guides/scripting.md) — write custom query and procedure logic
4242+- [Permissions](../guides/permissions.md) — manage user access to admin features
+9-14
docs/getting-started/deployment/docker.md
···11# Local Development with Docker
2233-This guide runs the full HappyView stack locally using Docker Compose: [Tap](https://github.com/bluesky-social/indigo/tree/main/cmd/tap), HappyView, and the web dashboard.
33+This guide runs HappyView and the dashboard locally using Docker Compose.
4455## Prerequisites
66···1414cp .env.example .env
1515```
16161717-Set `TAP_ADMIN_PASSWORD` in your `.env`. This shared secret is used by both Tap and HappyView:
1818-1919-```sh
2020-TAP_ADMIN_PASSWORD=your-secret-here
2121-```
2222-2323-The `docker-compose.yml` configures everything else (service connections) automatically. See the [database setup guide](../../guides/database-setup.md) if you want to use Postgres instead.
1717+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.
24182519## 2. Start the stack
2620···30243125This starts:
32263333-| Service | Port | Description |
3434-| ------------- | ---- | ---------------------------------- |
3535-| **tap** | 2480 | Firehose consumer, backfill worker |
3636-| **happyview** | 3000 | HappyView API server |
3737-| **web** | 3001 | Next.js dashboard |
2727+| Service | Port | Description |
2828+| ------------- | ---- | --------------------- |
2929+| **happyview** | 3000 | HappyView API server |
3030+| **web** | 3001 | Next.js dashboard |
38313932HappyView runs migrations automatically on startup. The first build will take a few minutes while Rust compiles.
3333+3434+The `happyview` container serves its own bundled dashboard at `http://localhost:3000`, but that copy is baked in at container build time and only updates when you rebuild the image. For day-to-day development, use the dev dashboard at `http://localhost:3001` — it hot-reloads on changes to the `web/` source.
40354136:::tip
4242-To use Postgres instead of SQLite, uncomment the `postgres` service in `docker-compose.yml` and update `DATABASE_URL` in `.env`. See the [database setup guide](../../guides/database-setup.md).
3737+SQLite is the default and requires no extra services. To use Postgres instead, uncomment the `postgres` service in `docker-compose.yml` and update `DATABASE_URL` in `.env`. See the [database setup guide](../../guides/database-setup.md).
4338:::
44394540## Next steps
+1-4
docs/getting-started/deployment/other.md
···11# Local Development from Source
2233-This guide runs HappyView directly with `cargo run`, with you managing Tap separately. If you'd rather use Docker Compose to run everything together, see [Local Development with Docker](docker.md).
33+This guide runs HappyView directly with `cargo run`. If you'd rather use Docker Compose, see [Local Development with Docker](docker.md).
4455## Prerequisites
6677- Rust (stable)
88-- A running [Tap](https://github.com/bluesky-social/indigo/tree/main/cmd/tap) instance (delivers real-time records and handles backfill). See the [Tap documentation](https://github.com/bluesky-social/indigo/tree/main/cmd/tap) for setup.
98- (Optional) PostgreSQL 17+ if you prefer Postgres over the default SQLite
1091110## 1. Clone and configure
···2322DATABASE_URL=sqlite://data/happyview.db?mode=rwc
2423PUBLIC_URL=http://localhost:3000
2524SESSION_SECRET=change-me-in-production
2626-TAP_URL=http://localhost:2480
2727-TAP_ADMIN_PASSWORD=your-secret-here
2825```
29263027Or if you prefer Postgres:
+7-1
docs/getting-started/deployment/railway.md
···11# Deploy on Railway
2233-The fastest way to get HappyView running is with Railway. This template deploys HappyView, [Tap](https://github.com/bluesky-social/indigo/tree/main/cmd/tap) (real-time data and backfill), and Postgres with a single click:
33+The fastest way to get HappyView running is with Railway. This template deploys HappyView and Postgres with a single click:
4455[](https://railway.com/deploy/happyview?referralCode=0QOgj_)
66···1616 :::
171718183. Access your HappyView dashboard at the instance's public URL. The first user to log in is automatically bootstrapped as the super user.
1919+2020+## Next steps
2121+2222+- [Configuration](../configuration.md) — full list of environment variables
2323+- [Dashboard](../dashboard.md) — manage lexicons, users, and plugins via the web UI
2424+- [Production deployment](../../reference/production-deployment.md) — hardening checklist for production instances
+4-8
docs/getting-started/quickstart.md
···8899| Option | Best for |
1010| ------------------------------------------ | ------------------------------------------------------------------------------------ |
1111-| [**Railway**](deployment/railway.md) | Fastest path — one-click deploy of the full stack (HappyView + Tap + Postgres) |
1111+| [**Railway**](deployment/railway.md) | Fastest path — one-click deploy of HappyView + Postgres |
1212| [**Docker Compose**](deployment/docker.md) | Local development with the full stack in containers |
1313| [**From source**](deployment/other.md) | Running HappyView with `cargo run` and managing dependencies yourself |
1414···16161717## 2. Log in to the dashboard
18181919-Open your HappyView instance in a browser. The built-in [dashboard](dashboard.md) is served at the root URL.
2020-2121-Click **Log in** and authenticate with your AT Protocol identity. On a fresh deployment with no users configured, the first authenticated request to any admin endpoint automatically bootstraps that user as the **super user** with all permissions granted.
1919+The built-in [dashboard](dashboard.md) is served at your instance's root URL. Log in with your AT Protocol identity — on a fresh deployment, the first handle to authenticate is automatically bootstrapped as the **super user** with all permissions, so use the handle you want to own the instance.
22202321## 3. Add your first lexicon
2422···29273. HappyView resolves the schema from the AT Protocol network and shows a preview
30284. Click **Add**
31293232-HappyView immediately starts indexing records for that collection. A backfill job is created to fetch historical records, and new records stream in via Tap in real time.
3030+HappyView immediately starts indexing records for that collection. A backfill job is created to fetch historical records, and new records stream in via Jetstream in real time.
33313432You can also upload lexicons manually via the dashboard or the [admin API](../reference/admin-api.md). See [Lexicons](../guides/lexicons.md) for the full details.
35333634## 4. Verify records are being indexed
37353838-Go to the **Dashboard** home page. The stat cards show the total record count and a breakdown by collection. You can also browse indexed records on the **Records** page.
3939-4040-To check backfill progress, go to the **Backfill** page. The Tap stats cards show how many repos and records Tap has processed.
3636+The dashboard home shows a live record count and a per-collection breakdown. For a deeper look, browse **Records** to inspect individual rows or **Backfill** to watch the historical fetch job drain.
41374238## 5. Query your data
4339
+9-9
docs/guides/backfill.md
···11# Backfill
2233-When you add a new record-type lexicon, HappyView starts indexing new records from that moment via [Tap](https://github.com/bluesky-social/indigo/tree/main/cmd/tap). But what about records that already exist on the network? That's what backfill does: HappyView discovers repos via the relay and delegates the actual record fetching to Tap.
33+When you add a new record-type lexicon, HappyView starts indexing new records from that moment via [Jetstream](https://github.com/bluesky-social/jetstream). But what about records that already exist on the network? That's what backfill does: HappyView discovers repos via the relay and fetches records directly from each user's PDS.
4455## When backfill runs
66···1212## How it works
131314141. **Determine target collections**: uses the specified collection, or all record lexicons with `backfill: true`
1515-2. **Discover DIDs**: HappyView calls the relay's `com.atproto.sync.listReposByCollection` to find repos that contain records for each target collection (paginated, 1000 per page)
1616-3. **Delegate to Tap**: HappyView sends discovered DIDs to Tap in batches of 1000 via its `/repos/add` endpoint
1717-4. **Tap fetches records**: Tap handles the actual record fetching from each user's PDS and delivers them to HappyView via the WebSocket channel
1515+2. **Discover DIDs**: HappyView calls the relay's `com.atproto.sync.listReposByCollection` to find repos that contain records for each target collection (paginated)
1616+3. **Resolve each PDS**: for each discovered DID, HappyView resolves the DID document via PLC to find the user's PDS endpoint
1717+4. **Fetch records**: HappyView calls `com.atproto.repo.listRecords` on each PDS for the target collection (paginated) and upserts each record into the local database
1818+5. **Track progress**: counters for `processed_repos` and `total_records` are updated as the job runs
18191920## Job lifecycle
20212121-HappyView marks a backfill job as "completed" once it finishes discovering repos and handing DIDs off to Tap (steps 1-3). This does **not** mean Tap has finished processing all the records. Tap works through them asynchronously after the handoff.
2222+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.
22232323-To see whether Tap is still working through the backlog, check the Tap stats on the dashboard's Backfill page or via `GET /admin/tap/stats`. The **outbox buffer** indicates how many events are still queued for delivery; a high number means Tap is actively processing.
2424+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).
24252526## Re-running backfills
26272727-Re-running a backfill for a collection that's already been backfilled is safe. HappyView removes the discovered repos from Tap before re-adding them, which clears Tap's cached state and forces a full re-fetch of all records from each repo's PDS. This means re-running a backfill will restore any records that were previously deleted from HappyView, as well as pick up repos that were added to the network since the last run.
2828+Re-running a backfill for a collection that's already been backfilled is safe. Each record is upserted by its AT URI, so existing records are refreshed in place and any new records discovered since the last run are added. This also picks up new repos that have joined the network since the previous backfill.
28292930## Restoring deleted records
30313131-Deleting records from HappyView (via the dashboard or API) only removes them from the local database — the records still exist on the AT Protocol network. To restore deleted records, create a backfill job for the affected collection. The backfill will clear Tap's cache for the discovered repos and re-fetch all records from the network, restoring any that were previously deleted.
3232+Deleting records from HappyView (via the dashboard or API) only removes them from the local database — the records still exist on the AT Protocol network. To restore deleted records, create a backfill job for the affected collection. The backfill will re-discover the repos and re-fetch all records from each PDS, restoring any that were previously deleted.
32333334## Next steps
34353536- [Lexicons](lexicons.md#backfill-flag): Control whether lexicons trigger backfill on upload
3637- [Admin API](../reference/admin-api.md#backfill): Full reference for backfill endpoints
3737-- [Admin API - Tap Stats](../reference/admin-api.md#tap-stats): Monitor Tap's processing progress
+7-1
docs/guides/database-setup.md
···5252The default `docker-compose.yml` ships with the Postgres service commented out. To use Postgres:
535354541. Uncomment the `postgres` service and `pgdata` volume in `docker-compose.yml`
5555-2. Uncomment the `depends_on: postgres` blocks in the `tap` and `happyview` services
5555+2. Uncomment the `depends_on: postgres` block in the `happyview` service
56563. Update `DATABASE_URL` in `.env`:
5757 ```sh
5858 DATABASE_URL=postgres://happyview:happyview@postgres/happyview
···6969Both backends support the same Lua database API (`db.query`, `db.get`, `db.count`). Write SQL in **SQLite syntax** by default. If you are using Postgres, HappyView automatically translates common SQLite patterns to Postgres equivalents at runtime.
70707171If you are migrating existing Lua scripts from Postgres SQL syntax to SQLite syntax, see the [Postgres to SQLite migration guide](postgres-to-sqlite-migration.md).
7272+7373+## Next steps
7474+7575+- [Postgres → SQLite migration](postgres-to-sqlite-migration.md) — switch an existing instance from Postgres to SQLite
7676+- [Lua scripting](scripting.md) — write queries that target either backend
7777+- [Configuration](../getting-started/configuration.md) — `DATABASE_URL` and related variables
+12-6
docs/guides/event-logs.md
···11# Event Logs
2233-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 Tap connectivity. Events are stored in a Postgres table and queryable via the [admin API](../reference/admin-api.md#event-logs).
33+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).
4455## Event types
66···2323| `record.created` | info | Record AT URI | `collection`, `did`, `rkey` |
2424| `record.deleted` | info | Record AT URI | `collection`, `did`, `rkey` |
25252626-Logged when records are received from Tap and stored or removed from the local database. These are system-triggered events (`actor_did` is null). If a database error occurs during the operation, the same event type is logged with `error` severity and the error message is included in the detail.
2626+Logged when records are received from Jetstream and stored or removed from the local database. These are system-triggered events (`actor_did` is null). If a database error occurs during the operation, the same event type is logged with `error` severity and the error message is included in the detail.
27272828### Script events
2929···91919292See [Backfill](backfill.md) for background on backfill jobs.
93939494-### Tap events
9494+### Jetstream events
95959696| Event Type | Severity | Subject | Detail |
9797|---|---|---|---|
9898-| `tap.connected` | info | — | `url` |
9999-| `tap.disconnected` | warn | — | `reason` |
9898+| `jetstream.connected` | info | — | `url` |
9999+| `jetstream.disconnected` | warn | — | `reason` |
100100101101-Logged when the WebSocket connection to [Tap](https://github.com/bluesky-social/indigo/tree/main/cmd/tap) is established or lost.
101101+Logged when the WebSocket connection to [Jetstream](https://github.com/bluesky-social/jetstream) is established or lost.
102102103103## Querying events
104104···127127Set `EVENT_LOG_RETENTION_DAYS=0` to disable automatic cleanup and keep logs indefinitely.
128128129129See [Configuration](../getting-started/configuration.md) for all environment variables.
130130+131131+## Next steps
132132+133133+- [Admin API — Event Logs](../reference/admin-api.md#event-logs) — full query parameters and response format
134134+- [Permissions](permissions.md) — control which users can read event logs
135135+- [Troubleshooting](../reference/troubleshooting.md) — using event logs to diagnose issues
+26-7
docs/guides/lexicons.md
···8899| Type | Effect |
1010| ------------- | ------------------------------------------------------------------------------ |
1111-| `record` | Syncs the collection filter to Tap and indexes records into Postgres. Supports [index hooks](index-hooks.md) |
1111+| `record` | Adds the collection to the Jetstream subscription filter and indexes records into the database. Supports [index hooks](index-hooks.md) |
1212| `query` | Registers a `GET /xrpc/{nsid}` endpoint that queries indexed records |
1313| `procedure` | Registers a `POST /xrpc/{nsid}` endpoint that proxies writes to the user's PDS |
1414| `definitions` | Stored but does not generate routes or subscriptions |
···31313232When uploading a record-type lexicon, HappyView automatically creates a backfill job to discover existing records. If you only want to index new records going forward, you can set `backfill` to `false`.
33333434-## Tap collection filters
3434+## Jetstream collection filters
35353636-When record-type lexicons change (uploaded or deleted), HappyView automatically syncs the updated collection filter to Tap. HappyView always includes `com.atproto.lexicon.schema` in the filter to track network lexicon updates.
3636+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.
37373838-Deleting a lexicon updates Tap's collection filters (stopping live indexing for that collection) but does **not** remove previously indexed repos or their cached state from Tap. To fully reset a collection's state, delete the lexicon, re-add it, and run a [backfill](backfill.md).
3838+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).
39394040## Network lexicons
41414242-If a lexicon has already been published, you don't need to upload the JSON manually. Point HappyView at the NSID and it fetches the lexicon directly from the network. Network lexicons are kept updated automatically via Tap. If the publisher updates their schema, your instance will pick up the change.
4242+If a lexicon has already been published, you don't need to upload the JSON manually. Point HappyView at the NSID and it fetches the lexicon directly from the network. Network lexicons are kept updated automatically via the Jetstream subscription. If the publisher updates their schema, your instance will pick up the change.
43434444### NSID authority resolution
4545···65656666The `value` field of the response is the raw lexicon JSON.
67676868-### Live updates via Tap
6868+### Live updates via Jetstream
69697070-Tap always subscribes to `com.atproto.lexicon.schema` alongside the dynamic record collections. When a record event arrives:
7070+The Jetstream subscription always includes `com.atproto.lexicon.schema` alongside the dynamic record collections. When a record event arrives:
71717272- **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.
7373- **delete**: The lexicon is removed from the `lexicons` table and registry.
···7575### Startup re-fetch
76767777On every startup, HappyView re-fetches all network lexicons from their respective PDSes. This ensures consistency even if events were missed while offline. Failures are logged as warnings but don't block startup.
7878+7979+## XRPC routing for unknown methods
8080+8181+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:
8282+8383+1. Extract the authority from the NSID (all segments except the last). `com.example.foo.getBar` → authority `com.example.foo`.
8484+2. Reverse it to form a domain: `foo.example.com`.
8585+3. Look up the `_lexicon.foo.example.com` TXT record for the authority's DID.
8686+4. Resolve that DID to a PDS endpoint via the PLC directory.
8787+5. Proxy the request to `{pds_endpoint}/xrpc/{method}`.
8888+8989+A few things to note:
9090+9191+- 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.
9292+- 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)).
9393+- 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.
9494+- "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`.
9595+9696+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.
78977998## Next steps
8099
+1-1
docs/guides/permissions.md
···5353|---|---|
5454| `backfill:create` | Start backfill jobs |
5555| `backfill:read` | View backfill job status |
5656-| `stats:read` | View record and Tap statistics |
5656+| `stats:read` | View record statistics |
5757| `events:read` | Query the event log |
58585959## Permission templates
+4
docs/guides/plugins.md
···119119| `GET /admin/plugins` | List installed plugins |
120120| `POST /admin/plugins` | Install a plugin |
121121| `POST /admin/plugins/preview` | Preview plugin before installing |
122122+| `GET /admin/plugins/official` | Browse the official plugin registry catalog |
122123| `DELETE /admin/plugins/{id}` | Remove a plugin |
123124| `POST /admin/plugins/{id}/reload` | Reload plugin from source |
125125+| `POST /admin/plugins/{id}/check-update` | Check whether a newer version is available |
124126| `GET /admin/plugins/{id}/secrets` | Get configured secrets (masked) |
125127| `PUT /admin/plugins/{id}/secrets` | Update plugin secrets |
128128+129129+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.
126130127131## Security
128132
+6
docs/guides/postgres-to-sqlite-migration.md
···8484## Rollback
85858686To switch back to Postgres, revert your `DATABASE_URL` to the Postgres connection string. Your Postgres database remains unchanged — HappyView does not modify it during the migration to SQLite.
8787+8888+## Next steps
8989+9090+- [Database setup](database-setup.md) — choose between SQLite and Postgres for new instances
9191+- [Backfill](backfill.md) — re-index records from the network after switching backends
9292+- [Lua scripting](scripting.md) — write SQL that works against either backend
+549-35
docs/reference/admin-api.md
···123123124124## Network Lexicons
125125126126-Network lexicons are fetched from the AT Protocol network via DNS TXT resolution and kept updated via Tap. See [Lexicons - Network lexicons](../guides/lexicons.md#network-lexicons) for background.
126126+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.
127127128128### Add a network lexicon
129129···218218}
219219```
220220221221-## Tap Stats
222222-223223-Aggregate stats from the [Tap](https://github.com/bluesky-social/indigo/tree/main/cmd/tap) instance. Useful for monitoring backfill progress. See [Backfill - Job lifecycle](../guides/backfill.md#job-lifecycle) for context.
224224-225225-### Get Tap stats
226226-227227-```
228228-GET /admin/tap/stats
229229-```
230230-231231-```sh
232232-curl http://localhost:3000/admin/tap/stats -H "$AUTH"
233233-```
234234-235235-**Response**: `200 OK`
236236-237237-```json
238238-{
239239- "repo_count": 5234,
240240- "record_count": 1048576,
241241- "outbox_buffer": 42
242242-}
243243-```
244244-245245-| Field | Type | Description |
246246-| --------------- | ------ | ----------------------------------------------------- |
247247-| `repo_count` | number | Total repos Tap is tracking |
248248-| `record_count` | number | Total records Tap has indexed |
249249-| `outbox_buffer` | number | Pending events awaiting delivery (high = Tap is busy) |
250250-251251-Returns `502 Bad Gateway` if Tap is unreachable.
252252-253221## Backfill
254222255223### Create a backfill job
···311279312280## Event Logs
313281314314-HappyView records an audit trail of system events: lexicon changes, record operations, Lua script executions and errors, user actions, backfill jobs, and Tap connectivity. See the [Event Logs guide](../guides/event-logs.md) for details on event types and retention.
282282+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.
315283316284### List event logs
317285···683651684652**Response**: `204 No Content`
685653654654+## Instance Settings
655655+656656+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.
657657+658658+### List settings
659659+660660+```
661661+GET /admin/settings
662662+```
663663+664664+```sh
665665+curl http://localhost:3000/admin/settings -H "$AUTH"
666666+```
667667+668668+Returns all key/value pairs stored in the `instance_settings` table.
669669+670670+### Upsert a setting
671671+672672+```
673673+PUT /admin/settings/{key}
674674+```
675675+676676+```sh
677677+curl -X PUT http://localhost:3000/admin/settings/app_name \
678678+ -H "$AUTH" \
679679+ -H "Content-Type: application/json" \
680680+ -d '{ "value": "My HappyView" }'
681681+```
682682+683683+### Delete a setting
684684+685685+```
686686+DELETE /admin/settings/{key}
687687+```
688688+689689+Removes the override; the corresponding environment variable (if any) takes effect again.
690690+691691+### Upload / delete logo
692692+693693+```
694694+PUT /admin/settings/logo
695695+DELETE /admin/settings/logo
696696+```
697697+698698+`PUT` accepts a binary image body and stores it as the instance logo (served via the public dashboard). `DELETE` removes the stored logo.
699699+700700+## Domain Management
701701+702702+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.
703703+704704+### List domains
705705+706706+```
707707+GET /admin/domains
708708+```
709709+710710+```sh
711711+curl http://localhost:3000/admin/domains -H "$AUTH"
712712+```
713713+714714+**Response**: `200 OK`
715715+716716+```json
717717+[
718718+ {
719719+ "id": "550e8400-e29b-41d4-a716-446655440000",
720720+ "url": "https://gamesgamesgamesgames.games",
721721+ "is_primary": true,
722722+ "created_at": "2026-04-16T00:00:00Z",
723723+ "updated_at": "2026-04-16T00:00:00Z"
724724+ }
725725+]
726726+```
727727+728728+### Add a domain
729729+730730+```
731731+POST /admin/domains
732732+```
733733+734734+```sh
735735+curl -X POST http://localhost:3000/admin/domains \
736736+ -H "$AUTH" \
737737+ -H "Content-Type: application/json" \
738738+ -d '{ "url": "https://api.cartridge.dev" }'
739739+```
740740+741741+| Field | Type | Required | Description |
742742+| ----- | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------ |
743743+| `url` | string | yes | Valid origin (scheme + host, no path or trailing slash). Must be `https` unless `PUBLIC_URL` is a loopback address. |
744744+745745+Returns `400 Bad Request` if the URL is invalid or already registered.
746746+747747+**Response**: `201 Created`
748748+749749+```json
750750+{
751751+ "id": "550e8400-e29b-41d4-a716-446655440001",
752752+ "url": "https://api.cartridge.dev",
753753+ "is_primary": false,
754754+ "created_at": "2026-04-16T00:00:00Z",
755755+ "updated_at": "2026-04-16T00:00:00Z"
756756+}
757757+```
758758+759759+Side effects: builds an OAuth client for the domain, updates the in-memory domain cache.
760760+761761+### Remove a domain
762762+763763+```
764764+DELETE /admin/domains/{id}
765765+```
766766+767767+```sh
768768+curl -X DELETE http://localhost:3000/admin/domains/550e8400-e29b-41d4-a716-446655440001 \
769769+ -H "$AUTH"
770770+```
771771+772772+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.
773773+774774+**Response**: `204 No Content`
775775+776776+Side effects: removes the domain's OAuth client and cache entry.
777777+778778+### Set primary domain
779779+780780+```
781781+POST /admin/domains/{id}/primary
782782+```
783783+784784+```sh
785785+curl -X POST http://localhost:3000/admin/domains/550e8400-e29b-41d4-a716-446655440001/primary \
786786+ -H "$AUTH"
787787+```
788788+789789+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.
790790+791791+**Response**: `204 No Content`
792792+793793+Side effects: updates the in-memory cache and the OAuth client registry's primary client reference.
794794+795795+## Script Variables
796796+797797+Script variables are encrypted key/value pairs available to Lua scripts via the `vars` global. Use them for secrets like API tokens.
798798+799799+### List script variables
800800+801801+```
802802+GET /admin/script-variables
803803+```
804804+805805+Requires `script-variables:read`. Returns a list of variable keys (values are not returned).
806806+807807+### Upsert a script variable
808808+809809+```
810810+POST /admin/script-variables
811811+```
812812+813813+Requires `script-variables:create`.
814814+815815+```sh
816816+curl -X POST http://localhost:3000/admin/script-variables \
817817+ -H "$AUTH" \
818818+ -H "Content-Type: application/json" \
819819+ -d '{ "key": "ALGOLIA_API_KEY", "value": "..." }'
820820+```
821821+822822+The value is encrypted at rest using `TOKEN_ENCRYPTION_KEY`.
823823+824824+### Delete a script variable
825825+826826+```
827827+DELETE /admin/script-variables/{key}
828828+```
829829+830830+Requires `script-variables:delete`.
831831+832832+## API Clients
833833+834834+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`.
835835+836836+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.
837837+838838+### List API clients
839839+840840+```
841841+GET /admin/api-clients
842842+```
843843+844844+Requires `api-clients:view`. Returns clients ordered by `created_at` descending. Secrets are never returned.
845845+846846+```sh
847847+curl http://localhost:3000/admin/api-clients -H "$AUTH"
848848+```
849849+850850+**Response**: `200 OK`
851851+852852+```json
853853+[
854854+ {
855855+ "id": "01J9...",
856856+ "client_key": "hvc_a1b2c3...",
857857+ "name": "My Game Client",
858858+ "client_id_url": "https://example.com/client-metadata.json",
859859+ "client_uri": "https://example.com",
860860+ "redirect_uris": ["https://example.com/callback"],
861861+ "scopes": "atproto",
862862+ "rate_limit_capacity": 200,
863863+ "rate_limit_refill_rate": 5.0,
864864+ "is_active": true,
865865+ "created_by": "did:plc:...",
866866+ "created_at": "2026-04-13T12:00:00Z",
867867+ "updated_at": "2026-04-13T12:00:00Z"
868868+ }
869869+]
870870+```
871871+872872+### Create an API client
873873+874874+```
875875+POST /admin/api-clients
876876+```
877877+878878+Requires `api-clients:create`. Generates a fresh `client_key` and `client_secret`. **The secret is only returned in this response** — store it immediately.
879879+880880+```sh
881881+curl -X POST http://localhost:3000/admin/api-clients \
882882+ -H "$AUTH" \
883883+ -H "Content-Type: application/json" \
884884+ -d '{
885885+ "name": "My Game Client",
886886+ "client_id_url": "https://example.com/client-metadata.json",
887887+ "client_uri": "https://example.com",
888888+ "redirect_uris": ["https://example.com/callback"],
889889+ "scopes": "atproto",
890890+ "rate_limit_capacity": 200,
891891+ "rate_limit_refill_rate": 5.0
892892+ }'
893893+```
894894+895895+| Field | Type | Required | Description |
896896+| ------------------------ | -------- | -------- | -------------------------------------------------------------------------------------- |
897897+| `name` | string | yes | Human-readable display name |
898898+| `client_id_url` | string | yes | URL to the client's published OAuth client metadata document |
899899+| `client_uri` | string | yes | The client's home/landing URL |
900900+| `redirect_uris` | string[] | yes | Allowed OAuth redirect URIs |
901901+| `scopes` | string | no | Space-separated OAuth scopes (default `"atproto"`) |
902902+| `rate_limit_capacity` | integer | no | Per-client token bucket capacity. Falls back to `DEFAULT_RATE_LIMIT_CAPACITY` if unset |
903903+| `rate_limit_refill_rate` | number | no | Tokens added per second. Falls back to `DEFAULT_RATE_LIMIT_REFILL_RATE` if unset |
904904+905905+**Response**: `201 Created`
906906+907907+```json
908908+{
909909+ "id": "01J9...",
910910+ "client_key": "hvc_a1b2c3...",
911911+ "client_secret": "hvs_d4e5f6...",
912912+ "name": "My Game Client",
913913+ "client_id_url": "https://example.com/client-metadata.json"
914914+}
915915+```
916916+917917+The new client is immediately registered with the OAuth registry and rate limiter, so it can authenticate without restarting HappyView.
918918+919919+### Get an API client
920920+921921+```
922922+GET /admin/api-clients/{id}
923923+```
924924+925925+Requires `api-clients:view`. Returns the same `ApiClientSummary` shape as the list endpoint, or `404 Not Found`.
926926+927927+### Update an API client
928928+929929+```
930930+PUT /admin/api-clients/{id}
931931+```
932932+933933+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.
934934+935935+| Field | Type | Description |
936936+| ------------------------ | -------- | ------------------------------------------------------------------------ |
937937+| `name` | string | New display name |
938938+| `client_uri` | string | New home URL |
939939+| `redirect_uris` | string[] | Replace the allowed redirect URIs |
940940+| `scopes` | string | Replace the OAuth scopes |
941941+| `rate_limit_capacity` | integer | New bucket capacity. Pass `null` to clear the override |
942942+| `rate_limit_refill_rate` | number | New refill rate. Pass `null` to clear the override |
943943+| `is_active` | boolean | Disable (`false`) or re-enable (`true`) the client without deleting it |
944944+945945+**Response**: `204 No Content`
946946+947947+The OAuth registry is updated in place. The `client_id_url` is immutable — to change it, delete and recreate the client.
948948+949949+### Delete an API client
950950+951951+```
952952+DELETE /admin/api-clients/{id}
953953+```
954954+955955+Requires `api-clients:delete`. Removes the client from the OAuth registry, the rate limiter, and the client identity store.
956956+957957+**Response**: `204 No Content`
958958+959959+## Plugins
960960+961961+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.
962962+963963+### List installed plugins
964964+965965+```
966966+GET /admin/plugins
967967+```
968968+969969+Requires `plugins:read`. Returns every loaded plugin with its source, required secrets, configuration status, and any pending updates from the official registry cache.
970970+971971+```sh
972972+curl http://localhost:3000/admin/plugins -H "$AUTH"
973973+```
974974+975975+**Response**: `200 OK`
976976+977977+```json
978978+{
979979+ "encryption_configured": true,
980980+ "plugins": [
981981+ {
982982+ "id": "steam",
983983+ "name": "Steam",
984984+ "version": "1.2.0",
985985+ "source": "url",
986986+ "url": "https://example.com/plugins/steam/manifest.json",
987987+ "sha256": null,
988988+ "enabled": true,
989989+ "auth_type": "openid",
990990+ "required_secrets": [
991991+ {
992992+ "key": "PLUGIN_STEAM_API_KEY",
993993+ "name": "Steam Web API Key",
994994+ "description": "Get your API key at steamcommunity.com/dev/apikey"
995995+ }
996996+ ],
997997+ "secrets_configured": true,
998998+ "loaded_at": null,
999999+ "update_available": false,
10001000+ "latest_version": "1.2.0",
10011001+ "pending_releases": []
10021002+ }
10031003+ ]
10041004+}
10051005+```
10061006+10071007+`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.
10081008+10091009+### Preview a plugin before installing
10101010+10111011+```
10121012+POST /admin/plugins/preview
10131013+```
10141014+10151015+Requires `plugins:create`. Fetches and parses a manifest without installing the plugin, so the dashboard can show what it would register.
10161016+10171017+```sh
10181018+curl -X POST http://localhost:3000/admin/plugins/preview \
10191019+ -H "$AUTH" \
10201020+ -H "Content-Type: application/json" \
10211021+ -d '{ "url": "https://example.com/plugins/steam/manifest.json" }'
10221022+```
10231023+10241024+**Response**: `200 OK`
10251025+10261026+```json
10271027+{
10281028+ "id": "steam",
10291029+ "name": "Steam",
10301030+ "version": "1.2.0",
10311031+ "description": "Import your Steam game library and playtime data.",
10321032+ "icon_url": "https://example.com/steam-icon.png",
10331033+ "auth_type": "openid",
10341034+ "required_secrets": [
10351035+ { "key": "PLUGIN_STEAM_API_KEY", "name": "Steam Web API Key", "description": "..." }
10361036+ ],
10371037+ "manifest_url": "https://example.com/plugins/steam/manifest.json",
10381038+ "wasm_url": "https://example.com/plugins/steam/steam.wasm"
10391039+}
10401040+```
10411041+10421042+Returns `400 Bad Request` if the manifest can't be fetched or parsed.
10431043+10441044+### Install a plugin
10451045+10461046+```
10471047+POST /admin/plugins
10481048+```
10491049+10501050+Requires `plugins:create`. Fetches the manifest, downloads the WASM, registers the plugin, and persists it.
10511051+10521052+```sh
10531053+curl -X POST http://localhost:3000/admin/plugins \
10541054+ -H "$AUTH" \
10551055+ -H "Content-Type: application/json" \
10561056+ -d '{
10571057+ "url": "https://example.com/plugins/steam/manifest.json",
10581058+ "sha256": "abc123..."
10591059+ }'
10601060+```
10611061+10621062+| Field | Type | Required | Description |
10631063+| -------- | ------ | -------- | -------------------------------------------------------------------------------------------- |
10641064+| `url` | string | yes | URL to the plugin's `manifest.json` |
10651065+| `sha256` | string | no | Optional sha256 of the WASM binary. If provided, install fails when the downloaded hash mismatches |
10661066+10671067+**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.
10681068+10691069+### List official plugins
10701070+10711071+```
10721072+GET /admin/plugins/official
10731073+```
10741074+10751075+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.
10761076+10771077+**Response**: `200 OK`
10781078+10791079+```json
10801080+{
10811081+ "last_refreshed_at": "2026-04-13T11:00:00Z",
10821082+ "plugins": [
10831083+ {
10841084+ "id": "steam",
10851085+ "name": "Steam",
10861086+ "description": "Import your Steam game library and playtime data.",
10871087+ "icon_url": "https://example.com/steam-icon.png",
10881088+ "latest_version": "1.2.0",
10891089+ "manifest_url": "https://example.com/plugins/steam/manifest.json"
10901090+ }
10911091+ ]
10921092+}
10931093+```
10941094+10951095+### Remove a plugin
10961096+10971097+```
10981098+DELETE /admin/plugins/{id}
10991099+```
11001100+11011101+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.
11021102+11031103+**Response**: `204 No Content`. Returns `404 Not Found` if no plugin with that id is loaded.
11041104+11051105+### Reload a plugin
11061106+11071107+```
11081108+POST /admin/plugins/{id}/reload
11091109+```
11101110+11111111+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.
11121112+11131113+The body is optional. To point the plugin at a new URL, pass:
11141114+11151115+```json
11161116+{ "url": "https://example.com/plugins/steam/manifest.json" }
11171117+```
11181118+11191119+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`.
11201120+11211121+**Response**: `200 OK` with the refreshed `PluginSummary`.
11221122+11231123+### Check for plugin updates
11241124+11251125+```
11261126+POST /admin/plugins/{id}/check-update
11271127+```
11281128+11291129+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.
11301130+11311131+**Response**: `200 OK` with a `PluginSummary`.
11321132+11331133+### Get plugin secrets
11341134+11351135+```
11361136+GET /admin/plugins/{id}/secrets
11371137+```
11381138+11391139+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.
11401140+11411141+**Response**: `200 OK`
11421142+11431143+```json
11441144+{
11451145+ "plugin_id": "steam",
11461146+ "secrets": {
11471147+ "PLUGIN_STEAM_API_KEY": "********ABCD"
11481148+ }
11491149+}
11501150+```
11511151+11521152+### Update plugin secrets
11531153+11541154+```
11551155+PUT /admin/plugins/{id}/secrets
11561156+```
11571157+11581158+Requires `plugins:create`. Encrypts the provided secret values with `TOKEN_ENCRYPTION_KEY` (AES-256-GCM) and upserts them into `plugin_configs`.
11591159+11601160+```sh
11611161+curl -X PUT http://localhost:3000/admin/plugins/steam/secrets \
11621162+ -H "$AUTH" \
11631163+ -H "Content-Type: application/json" \
11641164+ -d '{
11651165+ "secrets": {
11661166+ "PLUGIN_STEAM_API_KEY": "your-new-api-key"
11671167+ }
11681168+ }'
11691169+```
11701170+11711171+Special handling:
11721172+11731173+- 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).
11741174+- Empty string values are not stored — use them to clear a secret.
11751175+11761176+**Response**: `204 No Content`
11771177+6861178## Permissions
68711796881180Each admin API endpoint requires a specific permission. See the [Permissions guide](../guides/permissions.md) for the full list of permissions and templates.
···6971189| `GET /admin/network-lexicons` | `lexicons:read` |
6981190| `DELETE /admin/network-lexicons/{id}` | `lexicons:delete` |
6991191| `GET /admin/stats` | `stats:read` |
700700-| `GET /admin/tap/stats` | `stats:read` |
7011192| `POST /admin/backfill` | `backfill:create` |
7021193| `GET /admin/backfill/status` | `backfill:read` |
7031194| `GET /admin/events` | `events:read` |
···7171208| `GET /admin/labelers` | `labelers:read` |
7181209| `PATCH /admin/labelers/{did}` | `labelers:create` |
7191210| `DELETE /admin/labelers/{did}` | `labelers:delete` |
12111211+| `GET /admin/settings` | `settings:manage` |
12121212+| `PUT /admin/settings/{key}` | `settings:manage` |
12131213+| `DELETE /admin/settings/{key}` | `settings:manage` |
12141214+| `PUT /admin/settings/logo` | `settings:manage` |
12151215+| `DELETE /admin/settings/logo` | `settings:manage` |
12161216+| `GET /admin/plugins` | `plugins:read` |
12171217+| `POST /admin/plugins` | `plugins:create` |
12181218+| `POST /admin/plugins/preview` | `plugins:read` |
12191219+| `GET /admin/plugins/official` | `plugins:read` |
12201220+| `DELETE /admin/plugins/{id}` | `plugins:delete` |
12211221+| `POST /admin/plugins/{id}/reload` | `plugins:create` |
12221222+| `POST /admin/plugins/{id}/check-update` | `plugins:read` |
12231223+| `GET /admin/plugins/{id}/secrets` | `plugins:read` |
12241224+| `PUT /admin/plugins/{id}/secrets` | `plugins:create` |
12251225+| `GET /admin/domains` | `settings:manage` |
12261226+| `POST /admin/domains` | `settings:manage` |
12271227+| `DELETE /admin/domains/{id}` | `settings:manage` |
12281228+| `POST /admin/domains/{id}/primary` | `settings:manage` |
12291229+| `GET /admin/api-clients` | `api-clients:view` |
12301230+| `POST /admin/api-clients` | `api-clients:create` |
12311231+| `GET /admin/api-clients/{id}` | `api-clients:view` |
12321232+| `PUT /admin/api-clients/{id}` | `api-clients:edit` |
12331233+| `DELETE /admin/api-clients/{id}` | `api-clients:delete` |
+19-15
docs/reference/architecture.md
···11# Architecture
2233-Guide for contributors working on HappyView itself. For a user-facing overview, see the [Introduction](/README.md).
33+Guide for contributors working on HappyView itself. For a user-facing overview, see the [Introduction](../README.md).
4455## System overview
66···23232424 DB[("SQLite / PostgreSQL<br/><small>records · lexicons</small>")]
25252626- Tap["Tap<br/><small>WebSocket</small>"] -->|record events| DB
2727- Relay["Relay<br/><small>Firehose</small>"] --> Tap
2626+ Jetstream["Jetstream<br/><small>WebSocket</small>"] -->|record events| DB
2727+ Relay["Relay<br/><small>listReposByCollection</small>"] -->|repo discovery| Backfill
2828+ Backfill["Backfill Worker"] -->|listRecords| PDS
2929+ Backfill --> DB
2830```
29313030-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. All record data enters the system through Tap, which handles both real-time firehose events and historical backfill. HappyView syncs collection filters to Tap and discovers repos via the relay for backfill, but Tap performs all record fetching.
3232+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.
31333234## Module overview
33353436```
3537src/
3636- main.rs Startup: config, DB, migrations, build OAuth client, spawn Tap worker, start server
3838+ main.rs Startup: config, DB, migrations, build OAuth client, spawn Jetstream worker, start server
3739 lib.rs AppState struct (incl. OAuth client + cookie key), module declarations
3840 config.rs Environment variable loading
3941 dns.rs DNS TXT resolver for atrium handle resolution
···4143 server.rs Axum router: fixed routes + admin nest + auth routes + XRPC catch-all + static files
4244 lexicon.rs ParsedLexicon, LexiconRegistry (Arc<RwLock<HashMap>>)
4345 profile.rs DID document resolution, PDS discovery, profile fetching
4444- tap.rs Tap WebSocket listener, collection filter sync, backfill delegation
4646+ jetstream.rs Jetstream WebSocket listener, collection filter sync, cursor persistence
4547 resolve.rs NSID authority resolution (DNS TXT → DID → PDS)
4648 auth/
4749 mod.rs Re-exports, COOKIE_NAME constant
···6264 network_lexicons.rs Network lexicon tracking (add, list, remove)
6365 records.rs Record listing handler
6466 stats.rs Record count stats
6565- backfill.rs Backfill job creation (relay discovery + Tap delegation)
6767+ backfill.rs Backfill job runner (relay discovery + per-PDS listRecords)
6668 types.rs Request/response structs for admin endpoints
6769 lua/
6870 mod.rs Re-exports
···129131### Real-time indexing
130132131133```
132132-Tap WebSocket connection (tap::spawn)
133133- -> Collection filters synced to Tap on startup and lexicon changes
134134- -> Record events:
134134+Jetstream WebSocket connection (jetstream::spawn)
135135+ -> Collection filters built from indexed lexicons and applied to subscription URL
136136+ -> Reconnects on collection filter changes (lexicon add/remove)
137137+ -> Record commit events:
135138 create/update -> UPSERT into records table
136139 delete -> DELETE from records table
137140 -> Lexicon schema events (com.atproto.lexicon.schema):
138141 -> Update tracked network lexicons in DB and registry
139139- -> Reconnects automatically on errors or collection filter changes
142142+ -> Cursor persisted to instance_settings for resume on reconnect
140143```
141144142145### Backfill
···144147```
145148POST /admin/backfill
146149 -> Create backfill_jobs record (status = running)
147147- -> Relay listReposByCollection -> list of DIDs
148148- -> Send DIDs to Tap in batches of 1000 (POST /repos/add)
149149- -> Mark job as completed
150150- -> Tap fetches records asynchronously and delivers via WebSocket
150150+ -> Relay listReposByCollection -> list of DIDs (paginated)
151151+ -> For each DID: resolve PDS via PLC, listRecords from that PDS (paginated)
152152+ -> UPSERT each record into records table
153153+ -> Update processed_repos / total_records counters
154154+ -> Mark job as completed (or failed with error message)
151155```
152156153157## Database schema
+1-1
docs/reference/changelog.md
···22222323## v1.9.0 — Event Logs
24242525-- **Event logging** — system-wide audit trail for lexicon changes, record operations, Lua script executions/errors, admin actions, backfill jobs, and Tap connectivity
2525+- **Event logging** — system-wide audit trail for lexicon changes, record operations, Lua script executions/errors, admin actions, backfill jobs, and firehose connectivity
2626- **`GET /admin/events`** — query event logs with filtering by event type, category, severity, and subject, with cursor pagination
2727- **Lua error context** — script errors capture full debugging context: error message, script source, input payload, and caller DID
2828- **Automatic retention cleanup** — configurable via `EVENT_LOG_RETENTION_DAYS` (default 30 days)
+5-5
docs/reference/glossary.md
···8899**DID** (Decentralized Identifier) — A persistent, globally unique identifier for an account (e.g. `did:plc:abc123`).
10101111-**Firehose** — A real-time stream of all record events (creates, updates, deletes) across the AT Protocol network. HappyView consumes this via [Tap](https://github.com/bluesky-social/indigo/tree/main/cmd/tap).
1111+**Firehose** — A real-time stream of all record events (creates, updates, deletes) across the AT Protocol network. HappyView consumes a filtered slice of this via [Jetstream](https://github.com/bluesky-social/jetstream).
12121313**Handle** — A human-readable name for an account (e.g. `user.bsky.social`). Handles resolve to a DID via DNS or the PLC directory.
1414···22222323**Record** — A single piece of data in an AT Protocol repository, identified by an AT URI (e.g. `at://did:plc:abc/xyz.statusphere.status/abc123`).
24242525-**Relay** — A network service that aggregates repository data from many PDSes. HappyView queries the relay during [backfill](../guides/backfill.md) to discover which repos contain records for a given collection, then delegates the actual record fetching to Tap.
2525+**Relay** — A network service that aggregates repository data from many PDSes. HappyView queries the relay during [backfill](../guides/backfill.md) to discover which repos contain records for a given collection, then fetches each repo's records directly from its PDS.
26262727**rkey** (Record Key) — The unique key for a record within a collection and repo. These are most commonly TIDs (timestamp-based) or NSIDs.
2828···32323333## HappyView-specific terms
34343535-**Backfill** — The process of bulk-indexing existing records from the network. HappyView discovers repos via the relay and delegates record fetching to Tap. Runs when a new record-type lexicon is uploaded or triggered manually. See [Backfill](../guides/backfill.md).
3535+**Backfill** — The process of bulk-indexing existing records from the network. HappyView discovers repos via the relay and fetches each repo's records directly from its PDS. Runs when a new record-type lexicon is uploaded or triggered manually. See [Backfill](../guides/backfill.md).
3636+3737+**Jetstream** — A [filtered firehose](https://github.com/bluesky-social/jetstream) maintained by Bluesky that delivers AT Protocol record commit events as JSON over WebSocket. HappyView subscribes to Jetstream with a collection filter built from its indexed record lexicons, and persists a cursor for resume on reconnect.
36383739**Network lexicon** — A lexicon fetched directly from the AT Protocol network via DNS authority resolution, rather than uploaded manually. See [Lexicons - Network lexicons](../guides/lexicons.md#network-lexicons).
3838-3939-**Tap** — A [firehose consumer and backfill worker](https://github.com/bluesky-social/indigo/tree/main/cmd/tap) that handles real-time record streaming, cryptographic verification, and historical record fetching. HappyView connects to Tap via WebSocket to receive record events, and delegates backfill work to Tap via its HTTP API.
40404141**Permission** — A granular access control right that authorizes a specific action in the admin API. HappyView defines 20 permissions organized by category (e.g. `lexicons:create`, `users:read`). See [Permissions](../guides/permissions.md).
4242
+53-64
docs/reference/production-deployment.md
···11-# Deployment
11+# Production
2233-HappyView requires a database. SQLite is the default; Postgres is also supported, but requires additional setup. The [Quickstart](../getting-started/deployment/railway.md) covers the fastest path with Railway. This page covers other deployment options.
33+This page covers what to change when taking a HappyView instance from local development to production. For setup instructions, see [Deployment](../getting-started/deployment/railway.md). This page assumes you already have a working deployment and focuses on hardening and operational concerns.
4455-## Docker
55+## Session secret
6677-Build the image:
77+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.
8899```sh
1010-docker build -t happyview .
1010+openssl rand -base64 48
1111```
12121313-For local development, see [Docker deployment](../getting-started/deployment/docker.md).
1313+Never commit the secret to source control. Store it in your platform's secret manager (Railway variables, Docker secrets, Kubernetes secrets, etc.).
14141515-### Production Compose example
1515+## Token encryption key
16161717-:::note
1818-This example omits [Tap](https://github.com/bluesky-social/indigo/tree/main/cmd/tap), which is required for real-time record streaming and backfill. See the full `docker-compose.yml` in the repository for a complete configuration including Tap.
1919-:::
1717+If you use [plugins](../guides/plugins.md) that require secrets (API keys, OAuth credentials), set `TOKEN_ENCRYPTION_KEY` to a base64-encoded 32-byte key. This encrypts plugin secrets at rest using AES-256-GCM:
1818+1919+```sh
2020+openssl rand -base64 32
2121+```
2222+2323+Without this variable, the dashboard's plugin secret fields are disabled and plugins can only read secrets from environment variables.
20242121-Using SQLite (default):
2525+## TLS and `PUBLIC_URL`
22262323-```yaml
2424-services:
2525- happyview:
2626- image: happyview:latest
2727- ports:
2828- - "3000:3000"
2929- environment:
3030- DATABASE_URL: "sqlite://data/happyview.db?mode=rwc"
3131- PUBLIC_URL: "https://happyview.example.com"
3232- SESSION_SECRET: "${SESSION_SECRET}"
3333- volumes:
3434- - happyview-data:/app/data
2727+HappyView does not terminate TLS. Put it behind a reverse proxy (nginx, Caddy, Cloudflare Tunnel, a platform-managed load balancer) and set `PUBLIC_URL` to the public HTTPS URL:
35283636-volumes:
3737- happyview-data:
2929+```sh
3030+PUBLIC_URL=https://happyview.example.com
3831```
39324040-Using Postgres:
3333+`PUBLIC_URL` is used to construct OAuth redirect URIs, so it must exactly match the URL users hit — including scheme. A mismatch breaks OAuth login.
3434+3535+## Database
3636+3737+SQLite is fine for small to medium instances and is the default. Switch to Postgres if you need:
3838+3939+- Multiple HappyView replicas sharing one database
4040+- Larger-than-memory working sets
4141+- External tools that need direct read access to the records table
4242+4343+See the [database setup guide](../guides/database-setup.md) for configuration details and [Postgres → SQLite migration](../guides/postgres-to-sqlite-migration.md) if you're moving the other direction. Migrations run automatically on startup regardless of backend.
4444+4545+## Rate limits
4646+4747+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.
4848+4949+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)).
41504242-```yaml
4343-services:
4444- postgres:
4545- image: postgres:17
4646- environment:
4747- POSTGRES_USER: happyview
4848- POSTGRES_PASSWORD: "${POSTGRES_PASSWORD}"
4949- POSTGRES_DB: happyview
5050- volumes:
5151- - pgdata:/var/lib/postgresql/data
5151+## Logging
52525353- happyview:
5454- image: happyview:latest
5555- ports:
5656- - "3000:3000"
5757- environment:
5858- DATABASE_URL: "postgres://happyview:${POSTGRES_PASSWORD}@postgres/happyview"
5959- PUBLIC_URL: "https://happyview.example.com"
6060- SESSION_SECRET: "${SESSION_SECRET}"
6161- depends_on:
6262- postgres:
6363- condition: service_healthy
5353+The default `RUST_LOG` setting (`happyview=debug,tower_http=debug`) is noisy. For production, drop the verbosity:
64546565-volumes:
6666- pgdata:
5555+```sh
5656+RUST_LOG=happyview=info,tower_http=info
6757```
68586969-## Railway / Fly.io / other platforms
5959+Structured logs go to stdout, so any platform that captures container stdout (Railway, Fly, ECS, Kubernetes) will ingest them without further configuration. For retention and querying, ship stdout to your usual log aggregator.
70607171-The general process for any hosting platform:
6161+## Event log retention
72627373-1. Choose a database: SQLite (default, zero setup) or Postgres 17+ (provision separately)
7474-2. Set `DATABASE_URL`, `PUBLIC_URL`, and `SESSION_SECRET` environment variables (see [Configuration](../getting-started/configuration.md) for all options)
7575-3. Deploy the Docker image or build from source
7676-4. HappyView listens on `PORT` (default `3000`)
7777-5. Health check: `GET /health` returns `ok`
6363+The admin [event log](../guides/event-logs.md) is stored in the same database as records. `EVENT_LOG_RETENTION_DAYS` (default `30`) controls automatic cleanup. Set to `0` to keep events indefinitely — useful for compliance-sensitive deployments, but plan for database growth.
78647979-See the [database setup guide](../guides/database-setup.md) for details on both backends.
6565+## Health checks
80668181-For Railway specifically, the [Quickstart](../getting-started/deployment/railway.md) template handles all of this with a single click.
6767+`GET /health` returns `200 ok` when HappyView can bind its HTTP listener. Use it as the readiness/liveness probe for your platform.
82688383-## Database
6969+For a deeper check, hit `GET /xrpc/com.atproto.server.describeServer` — this exercises the database and lexicon registry, and only returns `200` if HappyView can actually serve requests.
84708585-HappyView supports SQLite (default) and Postgres. The backend is auto-detected from the `DATABASE_URL` scheme (`sqlite://` or `postgres://`). Migrations run automatically on startup. No manual migration step is needed. See the [database setup guide](../guides/database-setup.md) for details.
7171+## Backups
86728787-## TLS
7373+- **SQLite**: back up the database file (e.g. `data/happyview.db`) plus its `-wal` and `-shm` sidecar files. Use `sqlite3 happyview.db ".backup '/path/backup.db'"` for a consistent snapshot while HappyView is running.
7474+- **Postgres**: standard `pg_dump` / managed-Postgres snapshots.
88758989-HappyView does not terminate TLS. Put it behind a reverse proxy (nginx, Caddy, Cloudflare Tunnel, etc.) for HTTPS. Make sure `PUBLIC_URL` matches the public-facing URL (including `https://`).
7676+Most of what HappyView stores is derivable from the network — lost records can be re-indexed via [backfill](../guides/backfill.md). What you cannot recover from the network: user accounts and permissions, API keys, API clients, plugin secrets, and the Jetstream cursor. Prioritize those in your backup plan.
90779191-## Logging
7878+## Next steps
92799393-HappyView uses the `RUST_LOG` environment variable to control log output. The default (`happyview=debug,tower_http=debug`) logs all HappyView activity and HTTP requests. For production, consider `happyview=info,tower_http=info` to reduce noise. See [Configuration](../getting-started/configuration.md) for details.
8080+- [Configuration](../getting-started/configuration.md) — full environment variable reference
8181+- [Permissions](../guides/permissions.md) — lock down admin access before exposing the dashboard publicly
8282+- [Troubleshooting](troubleshooting.md) — diagnose issues with a running instance
+3-3
docs/reference/troubleshooting.md
···20202121- The query lexicon is missing a `target_collection`. Without it, the query doesn't know which records to read. See [Lexicons - target_collection](../guides/lexicons.md#target-collection).
2222- The record-type lexicon hasn't finished backfilling. Check backfill status with `GET /admin/backfill/status` or the dashboard.
2323-- Records exist on the network but HappyView hasn't indexed them yet. Tap only picks up new events from when the collection filter was added. Use [backfill](../guides/backfill.md) for historical records.
2323+- Records exist on the network but HappyView hasn't indexed them yet. Jetstream only delivers events from after the collection was added to the filter. Use [backfill](../guides/backfill.md) to import historical records.
24242525## Procedure returns 401 Unauthorized
2626···82828383**Causes**:
84848585-- HappyView receives real-time events via [Tap](https://github.com/bluesky-social/indigo/tree/main/cmd/tap). Make sure Tap is running and connected to HappyView. See the [Tap documentation](https://github.com/bluesky-social/indigo/tree/main/cmd/tap) for configuration.
8585+- HappyView receives real-time events via [Jetstream](https://github.com/bluesky-social/jetstream). Verify the `JETSTREAM_URL` is reachable and check server logs for `jetstream.disconnected` events.
8686- No record-type lexicon exists for the collection. HappyView only indexes collections that have a corresponding record-type lexicon.
8787-- The Tap connection hasn't synced the new collection filter after a lexicon change. This should happen automatically. Check server logs for connection errors.
8787+- 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.
88888989## OAuth or login issues
9090
+1-1
docs/tutorials/statusphere.md
···5252 }'
5353```
54545555-HappyView now subscribes to `xyz.statusphere.status` via Tap. The `backfill` flag tells HappyView to also index existing status records from the network. You can monitor progress with `GET /admin/backfill/status` or the [dashboard](../getting-started/dashboard.md).
5555+HappyView now subscribes to `xyz.statusphere.status` via Jetstream. The `backfill` flag tells HappyView to also index existing status records from the network. You can monitor progress with `GET /admin/backfill/status` or the [dashboard](../getting-started/dashboard.md).
56565757:::tip
5858Since the `xyz.statusphere.status` lexicon is [published on the AT Protocol network](../guides/lexicons.md#network-lexicons), you can also add it as a network lexicon instead of uploading the JSON manually: