Something went wrong. Try again.
A lexicon-driven AppView for ATProto.
Something went wrong. Try again.
63 kB · 123 lines
HTML
at gh-pages
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123<!doctype html><html lang="en" dir="ltr" class="docs-wrapper plugin-docs plugin-id-default docs-version-current docs-doc-page docs-doc-id-getting-started/authentication" data-has-hydrated="false"><head><meta charset="UTF-8"><meta name="generator" content="Docusaurus v3.10.0"><title data-rh="true">Authentication | HappyView</title><meta data-rh="true" name="viewport" content="width=device-width,initial-scale=1"><meta data-rh="true" name="twitter:card" content="summary_large_image"><meta data-rh="true" property="og:image" content="https://happyview.dev/img/og.png"><meta data-rh="true" name="twitter:image" content="https://happyview.dev/img/og.png"><meta data-rh="true" property="og:url" content="https://happyview.dev/getting-started/authentication"><meta data-rh="true" property="og:locale" content="en"><meta data-rh="true" name="docusaurus_locale" content="en"><meta data-rh="true" name="docsearch:language" content="en"><meta data-rh="true" name="docusaurus_version" content="current"><meta data-rh="true" name="docusaurus_tag" content="docs-default-current"><meta data-rh="true" name="docsearch:version" content="current"><meta data-rh="true" name="docsearch:docusaurus_tag" content="docs-default-current"><meta data-rh="true" property="og:title" content="Authentication | HappyView"><meta data-rh="true" name="description" content="HappyView has two distinct authentication surfaces:"><meta data-rh="true" property="og:description" content="HappyView has two distinct authentication surfaces:"><link data-rh="true" rel="icon" href="/img/favicon.png"><link data-rh="true" rel="canonical" href="https://happyview.dev/getting-started/authentication"><link data-rh="true" rel="alternate" href="https://happyview.dev/getting-started/authentication" hreflang="en"><link data-rh="true" rel="alternate" href="https://happyview.dev/getting-started/authentication" hreflang="x-default"><script data-rh="true" type="application/ld+json">{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"name":"Authentication","item":"https://happyview.dev/getting-started/authentication"}]}</script><link rel="stylesheet" href="/assets/css/styles.18b9c377.css"><script src="/assets/js/runtime~main.413aac68.js" defer="defer"></script><script src="/assets/js/main.c991874c.js" defer="defer"></script></head><body><svg style="display: none;"><defs><symbol id="theme-svg-external-link" viewBox="0 0 24 24"><path fill="currentColor" d="M21 13v10h-21v-19h12v2h-10v15h17v-8h2zm3-12h-10.988l4.035 4-6.977 7.07 2.828 2.828 6.977-7.07 4.125 4.172v-11z"/></symbol></defs></svg><script>!function(){var t=function(){try{return new URLSearchParams(window.location.search).get("docusaurus-theme")}catch(t){}}()||function(){try{return window.localStorage.getItem("theme")}catch(t){}}();document.documentElement.setAttribute("data-theme",t||"light"),document.documentElement.setAttribute("data-theme-choice",t||"light")}(),function(){try{const c=new URLSearchParams(window.location.search).entries();for(var[t,e]of c)if(t.startsWith("docusaurus-data-")){var a=t.replace("docusaurus-data-","data-");document.documentElement.setAttribute(a,e)}}catch(t){}}()</script><div id="__docusaurus"><link rel="preload" as="image" href="/img/logo.light.png"><link rel="preload" as="image" href="/img/logo.dark.png"><div role="region" aria-label="Skip to main content"><a class="skipToContent_hn_G" href="#__docusaurus_skipToContent_fallback">Skip to main content</a></div><nav aria-label="Main" class="theme-layout-navbar navbar navbar--fixed-top"><div class="navbar__inner"><div class="theme-layout-navbar-left navbar__items"><button aria-label="Toggle navigation bar" aria-expanded="false" class="navbar__toggle clean-btn" type="button"><svg width="30" height="30" viewBox="0 0 30 30" aria-hidden="true"><path stroke="currentColor" stroke-linecap="round" stroke-miterlimit="10" stroke-width="2" d="M4 7h22M4 15h22M4 23h22"></path></svg></button><a class="navbar__brand" href="/"><div class="navbar__logo"><img src="/img/logo.light.png" alt="HappyView" class="themedComponent_EfZ_ themedComponent--light_EnZ3"><img src="/img/logo.dark.png" alt="HappyView" class="themedComponent_EfZ_ themedComponent--dark_UTRq"></div><b class="navbar__title text--truncate"></b></a><a aria-current="page" class="navbar__item navbar__link navbar__link--active" href="/">Docs</a></div><div class="theme-layout-navbar-right navbar__items navbar__items--right"><a href="https://github.com/gamesgamesgamesgamesgames/happyview" target="_blank" rel="noopener noreferrer" class="navbar__item navbar__link">GitHub<svg width="13.5" height="13.5" aria-label="(opens in new tab)" class="iconExternalLink_yqny"><use href="#theme-svg-external-link"></use></svg></a><div class="toggle_pDN8 colorModeToggle_WR5U"><button class="clean-btn toggleButton__Co1 toggleButtonDisabled_pTNf" type="button" disabled="" title="system mode" aria-label="Switch between dark and light mode (currently system mode)"><svg viewBox="0 0 24 24" width="24" height="24" aria-hidden="true" class="toggleIcon_IF_c lightToggleIcon_z1QC"><path fill="currentColor" d="M12,9c1.65,0,3,1.35,3,3s-1.35,3-3,3s-3-1.35-3-3S10.35,9,12,9 M12,7c-2.76,0-5,2.24-5,5s2.24,5,5,5s5-2.24,5-5 S14.76,7,12,7L12,7z M2,13l2,0c0.55,0,1-0.45,1-1s-0.45-1-1-1l-2,0c-0.55,0-1,0.45-1,1S1.45,13,2,13z M20,13l2,0c0.55,0,1-0.45,1-1 s-0.45-1-1-1l-2,0c-0.55,0-1,0.45-1,1S19.45,13,20,13z M11,2v2c0,0.55,0.45,1,1,1s1-0.45,1-1V2c0-0.55-0.45-1-1-1S11,1.45,11,2z M11,20v2c0,0.55,0.45,1,1,1s1-0.45,1-1v-2c0-0.55-0.45-1-1-1C11.45,19,11,19.45,11,20z M5.99,4.58c-0.39-0.39-1.03-0.39-1.41,0 c-0.39,0.39-0.39,1.03,0,1.41l1.06,1.06c0.39,0.39,1.03,0.39,1.41,0s0.39-1.03,0-1.41L5.99,4.58z M18.36,16.95 c-0.39-0.39-1.03-0.39-1.41,0c-0.39,0.39-0.39,1.03,0,1.41l1.06,1.06c0.39,0.39,1.03,0.39,1.41,0c0.39-0.39,0.39-1.03,0-1.41 L18.36,16.95z M19.42,5.99c0.39-0.39,0.39-1.03,0-1.41c-0.39-0.39-1.03-0.39-1.41,0l-1.06,1.06c-0.39,0.39-0.39,1.03,0,1.41 s1.03,0.39,1.41,0L19.42,5.99z M7.05,18.36c0.39-0.39,0.39-1.03,0-1.41c-0.39-0.39-1.03-0.39-1.41,0l-1.06,1.06 c-0.39,0.39-0.39,1.03,0,1.41s1.03,0.39,1.41,0L7.05,18.36z"></path></svg><svg viewBox="0 0 24 24" width="24" height="24" aria-hidden="true" class="toggleIcon_IF_c darkToggleIcon_O_bk"><path fill="currentColor" d="M9.37,5.51C9.19,6.15,9.1,6.82,9.1,7.5c0,4.08,3.32,7.4,7.4,7.4c0.68,0,1.35-0.09,1.99-0.27C17.45,17.19,14.93,19,12,19 c-3.86,0-7-3.14-7-7C5,9.07,6.81,6.55,9.37,5.51z M12,3c-4.97,0-9,4.03-9,9s4.03,9,9,9s9-4.03,9-9c0-0.46-0.04-0.92-0.1-1.36 c-0.98,1.37-2.58,2.26-4.4,2.26c-2.98,0-5.4-2.42-5.4-5.4c0-1.81,0.89-3.42,2.26-4.4C12.92,3.04,12.46,3,12,3L12,3z"></path></svg><svg viewBox="0 0 24 24" width="24" height="24" aria-hidden="true" class="toggleIcon_IF_c systemToggleIcon_SKeC"><path fill="currentColor" d="m12 21c4.971 0 9-4.029 9-9s-4.029-9-9-9-9 4.029-9 9 4.029 9 9 9zm4.95-13.95c1.313 1.313 2.05 3.093 2.05 4.95s-0.738 3.637-2.05 4.95c-1.313 1.313-3.093 2.05-4.95 2.05v-14c1.857 0 3.637 0.737 4.95 2.05z"></path></svg></button></div><div class="navbarSearchContainer_qQFS"></div></div></div><div role="presentation" class="navbar-sidebar__backdrop"></div></nav><div id="__docusaurus_skipToContent_fallback" class="theme-layout-main main-wrapper mainWrapper_NcQk"><div class="docsWrapper_tjyD"><button aria-label="Scroll back to top" class="clean-btn theme-back-to-top-button backToTopButton_J6bC" type="button"></button><div class="docRoot_mCS9"><aside class="theme-doc-sidebar-container docSidebarContainer_AIvH"><div class="sidebarViewport_v5_F"><div class="sidebar_TbVu"><nav aria-label="Docs sidebar" class="menu thin-scrollbar menu_hz51"><ul class="theme-doc-sidebar-menu menu__list"><li class="theme-doc-sidebar-item-link theme-doc-sidebar-item-link-level-1 menu__list-item"><a class="menu__link" href="/"><span title="Introduction" class="linkLabel_GBZK">Introduction</span></a></li><li class="theme-doc-sidebar-item-category theme-doc-sidebar-item-category-level-1 menu__list-item"><div class="menu__list-item-collapsible"><a class="categoryLink_RHuM menu__link menu__link--sublist menu__link--sublist-caret menu__link--active" role="button" aria-expanded="true" href="/getting-started/quickstart"><span title="Getting Started" class="categoryLinkLabel_ppUb">Getting Started</span></a></div><ul class="menu__list"><li class="theme-doc-sidebar-item-link theme-doc-sidebar-item-link-level-2 menu__list-item"><a class="menu__link" tabindex="0" href="/getting-started/quickstart"><span title="Quickstart" class="linkLabel_GBZK">Quickstart</span></a></li><li class="theme-doc-sidebar-item-category theme-doc-sidebar-item-category-level-2 menu__list-item menu__list-item--collapsed"><div class="menu__list-item-collapsible"><a class="categoryLink_RHuM menu__link menu__link--sublist menu__link--sublist-caret" role="button" aria-expanded="false" tabindex="0" href="/getting-started/deployment/railway"><span title="Deployment" class="categoryLinkLabel_ppUb">Deployment</span></a></div></li><li class="theme-doc-sidebar-item-link theme-doc-sidebar-item-link-level-2 menu__list-item"><a class="menu__link" tabindex="0" href="/getting-started/configuration"><span title="Configuration" class="linkLabel_GBZK">Configuration</span></a></li><li class="theme-doc-sidebar-item-link theme-doc-sidebar-item-link-level-2 menu__list-item"><a class="menu__link menu__link--active" aria-current="page" tabindex="0" href="/getting-started/authentication"><span title="Authentication" class="linkLabel_GBZK">Authentication</span></a></li><li class="theme-doc-sidebar-item-link theme-doc-sidebar-item-link-level-2 menu__list-item"><a class="menu__link" tabindex="0" href="/getting-started/dashboard"><span title="Dashboard" class="linkLabel_GBZK">Dashboard</span></a></li></ul></li><li class="theme-doc-sidebar-item-category theme-doc-sidebar-item-category-level-1 menu__list-item menu__list-item--collapsed"><div class="menu__list-item-collapsible"><a class="categoryLink_RHuM menu__link menu__link--sublist menu__link--sublist-caret" role="button" aria-expanded="false" href="/tutorials/statusphere"><span title="Tutorials" class="categoryLinkLabel_ppUb">Tutorials</span></a></div></li><li class="theme-doc-sidebar-item-category theme-doc-sidebar-item-category-level-1 menu__list-item menu__list-item--collapsed"><div class="menu__list-item-collapsible"><a class="categoryLink_RHuM menu__link menu__link--sublist menu__link--sublist-caret" role="button" aria-expanded="false" href="/guides/lexicons"><span title="Guides" class="categoryLinkLabel_ppUb">Guides</span></a></div></li><li class="theme-doc-sidebar-item-category theme-doc-sidebar-item-category-level-1 menu__list-item menu__list-item--collapsed"><div class="menu__list-item-collapsible"><a class="categoryLink_RHuM menu__link menu__link--sublist menu__link--sublist-caret" role="button" aria-expanded="false" href="/sdk/overview"><span title="JavaScript SDK" class="categoryLinkLabel_ppUb">JavaScript SDK</span></a></div></li><li class="theme-doc-sidebar-item-category theme-doc-sidebar-item-category-level-1 menu__list-item menu__list-item--collapsed"><div class="menu__list-item-collapsible"><a class="categoryLink_RHuM menu__link menu__link--sublist menu__link--sublist-caret" role="button" aria-expanded="false" href="/reference/xrpc-api"><span title="Reference" class="categoryLinkLabel_ppUb">Reference</span></a></div></li></ul></nav></div></div></aside><main class="docMainContainer_Gcuv"><div class="container padding-top--md padding-bottom--lg"><div class="row"><div class="col docItemCol_SLVn"><div class="docItemContainer_gUK8"><article><nav class="theme-doc-breadcrumbs breadcrumbsContainer__5rh" aria-label="Breadcrumbs"><ul class="breadcrumbs"><li class="breadcrumbs__item"><a aria-label="Home page" class="breadcrumbs__link" href="/"><svg viewBox="0 0 24 24" class="breadcrumbHomeIcon_ilNI"><path d="M10 19v-5h4v5c0 .55.45 1 1 1h3c.55 0 1-.45 1-1v-7h1.7c.46 0 .68-.57.33-.87L12.67 3.6c-.38-.34-.96-.34-1.34 0l-8.36 7.53c-.34.3-.13.87.33.87H5v7c0 .55.45 1 1 1h3c.55 0 1-.45 1-1z" fill="currentColor"></path></svg></a></li><li class="breadcrumbs__item"><span class="breadcrumbs__link">Getting Started</span></li><li class="breadcrumbs__item breadcrumbs__item--active"><span class="breadcrumbs__link">Authentication</span></li></ul></nav><div class="tocCollapsible_vaUl theme-doc-toc-mobile tocMobile_CcuZ"><button type="button" class="clean-btn tocCollapsibleButton__AHn">On this page</button></div><div class="theme-doc-markdown markdown"><header><h1>Authentication</h1></header><p>HappyView has two distinct authentication surfaces:</p><ul><li class=""><strong>XRPC</strong> (<code>/xrpc/*</code>) — client-level identification via an <strong>API client key</strong> on every request, plus optional user-level atproto OAuth for endpoints that need a specific user's identity (e.g. procedures that write to a PDS).</li><li class=""><strong>Admin API</strong> (<code>/admin/*</code>) — user-level authentication via admin API keys or service auth JWTs, gated by <a class="" href="/guides/permissions">permissions</a>.</li></ul><h2 class="anchor anchorTargetStickyNavbar_zGuS" id="which-endpoints-require-what">Which endpoints require what?<a href="#which-endpoints-require-what" class="hash-link" aria-label="Direct link to Which endpoints require what?" title="Direct link to Which endpoints require what?" translate="no"></a></h2><table><thead><tr><th>Endpoint type</th><th>Client identification</th><th>User authentication</th></tr></thead><tbody><tr><td>Queries (<code>GET /xrpc/{method}</code>)</td><td><code>X-Client-Key</code> required</td><td>Optional — DPoP auth if the query needs to know who the user is</td></tr><tr><td>Procedures (<code>POST /xrpc/{method}</code>)</td><td><code>X-Client-Key</code> required</td><td>Required — DPoP auth so HappyView can proxy writes to the user's PDS</td></tr><tr><td>Admin API (<code>/admin/*</code>)</td><td>—</td><td>Required — admin API key or service auth JWT with the right <a class="" href="/guides/permissions">permissions</a></td></tr><tr><td>Health check (<code>GET /health</code>)</td><td>—</td><td>—</td></tr></tbody></table><h2 class="anchor anchorTargetStickyNavbar_zGuS" id="xrpc-api-client-identification">XRPC: API client identification<a href="#xrpc-api-client-identification" class="hash-link" aria-label="Direct link to XRPC: API client identification" title="Direct link to XRPC: API client identification" translate="no"></a></h2><p>Every XRPC request — including unauthenticated <code>GET</code> 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 <code>401 Unauthorized</code> with <code>Missing client identification</code>.</p><p>Register a client in the dashboard (<strong>Settings > API Clients > New client</strong>) or via <code>POST /admin/api-clients</code>. You'll get back an <code>hvc_…</code> client key and an <code>hvs_…</code> client secret — <strong>the secret is only shown once</strong>, so capture it immediately.</p><p>HappyView resolves the client key from the first of:</p><ol><li class="">The <code>X-Client-Key</code> request header.</li><li class="">A <code>client_key</code> query-string parameter.</li></ol><p>On top of the client key, HappyView does best-effort validation that the caller actually controls the client:</p><ul><li class="">If an <code>Origin</code> header is present (typical for browser apps), it must match the client's registered <code>client_uri</code>.</li><li class="">Otherwise, an <code>X-Client-Secret</code> header may be supplied and must match the stored secret (typical for server-to-server callers).</li></ul><p>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.</p><h3 class="anchor anchorTargetStickyNavbar_zGuS" id="calling-a-query">Calling a query<a href="#calling-a-query" class="hash-link" aria-label="Direct link to Calling a query" title="Direct link to Calling a query" translate="no"></a></h3><div class="language-sh codeBlockContainer_ei_j theme-code-block" style="--prism-color:#bfc7d5;--prism-background-color:#292d3e"><div class="codeBlockContent_p9UO"><pre tabindex="0" class="prism-code language-sh codeBlock_NBAy thin-scrollbar" style="color:#bfc7d5;background-color:#292d3e"><code class="codeBlockLines_AQpN"><div class="token-line" style="color:#bfc7d5"><span class="token plain">curl 'https://happyview.example.com/xrpc/com.example.feed.getHot' \</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain"> -H 'X-Client-Key: hvc_a1b2c3...'</span><br></div></code></pre></div></div><p>For a server-to-server integration, add the secret:</p><div class="language-sh codeBlockContainer_ei_j theme-code-block" style="--prism-color:#bfc7d5;--prism-background-color:#292d3e"><div class="codeBlockContent_p9UO"><pre tabindex="0" class="prism-code language-sh codeBlock_NBAy thin-scrollbar" style="color:#bfc7d5;background-color:#292d3e"><code class="codeBlockLines_AQpN"><div class="token-line" style="color:#bfc7d5"><span class="token plain">curl 'https://happyview.example.com/xrpc/com.example.feed.getHot' \</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain"> -H 'X-Client-Key: hvc_a1b2c3...' \</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain"> -H 'X-Client-Secret: hvs_d4e5f6...'</span><br></div></code></pre></div></div><h3 class="anchor anchorTargetStickyNavbar_zGuS" id="authenticating-users-for-procedures">Authenticating users for procedures<a href="#authenticating-users-for-procedures" class="hash-link" aria-label="Direct link to Authenticating users for procedures" title="Direct link to Authenticating users for procedures" translate="no"></a></h3><p>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 atproto OAuth session.</p><p>XRPC routes only accept <strong>DPoP auth</strong> (<code>Authorization: DPoP <token></code> + <code>DPoP</code> proof header + <code>X-Client-Key</code>). Bearer tokens and service auth JWTs are not accepted on XRPC endpoints.</p><p>Third-party apps authenticate users through the <a href="#dpop-key-provisioning-for-third-party-apps" class="">DPoP key provisioning</a> 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.</p><p>The <a class="" href="/sdk/overview">JavaScript SDK</a> handles this entire flow for you:</p><div class="language-typescript codeBlockContainer_ei_j theme-code-block" style="--prism-color:#bfc7d5;--prism-background-color:#292d3e"><div class="codeBlockContent_p9UO"><pre tabindex="0" class="prism-code language-typescript codeBlock_NBAy thin-scrollbar" style="color:#bfc7d5;background-color:#292d3e"><code class="codeBlockLines_AQpN"><div class="token-line" style="color:#bfc7d5"><span class="token keyword" style="font-style:italic">import</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(199, 146, 234)">{</span><span class="token plain"> Client </span><span class="token punctuation" style="color:rgb(199, 146, 234)">}</span><span class="token plain"> </span><span class="token keyword" style="font-style:italic">from</span><span class="token plain"> </span><span class="token string" style="color:rgb(195, 232, 141)">"@atproto/lex"</span><span class="token punctuation" style="color:rgb(199, 146, 234)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain"></span><span class="token keyword" style="font-style:italic">import</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(199, 146, 234)">{</span><span class="token plain"> HappyViewBrowserClient </span><span class="token punctuation" style="color:rgb(199, 146, 234)">}</span><span class="token plain"> </span><span class="token keyword" style="font-style:italic">from</span><span class="token plain"> </span><span class="token string" style="color:rgb(195, 232, 141)">"@happyview/oauth-client-browser"</span><span class="token punctuation" style="color:rgb(199, 146, 234)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain"></span><span class="token keyword" style="font-style:italic">import</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(199, 146, 234)">{</span><span class="token plain"> createAgent </span><span class="token punctuation" style="color:rgb(199, 146, 234)">}</span><span class="token plain"> </span><span class="token keyword" style="font-style:italic">from</span><span class="token plain"> </span><span class="token string" style="color:rgb(195, 232, 141)">"@happyview/lex-agent"</span><span class="token punctuation" style="color:rgb(199, 146, 234)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain"></span><span class="token keyword" style="font-style:italic">const</span><span class="token plain"> oauthClient </span><span class="token operator" style="color:rgb(137, 221, 255)">=</span><span class="token plain"> </span><span class="token keyword" style="font-style:italic">new</span><span class="token plain"> </span><span class="token class-name" style="color:rgb(255, 203, 107)">HappyViewBrowserClient</span><span class="token punctuation" style="color:rgb(199, 146, 234)">(</span><span class="token punctuation" style="color:rgb(199, 146, 234)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain"> instanceUrl</span><span class="token operator" style="color:rgb(137, 221, 255)">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(195, 232, 141)">"https://happyview.example.com"</span><span class="token punctuation" style="color:rgb(199, 146, 234)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain"> clientKey</span><span class="token operator" style="color:rgb(137, 221, 255)">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(195, 232, 141)">"hvc_your_client_key"</span><span class="token punctuation" style="color:rgb(199, 146, 234)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain"></span><span class="token punctuation" style="color:rgb(199, 146, 234)">}</span><span class="token punctuation" style="color:rgb(199, 146, 234)">)</span><span class="token punctuation" style="color:rgb(199, 146, 234)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain"></span><span class="token comment" style="color:rgb(105, 112, 152);font-style:italic">// Login — redirects to the user's PDS for authorization</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain"></span><span class="token keyword" style="font-style:italic">await</span><span class="token plain"> oauthClient</span><span class="token punctuation" style="color:rgb(199, 146, 234)">.</span><span class="token function" style="color:rgb(130, 170, 255)">login</span><span class="token punctuation" style="color:rgb(199, 146, 234)">(</span><span class="token string" style="color:rgb(195, 232, 141)">"alice.bsky.social"</span><span class="token punctuation" style="color:rgb(199, 146, 234)">)</span><span class="token punctuation" style="color:rgb(199, 146, 234)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain"></span><span class="token comment" style="color:rgb(105, 112, 152);font-style:italic">// On /oauth/callback — complete the token exchange</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain"></span><span class="token keyword" style="font-style:italic">const</span><span class="token plain"> session </span><span class="token operator" style="color:rgb(137, 221, 255)">=</span><span class="token plain"> </span><span class="token keyword" style="font-style:italic">await</span><span class="token plain"> oauthClient</span><span class="token punctuation" style="color:rgb(199, 146, 234)">.</span><span class="token function" style="color:rgb(130, 170, 255)">callback</span><span class="token punctuation" style="color:rgb(199, 146, 234)">(</span><span class="token punctuation" style="color:rgb(199, 146, 234)">)</span><span class="token punctuation" style="color:rgb(199, 146, 234)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain"></span><span class="token comment" style="color:rgb(105, 112, 152);font-style:italic">// Create a type-safe Lex client</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain"></span><span class="token keyword" style="font-style:italic">const</span><span class="token plain"> agent </span><span class="token operator" style="color:rgb(137, 221, 255)">=</span><span class="token plain"> </span><span class="token function" style="color:rgb(130, 170, 255)">createAgent</span><span class="token punctuation" style="color:rgb(199, 146, 234)">(</span><span class="token plain">session</span><span class="token punctuation" style="color:rgb(199, 146, 234)">)</span><span class="token punctuation" style="color:rgb(199, 146, 234)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain"></span><span class="token keyword" style="font-style:italic">const</span><span class="token plain"> lex </span><span class="token operator" style="color:rgb(137, 221, 255)">=</span><span class="token plain"> </span><span class="token keyword" style="font-style:italic">new</span><span class="token plain"> </span><span class="token class-name" style="color:rgb(255, 203, 107)">Client</span><span class="token punctuation" style="color:rgb(199, 146, 234)">(</span><span class="token plain">agent</span><span class="token punctuation" style="color:rgb(199, 146, 234)">)</span><span class="token punctuation" style="color:rgb(199, 146, 234)">;</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain"></span><span class="token comment" style="color:rgb(105, 112, 152);font-style:italic">// Make authenticated XRPC calls</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain"></span><span class="token keyword" style="font-style:italic">await</span><span class="token plain"> lex</span><span class="token punctuation" style="color:rgb(199, 146, 234)">.</span><span class="token function" style="color:rgb(130, 170, 255)">xrpc</span><span class="token punctuation" style="color:rgb(199, 146, 234)">(</span><span class="token plain">myLexicons</span><span class="token punctuation" style="color:rgb(199, 146, 234)">.</span><span class="token plain">com</span><span class="token punctuation" style="color:rgb(199, 146, 234)">.</span><span class="token plain">example</span><span class="token punctuation" style="color:rgb(199, 146, 234)">.</span><span class="token plain">createPost</span><span class="token punctuation" style="color:rgb(199, 146, 234)">,</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(199, 146, 234)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain"> input</span><span class="token operator" style="color:rgb(137, 221, 255)">:</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(199, 146, 234)">{</span><span class="token plain"> text</span><span class="token operator" style="color:rgb(137, 221, 255)">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(195, 232, 141)">"Hello from HappyView!"</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(199, 146, 234)">}</span><span class="token punctuation" style="color:rgb(199, 146, 234)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain"></span><span class="token punctuation" style="color:rgb(199, 146, 234)">}</span><span class="token punctuation" style="color:rgb(199, 146, 234)">)</span><span class="token punctuation" style="color:rgb(199, 146, 234)">;</span><br></div></code></pre></div></div><p>For procedures, HappyView proxies the write to the user's PDS using the stored OAuth session (see <a href="#proxying-procedures-to-the-users-pds" class="">Proxying procedures</a> below).</p><h2 class="anchor anchorTargetStickyNavbar_zGuS" id="admin-api-user-authentication">Admin API: user authentication<a href="#admin-api-user-authentication" class="hash-link" aria-label="Direct link to Admin API: user authentication" title="Direct link to Admin API: user authentication" translate="no"></a></h2><p>Admin endpoints don't use API clients. They require a real HappyView user, identified by one of two methods:</p><h3 class="anchor anchorTargetStickyNavbar_zGuS" id="admin-api-key">Admin API key<a href="#admin-api-key" class="hash-link" aria-label="Direct link to Admin API key" title="Direct link to Admin API key" translate="no"></a></h3><p>For automation — CI/CD, monitoring, cron jobs — create an <a class="" href="/guides/api-keys">admin API key</a> at <strong>Settings > API Keys</strong> or via <code>POST /admin/api-keys</code> and pass it as a bearer token:</p><div class="language-sh codeBlockContainer_ei_j theme-code-block" style="--prism-color:#bfc7d5;--prism-background-color:#292d3e"><div class="codeBlockContent_p9UO"><pre tabindex="0" class="prism-code language-sh codeBlock_NBAy thin-scrollbar" style="color:#bfc7d5;background-color:#292d3e"><code class="codeBlockLines_AQpN"><div class="token-line" style="color:#bfc7d5"><span class="token plain">export TOKEN="hv_your-api-key-here"</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">curl http://localhost:3000/admin/lexicons \</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain"> -H "Authorization: Bearer $TOKEN"</span><br></div></code></pre></div></div><p>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.</p><h3 class="anchor anchorTargetStickyNavbar_zGuS" id="service-auth-jwt">Service auth JWT<a href="#service-auth-jwt" class="hash-link" aria-label="Direct link to Service auth JWT" title="Direct link to Service auth JWT" translate="no"></a></h3><p>HappyView also accepts standard atproto inter-service auth JWTs in the <code>Authorization</code> 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 <code>#atproto</code> verification method, and treats the issuer DID as the caller identity.</p><p>For a service auth JWT to validate:</p><ul><li class=""><code>alg</code> must be <code>ES256</code> or <code>ES256K</code>.</li><li class=""><code>typ</code> must not be <code>at+jwt</code>, <code>refresh+jwt</code>, or <code>dpop+jwt</code> (those are other token types, not inter-service JWTs).</li><li class=""><code>exp</code> must be in the future.</li><li class="">The signature must verify against the issuer DID's atproto signing key.</li></ul><p>As with the other methods, the resolved DID still has to exist in the HappyView <code>users</code> table with the right permissions to hit admin endpoints — service auth gets you identified, not privileged.</p><h3 class="anchor anchorTargetStickyNavbar_zGuS" id="admin-access-and-the-first-user">Admin access and the first user<a href="#admin-access-and-the-first-user" class="hash-link" aria-label="Direct link to Admin access and the first user" title="Direct link to Admin access and the first user" translate="no"></a></h3><p>On a fresh deployment, the <code>users</code> table is empty. The first authenticated request to any admin endpoint auto-bootstraps that user as the <strong>super user</strong> with all permissions granted. This includes logging in to the dashboard — the dashboard makes admin API calls on your behalf, so the first person to log in becomes the super user.</p><p>To add more users after that, use <code>POST /admin/users</code> or the <a class="" href="/getting-started/dashboard">dashboard</a>. You can assign permissions individually or use a template (<code>viewer</code>, <code>operator</code>, <code>manager</code>, <code>full_access</code>). See <a class="" href="/reference/admin/users">Admin API — Users</a> for details.</p><h2 class="anchor anchorTargetStickyNavbar_zGuS" id="proxying-procedures-to-the-users-pds">Proxying procedures to the user's PDS<a href="#proxying-procedures-to-the-users-pds" class="hash-link" aria-label="Direct link to Proxying procedures to the user's PDS" title="Direct link to Proxying procedures to the user's PDS" translate="no"></a></h2><p>When a client calls an XRPC procedure that writes a record, HappyView proxies the write to the user's PDS. This requires a DPoP-authenticated session — the app must have gone through the <a href="#dpop-key-provisioning-for-third-party-apps" class="">DPoP key provisioning</a> flow and registered tokens for the user. HappyView uses the app's provisioned DPoP key to generate fresh proofs and attach the stored access token to the outbound PDS request.</p><p>A request that only carries an <code>X-Client-Key</code> header (no DPoP token) can hit queries but can't proxy writes — there's no user to write as.</p><h2 class="anchor anchorTargetStickyNavbar_zGuS" id="dpop-key-provisioning-for-third-party-apps">DPoP key provisioning for third-party apps<a href="#dpop-key-provisioning-for-third-party-apps" class="hash-link" aria-label="Direct link to DPoP key provisioning for third-party apps" title="Direct link to DPoP key provisioning for third-party apps" translate="no"></a></h2><p>Third-party apps that want HappyView to make PDS writes on behalf of their users use the <strong>DPoP key provisioning</strong> flow. This avoids browser-based redirects through HappyView's domain, which can be blocked by Firefox's Bounce Tracker Protection.</p><p>The 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 <code>Authorization: DPoP <access_token></code> plus a <code>DPoP</code> proof header and <code>X-Client-Key</code> will have HappyView proxy writes using the stored session.</p><p>The client app and HappyView share the same DPoP keypair, so both can generate valid proofs that the PDS will accept. The PDS binds tokens to a key's thumbprint but it doesn't care who signs the proof, only that it was signed by the right key.</p><h3 class="anchor anchorTargetStickyNavbar_zGuS" id="flow-overview">Flow overview<a href="#flow-overview" class="hash-link" aria-label="Direct link to Flow overview" title="Direct link to Flow overview" translate="no"></a></h3><!-- --><div class="theme-admonition theme-admonition-tip admonition_Dpr7 alert alert--success"><div class="admonitionHeading_d6Ef"><span class="admonitionIcon_epaQ"><svg viewBox="0 0 12 16"><path fill-rule="evenodd" d="M6.5 0C3.48 0 1 2.19 1 5c0 .92.55 2.25 1 3 1.34 2.25 1.78 2.78 2 4v1h5v-1c.22-1.22.66-1.75 2-4 .45-.75 1-2.08 1-3 0-2.81-2.48-5-5.5-5zm3.64 7.48c-.25.44-.47.8-.67 1.11-.86 1.41-1.25 2.06-1.45 3.23-.02.05-.02.11-.02.17H5c0-.06 0-.13-.02-.17-.2-1.17-.59-1.83-1.45-3.23-.2-.31-.42-.67-.67-1.11C2.44 6.78 2 5.65 2 5c0-2.2 2.02-4 4.5-4 1.22 0 2.36.42 3.22 1.19C10.55 2.94 11 3.94 11 5c0 .66-.44 1.78-.86 2.48zM4 14h5c-.23 1.14-1.3 2-2.5 2s-2.27-.86-2.5-2z"></path></svg></span>tip</div><div class="admonitionContent__5gi"><p>The <a class="" href="/sdk/overview">JavaScript SDK</a> handles this entire flow for you. The raw HTTP flow below is useful for understanding the protocol or building a non-JavaScript client.</p></div></div><h3 class="anchor anchorTargetStickyNavbar_zGuS" id="api-clients-confidential-vs-public">API clients: confidential vs public<a href="#api-clients-confidential-vs-public" class="hash-link" aria-label="Direct link to API clients: confidential vs public" title="Direct link to API clients: confidential vs public" translate="no"></a></h3><p>API clients have a <code>client_type</code> field — either <code>confidential</code> (default) or <code>public</code>.</p><ul><li class=""><strong>Confidential clients</strong> authenticate with <code>X-Client-Key</code> + <code>X-Client-Secret</code> headers on every <code>/oauth/*</code> request.</li><li class=""><strong>Public clients</strong> (browser apps that can't keep a secret) authenticate with <code>X-Client-Key</code> header + PKCE. The app sends a <code>pkce_challenge</code> (S256) in the body when provisioning a key, then proves possession with <code>pkce_verifier</code> when registering a session. Public clients also have <code>allowed_origins</code> — the <code>Origin</code> header must match.</li></ul><h3 class="anchor anchorTargetStickyNavbar_zGuS" id="the-full-flow">The full flow<a href="#the-full-flow" class="hash-link" aria-label="Direct link to The full flow" title="Direct link to The full flow" translate="no"></a></h3><h4 class="anchor anchorTargetStickyNavbar_zGuS" id="1-provision-a-dpop-key">1. Provision a DPoP key<a href="#1-provision-a-dpop-key" class="hash-link" aria-label="Direct link to 1. Provision a DPoP key" title="Direct link to 1. Provision a DPoP key" translate="no"></a></h4><div class="language-text codeBlockContainer_ei_j theme-code-block" style="--prism-color:#bfc7d5;--prism-background-color:#292d3e"><div class="codeBlockContent_p9UO"><pre tabindex="0" class="prism-code language-text codeBlock_NBAy thin-scrollbar" style="color:#bfc7d5;background-color:#292d3e"><code class="codeBlockLines_AQpN"><div class="token-line" style="color:#bfc7d5"><span class="token plain">POST /oauth/dpop-keys</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">X-Client-Key: hvc_...</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">X-Client-Secret: hvs_...</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">Content-Type: application/json</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">{}</span><br></div></code></pre></div></div><p>For public clients, omit <code>X-Client-Secret</code> and include the PKCE challenge in the body:</p><div class="language-text codeBlockContainer_ei_j theme-code-block" style="--prism-color:#bfc7d5;--prism-background-color:#292d3e"><div class="codeBlockContent_p9UO"><pre tabindex="0" class="prism-code language-text codeBlock_NBAy thin-scrollbar" style="color:#bfc7d5;background-color:#292d3e"><code class="codeBlockLines_AQpN"><div class="token-line" style="color:#bfc7d5"><span class="token plain">POST /oauth/dpop-keys</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">X-Client-Key: hvc_...</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">Origin: http://localhost:3000</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">Content-Type: application/json</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">{ "pkce_challenge": "base64url..." }</span><br></div></code></pre></div></div><p>Response:</p><div class="language-json codeBlockContainer_ei_j theme-code-block" style="--prism-color:#bfc7d5;--prism-background-color:#292d3e"><div class="codeBlockContent_p9UO"><pre tabindex="0" class="prism-code language-json codeBlock_NBAy thin-scrollbar" style="color:#bfc7d5;background-color:#292d3e"><code class="codeBlockLines_AQpN"><div class="token-line" style="color:#bfc7d5"><span class="token punctuation" style="color:rgb(199, 146, 234)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain"> </span><span class="token property">"provision_id"</span><span class="token operator" style="color:rgb(137, 221, 255)">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(195, 232, 141)">"hvp_..."</span><span class="token punctuation" style="color:rgb(199, 146, 234)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain"> </span><span class="token property">"dpop_key"</span><span class="token operator" style="color:rgb(137, 221, 255)">:</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(199, 146, 234)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain"> </span><span class="token property">"kty"</span><span class="token operator" style="color:rgb(137, 221, 255)">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(195, 232, 141)">"EC"</span><span class="token punctuation" style="color:rgb(199, 146, 234)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain"> </span><span class="token property">"crv"</span><span class="token operator" style="color:rgb(137, 221, 255)">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(195, 232, 141)">"P-256"</span><span class="token punctuation" style="color:rgb(199, 146, 234)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain"> </span><span class="token property">"x"</span><span class="token operator" style="color:rgb(137, 221, 255)">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(195, 232, 141)">"..."</span><span class="token punctuation" style="color:rgb(199, 146, 234)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain"> </span><span class="token property">"y"</span><span class="token operator" style="color:rgb(137, 221, 255)">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(195, 232, 141)">"..."</span><span class="token punctuation" style="color:rgb(199, 146, 234)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain"> </span><span class="token property">"d"</span><span class="token operator" style="color:rgb(137, 221, 255)">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(195, 232, 141)">"..."</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain"> </span><span class="token punctuation" style="color:rgb(199, 146, 234)">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain"></span><span class="token punctuation" style="color:rgb(199, 146, 234)">}</span><br></div></code></pre></div></div><p>The <code>dpop_key</code> is the private JWK. Use it to generate DPoP proofs during your OAuth flow with the user's PDS.</p><h4 class="anchor anchorTargetStickyNavbar_zGuS" id="2-run-oauth-with-the-users-pds">2. Run OAuth with the user's PDS<a href="#2-run-oauth-with-the-users-pds" class="hash-link" aria-label="Direct link to 2. Run OAuth with the user's PDS" title="Direct link to 2. Run OAuth with the user's PDS" translate="no"></a></h4><p>Use the provisioned DPoP key as your DPoP keypair in a standard atproto OAuth flow with the user's PDS. HappyView is not involved in this step — the app talks directly to the PDS authorization server.</p><h4 class="anchor anchorTargetStickyNavbar_zGuS" id="3-register-the-session">3. Register the session<a href="#3-register-the-session" class="hash-link" aria-label="Direct link to 3. Register the session" title="Direct link to 3. Register the session" translate="no"></a></h4><p>After the OAuth callback, register the token set with HappyView:</p><div class="language-text codeBlockContainer_ei_j theme-code-block" style="--prism-color:#bfc7d5;--prism-background-color:#292d3e"><div class="codeBlockContent_p9UO"><pre tabindex="0" class="prism-code language-text codeBlock_NBAy thin-scrollbar" style="color:#bfc7d5;background-color:#292d3e"><code class="codeBlockLines_AQpN"><div class="token-line" style="color:#bfc7d5"><span class="token plain">POST /oauth/sessions</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">X-Client-Key: hvc_...</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">X-Client-Secret: hvs_...</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">Content-Type: application/json</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">{</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain"> "provision_id": "hvp_...",</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain"> "did": "did:plc:user123",</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain"> "access_token": "...",</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain"> "refresh_token": "...",</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain"> "expires_at": "2026-04-17T00:00:00Z",</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain"> "scopes": "atproto transition:generic",</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain"> "pds_url": "https://bsky.social",</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain"> "issuer": "https://bsky.social"</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">}</span><br></div></code></pre></div></div><p>For public clients, omit <code>X-Client-Secret</code> and include the PKCE verifier in the body:</p><div class="language-json codeBlockContainer_ei_j theme-code-block" style="--prism-color:#bfc7d5;--prism-background-color:#292d3e"><div class="codeBlockContent_p9UO"><pre tabindex="0" class="prism-code language-json codeBlock_NBAy thin-scrollbar" style="color:#bfc7d5;background-color:#292d3e"><code class="codeBlockLines_AQpN"><div class="token-line" style="color:#bfc7d5"><span class="token punctuation" style="color:rgb(199, 146, 234)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain"> </span><span class="token property">"provision_id"</span><span class="token operator" style="color:rgb(137, 221, 255)">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(195, 232, 141)">"hvp_..."</span><span class="token punctuation" style="color:rgb(199, 146, 234)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain"> </span><span class="token property">"pkce_verifier"</span><span class="token operator" style="color:rgb(137, 221, 255)">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(195, 232, 141)">"..."</span><span class="token punctuation" style="color:rgb(199, 146, 234)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain"> </span><span class="token property">"did"</span><span class="token operator" style="color:rgb(137, 221, 255)">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(195, 232, 141)">"did:plc:user123"</span><span class="token punctuation" style="color:rgb(199, 146, 234)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain"> </span><span class="token property">"access_token"</span><span class="token operator" style="color:rgb(137, 221, 255)">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(195, 232, 141)">"..."</span><span class="token punctuation" style="color:rgb(199, 146, 234)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain"> </span><span class="token property">"refresh_token"</span><span class="token operator" style="color:rgb(137, 221, 255)">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(195, 232, 141)">"..."</span><span class="token punctuation" style="color:rgb(199, 146, 234)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain"> </span><span class="token property">"expires_at"</span><span class="token operator" style="color:rgb(137, 221, 255)">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(195, 232, 141)">"2026-04-17T00:00:00Z"</span><span class="token punctuation" style="color:rgb(199, 146, 234)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain"> </span><span class="token property">"scopes"</span><span class="token operator" style="color:rgb(137, 221, 255)">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(195, 232, 141)">"atproto transition:generic"</span><span class="token punctuation" style="color:rgb(199, 146, 234)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain"> </span><span class="token property">"pds_url"</span><span class="token operator" style="color:rgb(137, 221, 255)">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(195, 232, 141)">"https://bsky.social"</span><span class="token punctuation" style="color:rgb(199, 146, 234)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain"> </span><span class="token property">"issuer"</span><span class="token operator" style="color:rgb(137, 221, 255)">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(195, 232, 141)">"https://bsky.social"</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain"></span><span class="token punctuation" style="color:rgb(199, 146, 234)">}</span><br></div></code></pre></div></div><p>Response:</p><div class="language-json codeBlockContainer_ei_j theme-code-block" style="--prism-color:#bfc7d5;--prism-background-color:#292d3e"><div class="codeBlockContent_p9UO"><pre tabindex="0" class="prism-code language-json codeBlock_NBAy thin-scrollbar" style="color:#bfc7d5;background-color:#292d3e"><code class="codeBlockLines_AQpN"><div class="token-line" style="color:#bfc7d5"><span class="token punctuation" style="color:rgb(199, 146, 234)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain"> </span><span class="token property">"session_id"</span><span class="token operator" style="color:rgb(137, 221, 255)">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(195, 232, 141)">"uuid"</span><span class="token punctuation" style="color:rgb(199, 146, 234)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain"> </span><span class="token property">"did"</span><span class="token operator" style="color:rgb(137, 221, 255)">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(195, 232, 141)">"did:plc:user123"</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain"></span><span class="token punctuation" style="color:rgb(199, 146, 234)">}</span><br></div></code></pre></div></div><h4 class="anchor anchorTargetStickyNavbar_zGuS" id="4-make-xrpc-requests">4. Make XRPC requests<a href="#4-make-xrpc-requests" class="hash-link" aria-label="Direct link to 4. Make XRPC requests" title="Direct link to 4. Make XRPC requests" translate="no"></a></h4><p>With a registered session, send XRPC requests using DPoP auth:</p><div class="language-sh codeBlockContainer_ei_j theme-code-block" style="--prism-color:#bfc7d5;--prism-background-color:#292d3e"><div class="codeBlockContent_p9UO"><pre tabindex="0" class="prism-code language-sh codeBlock_NBAy thin-scrollbar" style="color:#bfc7d5;background-color:#292d3e"><code class="codeBlockLines_AQpN"><div class="token-line" style="color:#bfc7d5"><span class="token plain">curl -X POST 'https://happyview.example.com/xrpc/com.example.feed.createPost' \</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain"> -H 'X-Client-Key: hvc_...' \</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain"> -H 'Authorization: DPoP <access_token>' \</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain"> -H 'DPoP: <proof_jwt>' \</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain"> -H 'Content-Type: application/json' \</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain"> -d '{"text": "Hello world"}'</span><br></div></code></pre></div></div><p>HappyView validates the DPoP proof, looks up the stored session, and proxies the write to the user's PDS using the provisioned DPoP key to generate a fresh proof.</p><h4 class="anchor anchorTargetStickyNavbar_zGuS" id="5-logout">5. Logout<a href="#5-logout" class="hash-link" aria-label="Direct link to 5. Logout" title="Direct link to 5. Logout" translate="no"></a></h4><p>Confidential clients authenticate with <code>X-Client-Key</code> + <code>X-Client-Secret</code>:</p><div class="language-text codeBlockContainer_ei_j theme-code-block" style="--prism-color:#bfc7d5;--prism-background-color:#292d3e"><div class="codeBlockContent_p9UO"><pre tabindex="0" class="prism-code language-text codeBlock_NBAy thin-scrollbar" style="color:#bfc7d5;background-color:#292d3e"><code class="codeBlockLines_AQpN"><div class="token-line" style="color:#bfc7d5"><span class="token plain">DELETE /oauth/sessions/did:plc:user123</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">X-Client-Key: hvc_...</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">X-Client-Secret: hvs_...</span><br></div></code></pre></div></div><p>Public clients must provide a valid DPoP proof to prove they hold the key:</p><div class="language-text codeBlockContainer_ei_j theme-code-block" style="--prism-color:#bfc7d5;--prism-background-color:#292d3e"><div class="codeBlockContent_p9UO"><pre tabindex="0" class="prism-code language-text codeBlock_NBAy thin-scrollbar" style="color:#bfc7d5;background-color:#292d3e"><code class="codeBlockLines_AQpN"><div class="token-line" style="color:#bfc7d5"><span class="token plain">DELETE /oauth/sessions/did:plc:user123</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">X-Client-Key: hvc_...</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">Authorization: DPoP <access_token></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">DPoP: <proof_jwt></span><br></div></code></pre></div></div><p>This deletes the stored session and the associated DPoP key.</p><h3 class="anchor anchorTargetStickyNavbar_zGuS" id="security-notes">Security notes<a href="#security-notes" class="hash-link" aria-label="Direct link to Security notes" title="Direct link to Security notes" translate="no"></a></h3><ul><li class="">Private keys and tokens are encrypted at rest with AES-256-GCM using <code>TOKEN_ENCRYPTION_KEY</code>.</li><li class="">DPoP proofs are validated for method, URL, timestamp (5-minute window), access token binding, and JWK thumbprint.</li><li class="">Scopes requested must include <code>atproto</code> and must be a subset of the API client's registered scopes.</li></ul><h2 class="anchor anchorTargetStickyNavbar_zGuS" id="next-steps">Next steps<a href="#next-steps" class="hash-link" aria-label="Direct link to Next steps" title="Direct link to Next steps" translate="no"></a></h2><ul><li class=""><a class="" href="/sdk/overview">JavaScript SDK</a> — authenticate and make XRPC calls from JavaScript</li><li class=""><a class="" href="/guides/permissions">Permissions</a> — full list of permissions and what each one grants</li><li class=""><a class="" href="/guides/api-keys">API Keys</a> — create scoped admin API keys for automation</li><li class=""><a class="" href="/reference/admin/api-clients">Admin API — API Clients</a> — register API clients and configure rate limits</li></ul></div></article><nav class="docusaurus-mt-lg pagination-nav" aria-label="Docs pages"><a class="pagination-nav__link pagination-nav__link--prev" href="/getting-started/configuration"><div class="pagination-nav__sublabel">Previous</div><div class="pagination-nav__label">Configuration</div></a><a class="pagination-nav__link pagination-nav__link--next" href="/getting-started/dashboard"><div class="pagination-nav__sublabel">Next</div><div class="pagination-nav__label">Dashboard</div></a></nav></div></div><div class="col col--3"><div class="tableOfContents_X45M thin-scrollbar theme-doc-toc-desktop"><ul class="table-of-contents table-of-contents__left-border"><li><a href="#which-endpoints-require-what" class="table-of-contents__link toc-highlight">Which endpoints require what?</a></li><li><a href="#xrpc-api-client-identification" class="table-of-contents__link toc-highlight">XRPC: API client identification</a><ul><li><a href="#calling-a-query" class="table-of-contents__link toc-highlight">Calling a query</a></li><li><a href="#authenticating-users-for-procedures" class="table-of-contents__link toc-highlight">Authenticating users for procedures</a></li></ul></li><li><a href="#admin-api-user-authentication" class="table-of-contents__link toc-highlight">Admin API: user authentication</a><ul><li><a href="#admin-api-key" class="table-of-contents__link toc-highlight">Admin API key</a></li><li><a href="#service-auth-jwt" class="table-of-contents__link toc-highlight">Service auth JWT</a></li><li><a href="#admin-access-and-the-first-user" class="table-of-contents__link toc-highlight">Admin access and the first user</a></li></ul></li><li><a href="#proxying-procedures-to-the-users-pds" class="table-of-contents__link toc-highlight">Proxying procedures to the user's PDS</a></li><li><a href="#dpop-key-provisioning-for-third-party-apps" class="table-of-contents__link toc-highlight">DPoP key provisioning for third-party apps</a><ul><li><a href="#flow-overview" class="table-of-contents__link toc-highlight">Flow overview</a></li><li><a href="#api-clients-confidential-vs-public" class="table-of-contents__link toc-highlight">API clients: confidential vs public</a></li><li><a href="#the-full-flow" class="table-of-contents__link toc-highlight">The full flow</a></li><li><a href="#security-notes" class="table-of-contents__link toc-highlight">Security notes</a></li></ul></li><li><a href="#next-steps" class="table-of-contents__link toc-highlight">Next steps</a></li></ul></div></div></div></div></main></div></div></div><footer class="theme-layout-footer footer"><div class="container container-fluid"><div class="footer__bottom text--center"><div class="footer__copyright">Copyright © 2026 <a href="https://birb.house">Birbhouse Games</a>.</div></div></div></footer></div></body></html>