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

Configure Feed

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

docs: add page for API clients

Trezy 26564c65 7aafc692

+370
+365
packages/docs/docs/guides/api-clients.md
··· 1 + # API Clients 2 + 3 + API clients identify your application to a HappyView instance. Every XRPC request — even unauthenticated queries — must include a client key. This guide walks through creating a client, choosing between public and confidential types, and authenticating users. 4 + 5 + For the admin CRUD endpoints, see the [API reference](../reference/admin/api-clients.md). For the JavaScript SDK, see the [SDK docs](../sdk/overview.md). 6 + 7 + ## Concepts 8 + 9 + An API client represents **your application**, not individual users. Create one client for your app and use the same client key everywhere. Users authenticate separately via OAuth — the client key identifies _who built the app_, not _who is using it_. 10 + 11 + Each client has: 12 + 13 + - An `hvc_`-prefixed **client key** — included in every request to identify your app 14 + - An `hvs_`-prefixed **client secret** — used by server-side apps to prove ownership (confidential clients only) 15 + - **Rate limits** — a token bucket that controls how many requests your app can make 16 + - **Scopes** — which lexicons your app is allowed to access 17 + 18 + ## Public vs. confidential clients 19 + 20 + Choose based on where your code runs: 21 + 22 + | | Confidential | Public | 23 + | ---------------------- | ------------------------------------------ | ------------------------------------------- | 24 + | **Use when** | Server-side apps, CLI tools, bots | Browser apps, mobile apps | 25 + | **Authentication** | `X-Client-Key` + `X-Client-Secret` headers | `X-Client-Key` + `Origin` header + PKCE | 26 + | **Can keep a secret?** | Yes | No | 27 + | **Origin validation** | No | Yes — `Origin` must match `allowed_origins` | 28 + | **PKCE required?** | No | Yes (S256) | 29 + 30 + :::tip 31 + If your app has a backend that can securely store the client secret, use a confidential client even if the frontend is a browser app. The backend can proxy OAuth operations. 32 + ::: 33 + 34 + ## Creating a client 35 + 36 + ### From the dashboard 37 + 38 + Go to **Settings > API Clients > New client** and fill in: 39 + 40 + - **Client type** — `confidential` (default) or `public` 41 + - **Name** — a human-readable label (e.g. "My atproto Client") 42 + - **Client ID URL** — URL to your published [OAuth client metadata](https://drafts.aaronpk.com/draft-parecki-oauth-client-id-metadata-document/draft-parecki-oauth-client-id-metadata-document.html) document 43 + - **Client URI** — your app's root domain (e.g. https://example.com) 44 + - **Redirect URIs** — where the PDS should redirect after authorization 45 + - **Allowed origins** — (public clients only) which `Origin` headers to accept 46 + - **Scopes** — `atproto` is always included; add custom scopes if your instance uses them 47 + 48 + **Save the client secret immediately.** It is only shown once and is hashed before storage. 49 + 50 + ### From the API 51 + 52 + ```sh 53 + curl -X POST http://localhost:3000/admin/api-clients \ 54 + -H "Authorization: Bearer $TOKEN" \ 55 + -H "Content-Type: application/json" \ 56 + -d '{ 57 + "name": "My atproto Client", 58 + "client_id_url": "https://example.com/client-metadata.json", 59 + "client_uri": "https://example.com", 60 + "redirect_uris": ["https://example.com/oauth/callback"], 61 + "client_type": "public", 62 + "allowed_origins": ["https://example.com"] 63 + }' 64 + ``` 65 + 66 + See the [API reference](../reference/admin/api-clients.md#create-an-api-client) for all fields. 67 + 68 + ## Using your client key 69 + 70 + Every XRPC request must include the client key. HappyView looks for it in this order: 71 + 72 + 1. `X-Client-Key` request header (preferred) 73 + 2. `client_key` query parameter 74 + 75 + ### Unauthenticated queries 76 + 77 + For public queries that don't need a user identity: 78 + 79 + ```sh 80 + curl 'https://happyview.example.com/xrpc/com.example.feed.getHot' \ 81 + -H 'X-Client-Key: hvc_a1b2c3...' 82 + ``` 83 + 84 + Server-side callers should also include the secret (since there's no origin to authenticate): 85 + 86 + ```sh 87 + curl 'https://happyview.example.com/xrpc/com.example.feed.getHot' \ 88 + -H 'X-Client-Key: hvc_a1b2c3...' \ 89 + -H 'X-Client-Secret: hvs_d4e5f6...' 90 + ``` 91 + 92 + ### Authenticated requests (user identity) 93 + 94 + Procedures — and queries whose scripts need to know who the caller is — require a user's OAuth session. This uses [DPoP authentication](../getting-started/authentication.md#dpop-key-provisioning-for-third-party-apps), where each request includes a cryptographic proof that the caller holds the right key. 95 + 96 + ```sh 97 + curl -X POST 'https://happyview.example.com/xrpc/com.example.createPost' \ 98 + -H 'X-Client-Key: hvc_...' \ 99 + -H 'Authorization: DPoP <access_token>' \ 100 + -H 'DPoP: <proof_jwt>' \ 101 + -H 'Content-Type: application/json' \ 102 + -d '{"text": "Hello world"}' 103 + ``` 104 + 105 + ## Authenticating users 106 + 107 + ### Using the JavaScript SDK 108 + 109 + The SDK handles the entire DPoP flow. A complete browser example: 110 + 111 + ```typescript 112 + import { HappyViewBrowserClient } from "@happyview/oauth-client-browser"; 113 + 114 + const client = new HappyViewBrowserClient({ 115 + instanceUrl: "https://happyview.example.com", 116 + clientKey: "hvc_your_client_key", 117 + }); 118 + 119 + // Login — redirects to the user's PDS 120 + await client.login("alice.bsky.social"); 121 + ``` 122 + 123 + On your callback page: 124 + 125 + ```typescript 126 + const session = await client.callback(); 127 + 128 + // Make authenticated requests 129 + const response = await session.fetchHandler( 130 + "/xrpc/com.example.getStuff?limit=10", 131 + { method: "GET" }, 132 + ); 133 + ``` 134 + 135 + On subsequent page loads, restore the session from localStorage: 136 + 137 + ```typescript 138 + const session = await client.restore(); 139 + if (session) { 140 + // User is still logged in 141 + } 142 + ``` 143 + 144 + For server-side Node.js apps, use the core [`@happyview/oauth-client`](../sdk/oauth-client.md) package with a confidential client. For type-safe XRPC calls, pair either client with [`@happyview/lex-agent`](../sdk/lex-agent.md). 145 + 146 + ### Manual DPoP flow 147 + 148 + If you're not using JavaScript, or want to understand the protocol, the DPoP flow has four phases. 149 + 150 + #### Phase 1: Provision a DPoP key 151 + 152 + Ask HappyView for an ES256 keypair that will be shared between your app and the instance. 153 + 154 + **Confidential client:** 155 + 156 + ```http 157 + POST /oauth/dpop-keys 158 + X-Client-Key: hvc_... 159 + X-Client-Secret: hvs_... 160 + Content-Type: application/json 161 + 162 + {} 163 + ``` 164 + 165 + **Public client:** 166 + 167 + ```http 168 + POST /oauth/dpop-keys 169 + X-Client-Key: hvc_... 170 + Origin: https://example.com 171 + Content-Type: application/json 172 + 173 + {"pkce_challenge": "<base64url-encoded S256 challenge>"} 174 + ``` 175 + 176 + **Response:** 177 + 178 + ```json 179 + { 180 + "provision_id": "hvp_...", 181 + "dpop_key": { 182 + "kty": "EC", 183 + "crv": "P-256", 184 + "x": "...", 185 + "y": "...", 186 + "d": "..." 187 + } 188 + } 189 + ``` 190 + 191 + The `dpop_key` is the full private JWK. Store it securely — you'll use it to sign DPoP proofs. 192 + 193 + #### Phase 2: OAuth with the user's PDS 194 + 195 + Run a standard atproto OAuth flow with the user's PDS authorization server, using the provisioned DPoP key as your keypair. HappyView is not involved in this step. 196 + 197 + 1. Resolve the user's handle to a DID 198 + 2. Resolve the DID document to find the PDS URL 199 + 3. Fetch the PDS's OAuth authorization server metadata 200 + 4. Redirect the user to the PDS authorization endpoint 201 + 5. Exchange the authorization code for tokens (using DPoP proofs signed with the provisioned key) 202 + 203 + #### Phase 3: Register the session 204 + 205 + After the OAuth callback, register the token set with HappyView so it can proxy requests on behalf of the user. 206 + 207 + **Confidential client:** 208 + 209 + ```http 210 + POST /oauth/sessions 211 + X-Client-Key: hvc_... 212 + X-Client-Secret: hvs_... 213 + Content-Type: application/json 214 + 215 + { 216 + "provision_id": "hvp_...", 217 + "did": "did:plc:user123", 218 + "access_token": "...", 219 + "refresh_token": "...", 220 + "expires_at": "2026-04-17T00:00:00Z", 221 + "scopes": "atproto transition:generic", 222 + "pds_url": "https://bsky.social", 223 + "issuer": "https://bsky.social" 224 + } 225 + ``` 226 + 227 + **Public client** — omit the secret, include the PKCE verifier: 228 + 229 + ```http 230 + POST /oauth/sessions 231 + X-Client-Key: hvc_... 232 + Content-Type: application/json 233 + 234 + { 235 + "provision_id": "hvp_...", 236 + "pkce_verifier": "...", 237 + "did": "did:plc:user123", 238 + "access_token": "...", 239 + "refresh_token": "...", 240 + "expires_at": "2026-04-17T00:00:00Z", 241 + "scopes": "atproto transition:generic", 242 + "pds_url": "https://bsky.social", 243 + "issuer": "https://bsky.social" 244 + } 245 + ``` 246 + 247 + #### Phase 4: Make authenticated XRPC requests 248 + 249 + With a registered session, sign each request with a DPoP proof: 250 + 251 + ```sh 252 + curl -X POST 'https://happyview.example.com/xrpc/com.example.createPost' \ 253 + -H 'X-Client-Key: hvc_...' \ 254 + -H 'Authorization: DPoP <access_token>' \ 255 + -H 'DPoP: <proof_jwt>' \ 256 + -H 'Content-Type: application/json' \ 257 + -d '{"text": "Hello world"}' 258 + ``` 259 + 260 + HappyView validates the proof, looks up the stored session, and proxies writes to the user's PDS using the shared DPoP key. 261 + 262 + #### Logout 263 + 264 + **Confidential:** 265 + 266 + ```http 267 + DELETE /oauth/sessions/did:plc:user123 268 + X-Client-Key: hvc_... 269 + X-Client-Secret: hvs_... 270 + ``` 271 + 272 + **Public** (must prove key possession): 273 + 274 + ```http 275 + DELETE /oauth/sessions/did:plc:user123 276 + X-Client-Key: hvc_... 277 + Authorization: DPoP <access_token> 278 + DPoP: <proof_jwt> 279 + ``` 280 + 281 + ### DPoP proof format 282 + 283 + If you're implementing the flow without the SDK, a DPoP proof JWT looks like this: 284 + 285 + **Header:** 286 + 287 + ```json 288 + { 289 + "alg": "ES256", 290 + "typ": "dpop+jwt", 291 + "jwk": { 292 + "kty": "EC", 293 + "crv": "P-256", 294 + "x": "...", 295 + "y": "..." 296 + } 297 + } 298 + ``` 299 + 300 + **Payload:** 301 + 302 + ```json 303 + { 304 + "htm": "POST", 305 + "htu": "https://happyview.example.com/xrpc/com.example.createPost", 306 + "iat": 1745452800, 307 + "ath": "<base64url SHA-256 of the access token>", 308 + "jti": "<unique identifier>" 309 + } 310 + ``` 311 + 312 + Validation rules: 313 + 314 + - `htm` must match the HTTP method (case-insensitive) 315 + - `htu` must match the request URL (scheme + host + path, no query string) 316 + - `iat` must be within 5 minutes of the server's clock 317 + - `ath` must be the base64url-encoded SHA-256 hash of the access token 318 + - The JWK thumbprint (RFC 7638, SHA-256) must match the key used during provisioning 319 + - The signature must verify against the embedded public JWK 320 + 321 + ## Scopes 322 + 323 + By default, a client's scopes are just `atproto`. You can add custom scopes when creating or updating the client. 324 + 325 + HappyView supports an `include:` directive that expands permission sets defined in lexicons. For example, if your instance has a lexicon `com.example.authBasic` with a `permissions` array in its definition, you can set the client's scopes to: 326 + 327 + ``` 328 + atproto include:com.example.authBasic 329 + ``` 330 + 331 + This expands to include all RPC methods and repository actions defined in that permission set. 332 + 333 + ## Rate limiting 334 + 335 + Each API client has its own token bucket for rate limiting: 336 + 337 + - **Capacity** — maximum tokens in the bucket 338 + - **Refill rate** — tokens added per second 339 + 340 + If not set on the client, the instance defaults apply (`DEFAULT_RATE_LIMIT_CAPACITY` and `DEFAULT_RATE_LIMIT_REFILL_RATE`). 341 + 342 + Rate limit state is returned in response headers: 343 + 344 + | Header | Description | 345 + | --------------------- | ------------------------------------------- | 346 + | `RateLimit-Limit` | Bucket capacity | 347 + | `RateLimit-Remaining` | Tokens remaining | 348 + | `RateLimit-Reset` | Unix timestamp when the bucket will be full | 349 + | `Retry-After` | Seconds to wait (only on `429` responses) | 350 + 351 + Adjust per-client rate limits via the dashboard or the [admin API](../reference/admin/api-clients.md#update-an-api-client). 352 + 353 + ## Security notes 354 + 355 + - Client secrets are SHA-256 hashed before storage — HappyView never stores the plaintext. 356 + - DPoP private keys and OAuth tokens are encrypted at rest with AES-256-GCM using the `TOKEN_ENCRYPTION_KEY` environment variable. 357 + - Re-authenticating the same user with the same client upserts the session. The old DPoP key is cleaned up automatically. 358 + - Multiple clients can have active sessions for the same user — sessions are isolated per client. 359 + 360 + ## Next steps 361 + 362 + - [Authentication](../getting-started/authentication.md) — full protocol details and security model 363 + - [JavaScript SDK](../sdk/overview.md) — get started with the SDK 364 + - [Admin API — API Clients](../reference/admin/api-clients.md) — CRUD endpoints 365 + - [Permissions](permissions.md) — control who can manage API clients
+5
packages/docs/sidebars.ts
··· 183 183 items: [ 184 184 { 185 185 type: "doc", 186 + id: "guides/api-clients", 187 + label: "API Clients", 188 + }, 189 + { 190 + type: "doc", 186 191 id: "guides/labelers", 187 192 label: "Labelers", 188 193 },