···991010| 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) |
1212+| Queries (`GET /xrpc/{method}`) | `X-Client-Key` required | Optional — DPoP auth if the query needs to know who the user is |
1313+| Procedures (`POST /xrpc/{method}`) | `X-Client-Key` required | Required — DPoP auth so HappyView can proxy writes to the user's PDS |
1414+| Admin API (`/admin/*`) | — | Required — session cookie, admin API key, or service auth JWT with the right [permissions](../guides/permissions.md) |
1515| Health check (`GET /health`) | — | — |
16161717## XRPC: API client identification
···4848 -H 'X-Client-Secret: hvs_d4e5f6...'
4949```
50505151-### Logging a user in so you can call procedures
5151+### Authenticating users for 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.
5454+5555+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.
5656+5757+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.
52585353-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:
5959+The [JavaScript SDK](../sdk/overview.md) handles this entire flow for you:
54605555-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`.
6161+```typescript
6262+import { Client } from "@atproto/lex";
6363+import { HappyViewBrowserClient } from "@happyview/oauth-client-browser";
6464+import { createAgent } from "@happyview/lex-agent";
6565+6666+const oauthClient = new HappyViewBrowserClient({
6767+ instanceUrl: "https://happyview.example.com",
6868+ clientKey: "hvc_your_client_key",
6969+});
7070+7171+// Login — redirects to the user's PDS for authorization
7272+await oauthClient.login("alice.bsky.social");
7373+7474+// On /oauth/callback — complete the token exchange
7575+const session = await oauthClient.callback();
7676+7777+// Create a type-safe Lex client
7878+const agent = createAgent(session);
7979+const lex = new Client(agent);
8080+8181+// Make authenticated XRPC calls
8282+await lex.xrpc(myLexicons.com.example.createPost, {
8383+ input: { text: "Hello from HappyView!" },
8484+});
8585+```
59866087For 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).
8888+8989+:::note
9090+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.
9191+:::
61926293## Admin API: user authentication
6394···112143Third-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.
113144114145The 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.
146146+147147+:::tip
148148+The [JavaScript SDK](../sdk/overview.md) handles this entire flow for you. The raw HTTP flow below is useful for understanding the protocol or building a non-JavaScript client.
149149+:::
115150116151### API clients: confidential vs public
117152···245280246281## Next steps
247282283283+- [JavaScript SDK](../sdk/overview.md) — authenticate and make XRPC calls from JavaScript
248284- [Permissions](../guides/permissions.md) — full list of permissions and what each one grants
249285- [API Keys](../guides/api-keys.md) — create scoped admin API keys for automation
250286- [Admin API — API Clients](../reference/admin-api.md#api-clients) — register API clients and configure rate limits
+60
packages/docs/docs/sdk/lex-agent.md
···11+# Lex Agent
22+33+The Lex agent adapter is the recommended way to interact with HappyView from JavaScript. It creates an [`@atproto/lex`](https://www.npmjs.com/package/@atproto/lex) `Agent` from a `HappyViewSession`, so you can use `@atproto/lex`'s type-safe `Client` to make XRPC calls with HappyView's DPoP authentication. All requests are routed to your HappyView instance, which handles its own lexicons locally and proxies standard AT Protocol methods (e.g., `com.atproto.repo.createRecord`) to the user's PDS.
44+55+The adapter gives you lexicon-level type checking on parameters, input bodies, and responses, and works with any library or tool that accepts an `@atproto/lex` `Agent`.
66+77+## Installation
88+99+```bash
1010+npm install @happyview/lex-agent @atproto/lex
1111+```
1212+1313+`@atproto/lex` is a peer dependency (`>=0.0.20`).
1414+1515+## Usage
1616+1717+```typescript
1818+import { Client } from "@atproto/lex";
1919+import { HappyViewBrowserClient } from "@happyview/oauth-client-browser";
2020+import { createAgent } from "@happyview/lex-agent";
2121+2222+const client = new HappyViewBrowserClient({
2323+ instanceUrl: "https://happyview.example.com",
2424+ clientKey: "hvc_your_client_key",
2525+});
2626+2727+// Authenticate (or restore a session)
2828+const session = await client.restore();
2929+3030+// Create a Lex agent from the session
3131+const agent = createAgent(session);
3232+const lex = new Client(agent);
3333+```
3434+3535+## Type-safe XRPC calls
3636+3737+With a `Client` instance, you can make type-safe XRPC calls using lexicon definitions:
3838+3939+```typescript
4040+// Query
4141+const result = await lex.xrpc(myLexicons.com.example.getGame, {
4242+ params: { slug: "celeste" },
4343+});
4444+4545+// Procedure
4646+await lex.xrpc(myLexicons.com.example.createPost, {
4747+ input: { text: "Hello from HappyView!" },
4848+});
4949+```
5050+5151+The `Client` validates parameters and return types against the lexicon schema at the type level, so your IDE catches mismatches before runtime.
5252+5353+## API
5454+5555+### `createAgent(session: HappyViewSession): Agent`
5656+5757+Creates an `@atproto/lex` `Agent` from a `HappyViewSession`.
5858+5959+- `agent.did` — the session user's DID
6060+- `agent.fetchHandler(path, init)` — delegates to `session.fetchHandler`, which attaches DPoP authentication headers and prepends the HappyView instance URL to relative paths
+144
packages/docs/docs/sdk/oauth-client-browser.md
···11+# Browser Client
22+33+The browser client handles the full OAuth redirect flow for browser apps authenticating with a HappyView instance. It wraps the [OAuth Client](./oauth-client.md) with Web Crypto, localStorage, and AT Protocol handle/DID resolution.
44+55+If you're starting a new app, consider using [`@happyview/lex-agent`](./lex-agent.md) with `@atproto/lex` instead — it provides type-safe XRPC calls and is the recommended way to interact with HappyView. This package is primarily useful if your app already uses `@atproto/oauth-client-browser` and you want to add HappyView authentication alongside it.
66+77+## Installation
88+99+```bash
1010+npm install @happyview/oauth-client-browser
1111+```
1212+1313+## Setup
1414+1515+```typescript
1616+import { HappyViewBrowserClient } from "@happyview/oauth-client-browser";
1717+1818+const client = new HappyViewBrowserClient({
1919+ instanceUrl: "https://happyview.example.com",
2020+ clientKey: "hvc_your_client_key",
2121+});
2222+```
2323+2424+The client uses Web Crypto and localStorage by default. You can override either:
2525+2626+```typescript
2727+const client = new HappyViewBrowserClient({
2828+ instanceUrl: "https://happyview.example.com",
2929+ clientKey: "hvc_your_client_key",
3030+ crypto: myCustomCryptoAdapter,
3131+ storage: myCustomStorageAdapter,
3232+});
3333+```
3434+3535+:::note
3636+The API client must be registered as a **public** client (no secret) with your app's origin in `allowed_origins`. See [Authentication — API clients](../getting-started/authentication.md#api-clients-confidential-vs-public).
3737+:::
3838+3939+## Login
4040+4141+`login()` resolves the user's handle, discovers their PDS, provisions a DPoP key, and redirects the browser to the PDS authorization server:
4242+4343+```typescript
4444+await client.login("alice.bsky.social");
4545+// Browser redirects — code stops here
4646+```
4747+4848+If you need the authorization URL without redirecting (e.g., for a popup or custom UI), use `prepareLogin()`:
4949+5050+```typescript
5151+const { authorizationUrl, did, state } =
5252+ await client.prepareLogin("alice.bsky.social");
5353+5454+// Open in a popup, new tab, etc.
5555+window.open(authorizationUrl);
5656+```
5757+5858+### What happens during login
5959+6060+1. The handle is resolved to a DID via `resolveHandleToDid`.
6161+2. The DID document is fetched to find the PDS URL.
6262+3. The PDS's OAuth authorization server metadata is fetched.
6363+4. A DPoP key is provisioned from HappyView.
6464+5. PKCE challenge/verifier pairs are generated (one for HappyView's DPoP provisioning, one for the PDS authorization server).
6565+6. The pending auth state is stored in localStorage.
6666+7. The browser is redirected to the PDS authorization endpoint.
6767+6868+## OAuth callback
6969+7070+Your app needs an `/oauth/callback` route. On that page, call `callback()` to complete the token exchange:
7171+7272+```typescript
7373+// On /oauth/callback
7474+const session = await client.callback();
7575+// Session is now stored in localStorage and ready to use
7676+```
7777+7878+`callback()` reads the `code` and `state` from the URL query string, exchanges the code for tokens at the PDS token endpoint, and registers the session with HappyView. The pending auth state is cleaned up automatically.
7979+8080+## Restore session
8181+8282+On subsequent page loads, restore the session from localStorage instead of re-authenticating:
8383+8484+```typescript
8585+const session = await client.restore();
8686+if (session) {
8787+ // User is still logged in
8888+}
8989+```
9090+9191+Returns `null` if no stored session is found.
9292+9393+## Authenticated requests
9494+9595+The session's `fetchHandler` attaches DPoP proof headers automatically:
9696+9797+```typescript
9898+const response = await session.fetchHandler(
9999+ "/xrpc/com.example.getStuff?limit=10",
100100+ { method: "GET" },
101101+);
102102+103103+const data = await response.json();
104104+```
105105+106106+Pass a relative path (prepends the HappyView instance URL) or a full URL (used as-is).
107107+108108+## Logout
109109+110110+```typescript
111111+await client.logout(session.did);
112112+```
113113+114114+## Resolution utilities
115115+116116+The browser client exports the resolution functions it uses internally. These are useful if you need to resolve handles or discover PDS URLs outside of the login flow:
117117+118118+```typescript
119119+import {
120120+ resolveHandleToDid,
121121+ resolveDidDocument,
122122+ resolvePdsUrl,
123123+ resolveAuthServerMetadata,
124124+} from "@happyview/oauth-client-browser";
125125+126126+const did = await resolveHandleToDid("alice.bsky.social");
127127+const doc = await resolveDidDocument(did);
128128+const pdsUrl = resolvePdsUrl(doc);
129129+const authMeta = await resolveAuthServerMetadata(pdsUrl);
130130+```
131131+132132+## Re-exports
133133+134134+This package re-exports everything from `@happyview/oauth-client`, so you don't need to install the core package separately. All types, error classes, and utilities are available:
135135+136136+```typescript
137137+import {
138138+ HappyViewBrowserClient,
139139+ HappyViewSession,
140140+ ApiError,
141141+ type CryptoAdapter,
142142+ type StorageAdapter,
143143+} from "@happyview/oauth-client-browser";
144144+```
+158
packages/docs/docs/sdk/oauth-client.md
···11+# OAuth Client
22+33+The core OAuth client handles DPoP key provisioning, session registration, and session restoration against a HappyView instance. It's platform-agnostic — you provide a `CryptoAdapter` and optional `StorageAdapter` for your environment.
44+55+If you're building a browser app, use the [Browser Client](./oauth-client-browser.md) instead. It wraps this package with Web Crypto, localStorage, and a complete OAuth redirect flow.
66+77+## Installation
88+99+```bash
1010+npm install @happyview/oauth-client
1111+```
1212+1313+## Setup
1414+1515+```typescript
1616+import { HappyViewOAuthClient } from "@happyview/oauth-client";
1717+1818+const client = new HappyViewOAuthClient({
1919+ instanceUrl: "https://happyview.example.com",
2020+ clientKey: "hvc_your_client_key",
2121+ clientSecret: "hvs_your_secret", // optional, for confidential clients
2222+ crypto: myCryptoAdapter,
2323+ storage: myStorageAdapter, // optional, defaults to in-memory
2424+});
2525+```
2626+2727+The `clientSecret` parameter makes this a **confidential client**. Omit it for public clients (browser apps), which use PKCE instead. See [Authentication — API clients](../getting-started/authentication.md#api-clients-confidential-vs-public) for details.
2828+2929+## DPoP key provisioning
3030+3131+Request a DPoP keypair from the HappyView instance. This is the first step of the [DPoP key provisioning flow](../getting-started/authentication.md#dpop-key-provisioning-for-third-party-apps).
3232+3333+```typescript
3434+const { provisionId, dpopKey, pkceVerifier } =
3535+ await client.provisionDpopKey();
3636+```
3737+3838+For public clients, `pkceVerifier` is included and must be passed back when registering the session. For confidential clients it will be `undefined`.
3939+4040+Use the returned `dpopKey` (a private JWK) as your DPoP keypair during your AT Protocol OAuth flow with the user's PDS.
4141+4242+## Session registration
4343+4444+After completing OAuth authorization with the user's PDS, register the session with HappyView:
4545+4646+```typescript
4747+const session = await client.registerSession({
4848+ provisionId,
4949+ pkceVerifier, // required for public clients
5050+ did: "did:plc:abc123",
5151+ accessToken: tokens.access_token,
5252+ refreshToken: tokens.refresh_token,
5353+ scopes: "atproto",
5454+ pdsUrl: "https://bsky.social",
5555+ issuer: tokens.iss,
5656+ dpopKey,
5757+});
5858+```
5959+6060+The returned `HappyViewSession` is ready to make authenticated requests. The session data is also persisted to the `StorageAdapter` for later restoration.
6161+6262+## Making authenticated requests
6363+6464+`HappyViewSession.fetchHandler` works like `fetch` but automatically attaches DPoP proof, authorization, and client key headers:
6565+6666+```typescript
6767+// Relative path — prepends the HappyView instance URL
6868+const response = await session.fetchHandler(
6969+ "/xrpc/com.example.getStuff?limit=10",
7070+ { method: "GET" },
7171+);
7272+7373+// Absolute URL — used as-is
7474+const response = await session.fetchHandler(
7575+ "https://other-service.example.com/xrpc/test.method",
7676+ { method: "GET" },
7777+);
7878+```
7979+8080+## Session restoration
8181+8282+Restore a previously stored session without re-authenticating:
8383+8484+```typescript
8585+// Restore the last active session
8686+const session = await client.restore();
8787+8888+// Restore a specific user's session
8989+const session = await client.restoreSession("did:plc:abc123");
9090+```
9191+9292+Returns `null` if no stored session is found.
9393+9494+## Logout
9595+9696+```typescript
9797+await client.deleteSession("did:plc:abc123");
9898+```
9999+100100+This deletes the session from both HappyView and local storage.
101101+102102+## Adapters
103103+104104+### CryptoAdapter
105105+106106+Implement this interface for your platform's cryptographic primitives:
107107+108108+```typescript
109109+interface CryptoAdapter {
110110+ generatePkceVerifier(): Promise<string>;
111111+ computePkceChallenge(verifier: string): Promise<string>;
112112+ signEs256(privateKey: JsonWebKey, payload: Uint8Array): Promise<Uint8Array>;
113113+ sha256(data: Uint8Array): Promise<Uint8Array>;
114114+ getRandomValues(length: number): Uint8Array;
115115+}
116116+```
117117+118118+### StorageAdapter
119119+120120+Implement this interface to persist sessions:
121121+122122+```typescript
123123+interface StorageAdapter {
124124+ get(key: string): Promise<string | null>;
125125+ set(key: string, value: string): Promise<void>;
126126+ delete(key: string): Promise<void>;
127127+}
128128+```
129129+130130+If no `StorageAdapter` is provided, sessions are stored in memory and won't survive page reloads or process restarts.
131131+132132+:::note
133133+The built-in `MemoryStorage` is exported for testing. In production, always provide a persistent storage adapter.
134134+:::
135135+136136+## Error handling
137137+138138+All errors extend `HappyViewError`:
139139+140140+| Error | When |
141141+| --- | --- |
142142+| `ApiError` | HappyView API returned a non-OK response (has `status` and `body`) |
143143+| `AuthenticationError` | Authentication failed (default status 401) |
144144+| `InvalidStateError` | Missing or invalid OAuth state |
145145+| `TokenExchangeError` | Token exchange with the PDS failed (has `status` and `body`) |
146146+| `ResolutionError` | Handle or DID resolution failed |
147147+148148+```typescript
149149+import { ApiError } from "@happyview/oauth-client";
150150+151151+try {
152152+ await client.registerSession(params);
153153+} catch (err) {
154154+ if (err instanceof ApiError) {
155155+ console.error(`API error ${err.status}:`, err.body);
156156+ }
157157+}
158158+```
+66
packages/docs/docs/sdk/overview.md
···11+# JavaScript SDK
22+33+HappyView provides JavaScript packages for building third-party apps that authenticate with a HappyView instance and make XRPC requests on behalf of users.
44+55+| Package | Purpose |
66+| -------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
77+| [`@happyview/lex-agent`](https://www.npmjs.com/package/@happyview/lex-agent) | Recommended — type-safe XRPC via [`@atproto/lex`](https://www.npmjs.com/package/@atproto/lex) `Client` with HappyView DPoP auth |
88+| [`@happyview/oauth-client`](https://www.npmjs.com/package/@happyview/oauth-client) | Platform-agnostic core — DPoP key provisioning, session management, authenticated fetch |
99+| [`@happyview/oauth-client-browser`](https://www.npmjs.com/package/@happyview/oauth-client-browser) | Browser OAuth wrapper for apps already using `@atproto/oauth-client-browser` |
1010+1111+## Which package do I need?
1212+1313+**Starting a new app?** Use `@happyview/lex-agent` with `@atproto/lex`. It gives you type-safe XRPC calls through a `Client` that routes requests to your HappyView instance with DPoP authentication. This is the recommended way to interact with HappyView from JavaScript.
1414+1515+**Already using `@atproto/oauth-client-browser`?** Add `@happyview/oauth-client-browser` to get a `HappyViewBrowserClient` that handles the HappyView-specific DPoP key provisioning and session registration on top of the standard AT Protocol OAuth flow.
1616+1717+**Building a server-side app or something more custom?** Use `@happyview/oauth-client` directly and provide your own `CryptoAdapter` and `StorageAdapter`.
1818+1919+## How it works
2020+2121+Third-party apps authenticate using HappyView's [DPoP key provisioning](../getting-started/authentication.md#dpop-key-provisioning-for-third-party-apps) flow:
2222+2323+1. The SDK requests a DPoP keypair from the HappyView instance.
2424+2. Your app runs a standard AT Protocol OAuth flow with the user's PDS using that keypair.
2525+3. The SDK registers the resulting tokens with HappyView.
2626+4. All subsequent XRPC requests are authenticated with DPoP proofs — HappyView handles its own lexicons locally and proxies standard AT Protocol writes to the user's PDS.
2727+2828+## Quick start
2929+3030+```bash
3131+npm install @happyview/lex-agent @happyview/oauth-client-browser @atproto/lex
3232+```
3333+3434+```typescript
3535+import { Client } from "@atproto/lex";
3636+import { HappyViewBrowserClient } from "@happyview/oauth-client-browser";
3737+import { createAgent } from "@happyview/lex-agent";
3838+3939+// Set up the OAuth client
4040+const oauthClient = new HappyViewBrowserClient({
4141+ instanceUrl: "https://happyview.example.com",
4242+ clientKey: "hvc_your_client_key",
4343+});
4444+4545+// Login — redirects to the user's PDS
4646+await oauthClient.login("alice.bsky.social");
4747+4848+// On /oauth/callback — complete the flow
4949+const session = await oauthClient.callback();
5050+5151+// Create a type-safe Lex client
5252+const agent = createAgent(session);
5353+const lex = new Client(agent);
5454+5555+// Make type-safe XRPC calls
5656+const result = await lex.xrpc(myLexicons.com.example.getGame, {
5757+ params: { slug: "celeste" },
5858+});
5959+```
6060+6161+## Next steps
6262+6363+- [Lex Agent](./lex-agent.md): type-safe XRPC with `@atproto/lex`
6464+- [OAuth Client](./oauth-client.md): platform-agnostic core client
6565+- [Browser Client](./oauth-client-browser.md): browser OAuth redirect flow
6666+- [Authentication](../getting-started/authentication.md): full details on DPoP key provisioning and API client types