Authentication
Read first: getting-started/scripts, getting-started/plugins-and-libraries
This page shows you the ways a person or a program proves who it is to HappyView, and what each one can reach.
There are two sides to it. Your app’s users call your XRPC endpoints under /xrpc/. Operators and automation call the admin API under /admin/. They take different credentials:
| Who | Calls | With |
|---|---|---|
| Anyone | XRPC queries | Nothing. The request is anonymous |
| A signed-in user, through your app | XRPC queries and procedures | A DPoP session and an API client key |
| You, in the dashboard | The admin API | The session cookie from signing in |
| Scripts, CI and cron jobs | The admin API | An admin API key |
| hpy, or another app you allow | The admin API | A DPoP session granted dev.happyview.adminFull |
Sign in to the dashboard
Section titled “Sign in to the dashboard”You sign in to the dashboard with your atproto account. Open your instance, enter your handle, and approve the sign-in on your PDS. HappyView sets a signed session cookie, and the dashboard uses it for every admin call it makes for you.
On a new instance, the first account to sign in becomes the operator, with the superadmin privilege and every permission. To choose that account in advance, set SUPERADMIN_DID. HappyView then gives that DID superadmin at boot and does not promote whoever signs in first.
Signing in needs a secure SESSION_SECRET of at least 32 bytes. With a missing or weak one, HappyView refuses to start a sign-in.
To let more people into the dashboard, add them as users and give them permissions. See Permissions.
Let your app’s users sign in
Section titled “Let your app’s users sign in”Your users sign in to your app, not to HappyView. Your app runs an atproto OAuth flow with the user’s PDS, and HappyView holds a copy of the session so it can act for that user.
Register an API client
Section titled “Register an API client”Every app that signs users in needs an API client. Open API Clients in the dashboard, under Access, and click Create API Client, or call POST /admin/api-clients. You give it:
- your app’s
client_id_url(where its OAuth client metadata lives),client_uriandredirect_uris; - the
scopesit may ask for. The default isatproto; - a
client_type:confidential(the default) for a server that can keep a secret, orpublicfor a browser app; - for a public client, its
allowed_origins.
You get back a client key starting with hvc_. A confidential client also gets a secret starting with hvs_. The secret is shown once, so save it.
Sign in with the SDK
Section titled “Sign in with the SDK”The JavaScript SDK runs the whole flow. In a browser app:
import { HappyViewBrowserClient } from "@happyview/oauth-client-browser";
const client = new HappyViewBrowserClient({ instanceUrl: "https://happyview.example.com", clientId: "https://app.example.com/oauth-client-metadata.json", clientKey: "hvc_your_client_key",});
// Restores a saved session, or finishes a sign-in when the user returns.const result = await client.init();
if (!result) { // Sends the user to their PDS to approve the sign-in. await client.signIn("alice.example.com");} else { // Signed in: calls carry the DPoP proof and the client key for you. const response = await result.session.fetchHandler( "/xrpc/xyz.statusphere.getStatuses", { method: "GET" }, ); console.log(result.session.did, await response.json());}What happens underneath
Section titled “What happens underneath”You only need this if you aren’t using the SDK. The full protocol is in OAuth.
- Your app asks HappyView for a DPoP key with
POST /oauth/dpop-keys. A confidential client sendsX-Client-KeyandX-Client-Secret. A public client sendsX-Client-Key, a matchingOrigin, and a PKCEpkce_challengein the body. HappyView returns aprovision_idand the private key. - Your app runs OAuth with the user’s PDS, signing its DPoP proofs with that key.
- Your app hands the tokens back with
POST /oauth/sessions. The granted scope must includeatproto, and every scope in it must be one the API client registered. A public client also sends itspkce_verifier. - From then on, each XRPC call carries
Authorization: DPoP <access token>, aDPoPproof header andX-Client-Key.
Because HappyView and your app hold the same key, HappyView can sign its own proofs and write to the user’s PDS for them. Each device gets its own key and its own session.
Who is calling, inside a script
Section titled “Who is calling, inside a script”A script sees the caller in ctx:
ctx.caller_didis the signed-in user’s DID. It is absent when the request is anonymous.ctx.has_pds_authistruewhen HappyView holds a session it can use to write to that user’s PDS.
Queries may be anonymous. Procedures may not: a procedure called with no credentials gets 401 before the script runs. In a query, ctx.has_pds_auth is always false, so write to the PDS from a procedure.
This query answers differently for signed-in and anonymous callers:
function handle(input, ctx) if ctx.caller_did == nil then return { signedIn = false } end
return { signedIn = true, did = ctx.caller_did }endexport default function handle(input, ctx) { if (ctx.caller_did === undefined) { return { signedIn: false }; }
return { signedIn: true, did: ctx.caller_did };}interface Ctx { caller_did?: string; has_pds_auth: boolean;}
export default function handle(input: Record<string, unknown>, ctx: Ctx) { if (ctx.caller_did === undefined) { return { signedIn: false }; }
return { signedIn: true, did: ctx.caller_did };}Other AppViews and services can also call your XRPC endpoints with an atproto service-auth JWT. They don’t need to be registered: when your instance exposes a service identity, any valid service-auth JWT is accepted. See Service identity.
Use the admin API with an API key
Section titled “Use the admin API with an API key”For automation, create an admin API key. Open API Keys in the dashboard, under Access, and click Create API Key, or call the admin API:
curl -X POST "http://127.0.0.1:3000/admin/api-keys" \ -H "$AUTH" \ -H "Content-Type: application/json" \ -d '{ "name": "ci", "permissions": ["lexicons:read", "records:read"] }'The response’s key starts with hv_. It is shown once. Send it as a bearer token:
curl "http://127.0.0.1:3000/admin/lexicons" \ -H "Authorization: Bearer hv_your_api_key"A key carries only the permissions you picked, and only those its owner still holds. A key never holds superadmin, even when its owner does, so superadmin-only actions aren’t available through keys. Creating a key needs api-keys:create, and you can’t give a key a permission you don’t have. See API keys.
Let an app manage your instance
Section titled “Let an app manage your instance”An app’s DPoP session belongs to the app. Signing in to an app as an admin does not, by default, let that app use the admin API. The app must ask for the dev.happyview.adminFull permission set, and you must approve it at sign-in.
dev.happyview.adminFull is a permission set titled “Manage your HappyView instances”. It holds one permission: rpc on dev.happyview.admin.manage, for any audience. To use it:
- Add
include:dev.happyview.adminFullto the API client’s scopes. - Have the app request that scope when the user signs in.
- The user approves it on their PDS’s consent screen.
The session then reaches /admin/ with the user’s own permissions, no more. HappyView checks the scope the user’s authorization server actually granted, not the scope the app registered, and checks again each time the access token expires. Revoking the grant at the PDS ends admin access within one token lifetime. A session without the grant gets 403.
hpy, the HappyView CLI, works this way. Every instance has a built-in public API client for it, hvc_hpy, whose scopes are atproto include:dev.happyview.adminFull. GET /config publishes the key as cli_client_key.
Next steps
Section titled “Next steps”- Testing: run your scripts against forged callers
- API clients: register and manage the apps that sign users in
- API keys
- Permissions: every permission and what it grants
- Service identity
- OAuth: the DPoP provisioning and session endpoints
- XRPC
- JavaScript SDK

