Skip to content

Authentication

How-to

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:

WhoCallsWith
AnyoneXRPC queriesNothing. The request is anonymous
A signed-in user, through your appXRPC queries and proceduresA DPoP session and an API client key
You, in the dashboardThe admin APIThe session cookie from signing in
Scripts, CI and cron jobsThe admin APIAn admin API key
hpy, or another app you allowThe admin APIA DPoP session granted dev.happyview.adminFull

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.

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.

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_uri and redirect_uris;
  • the scopes it may ask for. The default is atproto;
  • a client_type: confidential (the default) for a server that can keep a secret, or public for 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.

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());
}

You only need this if you aren’t using the SDK. The full protocol is in OAuth.

  1. Your app asks HappyView for a DPoP key with POST /oauth/dpop-keys. A confidential client sends X-Client-Key and X-Client-Secret. A public client sends X-Client-Key, a matching Origin, and a PKCE pkce_challenge in the body. HappyView returns a provision_id and the private key.
  2. Your app runs OAuth with the user’s PDS, signing its DPoP proofs with that key.
  3. Your app hands the tokens back with POST /oauth/sessions. The granted scope must include atproto, and every scope in it must be one the API client registered. A public client also sends its pkce_verifier.
  4. From then on, each XRPC call carries Authorization: DPoP <access token>, a DPoP proof header and X-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.

A script sees the caller in ctx:

  • ctx.caller_did is the signed-in user’s DID. It is absent when the request is anonymous.
  • ctx.has_pds_auth is true when 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 }
end

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.

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:

Terminal window
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:

Terminal window
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.

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:

  1. Add include:dev.happyview.adminFull to the API client’s scopes.
  2. Have the app request that scope when the user signs in.
  3. 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.