page.tsx637 lines · main
1import { PmTabs } from '../../components/pm-tabs';
2import { DocsShell } from '../../components/shell';
3import { pmInstall } from '../../lib/pm';
4
5export const metadata = { title: 'auth' };
6
7const INSTALL = pmInstall('@briven/auth');
8
9export default function AuthPage() {
10 return (
11 <DocsShell>
12 <h1 className="font-mono text-2xl tracking-tight">auth</h1>
13 <p className="mt-2 font-mono text-sm text-[var(--color-text-muted)]">
14 <code>@briven/auth</code> is a drop-in sign-in client for your briven project. one
15 package gives you email + password, magic links, email OTP, passkeys (WebAuthn), and
16 OAuth social login — plus React hooks and a prebuilt <code>&lt;BrivenSignIn /&gt;</code>{' '}
17 panel. the SDK
18 talks to the hosted briven auth service; you never run an auth server yourself.
19 </p>
20
21 <nav className="mt-6 flex flex-wrap gap-2 font-mono text-xs">
22 {[
23 ['setup (10 min)', '#setup'],
24 ['install', '#install'],
25 ['get your keys', '#keys'],
26 ['create the client', '#client'],
27 ['react bindings', '#react'],
28 ['sign-in flows', '#flows'],
29 ['two-factor + backup codes', '#two-factor'],
30 ['testing tokens', '#testing-tokens'],
31 ['email sender domain', '#sender-domain'],
32 ['verify users with tokens', '#verify-tokens'],
33 ['security', '#security'],
34 ['agents', '#agents'],
35 ].map(([label, href]) => (
36 <a
37 key={href}
38 href={href}
39 className="rounded-md border border-[var(--color-border)] px-3 py-1 text-[var(--color-text-muted)] hover:text-[var(--color-text)]"
40 >
41 {label}
42 </a>
43 ))}
44 </nav>
45
46 {/* setup --------------------------------------------------------- */}
47 <section id="setup" className="mt-10 scroll-mt-20 border-t border-[var(--color-border-subtle)] pt-8">
48 <h2 className="font-mono text-lg tracking-tight">setup in 10 minutes</h2>
49 <p className="mt-2 font-mono text-sm text-[var(--color-text-muted)]">
50 the shortest path from zero to a working sign-in. for a browser-only
51 sign-off after this, use the plain-language checklist in the briven
52 repo: <code>AUTH-GO-LIVE-CHECKLIST.md</code>.
53 </p>
54 <ol className="mt-3 list-inside list-decimal font-mono text-sm text-[var(--color-text-muted)] space-y-2">
55 <li>
56 open your project at{' '}
57 <a className="underline" href="https://briven.tech">
58 briven.tech
59 </a>{' '}
60 → <em>Auth</em> → enable auth if it is not already on.
61 </li>
62 <li>
63 <em>Auth → API keys</em> → create a key → copy{' '}
64 <code>pk_briven_auth_…</code> (browser-safe). never put a{' '}
65 <code>brk_…</code> key in the browser.
66 </li>
67 <li>
68 in your app folder, wire the project first:
69 <Snippet>{`# NEW project: briven setup my-app
70# EXISTING: briven connect p_…
71briven auth scaffold
72pnpm add @briven/auth`}</Snippet>
73 that writes <code>middleware.ts</code>, <code>lib/auth.ts</code>, and a
74 seeded <code>.env.local</code> if missing. never use{' '}
75 <code>setup --project</code> to attach — that flag is gone; use{' '}
76 <code>briven connect</code>.
77 </li>
78 <li>
79 paste the public key into <code>NEXT_PUBLIC_BRIVEN_AUTH_KEY</code> (and
80 the same value into <code>BRIVEN_AUTH_PUBLIC_KEY</code> for middleware).
81 </li>
82 <li>
83 send users to the hosted page or drop in the panel:
84 <Snippet>{`// hosted (fastest pilot)
85window.location.assign(auth.hostedPageURL('sign-in', '/dashboard'));
86
87// or embedded React panel
88import { BrivenSignIn } from '@briven/auth/react';
89// <BrivenSignIn redirectTo="/dashboard" />`}</Snippet>
90 </li>
91 <li>
92 prove it in a real browser: sign up, sign out, sign in, refresh still
93 signed in. optional: wrong-password rejection + rate limit after many
94 failures (production needs redis — <code>/ready</code> must show{' '}
95 <code>redis: ok</code>).
96 </li>
97 </ol>
98 <p className="mt-3 font-mono text-xs text-[var(--color-text-subtle)]">
99 starter copy: <code>examples/auth-pilot/</code> in the briven monorepo.
100 </p>
101 </section>
102
103 {/* install ------------------------------------------------------- */}
104 <section id="install" className="mt-10 scroll-mt-20 border-t border-[var(--color-border-subtle)] pt-8">
105 <h2 className="font-mono text-lg tracking-tight">install</h2>
106 <p className="mt-2 font-mono text-sm text-[var(--color-text-muted)]">
107 published to public npm — see{' '}
108 <a
109 className="underline"
110 href="https://www.npmjs.com/package/@briven/auth"
111 >
112 npm
113 </a>{' '}
114 for the current version.
115 </p>
116 <div className="mt-3">
117 <PmTabs commands={INSTALL} />
118 </div>
119 <p className="mt-3 font-mono text-xs text-[var(--color-text-subtle)]">
120 subpaths: <code>@briven/auth</code> (the vanilla client, zero React deps),{' '}
121 <code>@briven/auth/react</code> (hooks + the <code>&lt;BrivenSignIn /&gt;</code>{' '}
122 component), and <code>@briven/auth/server</code> (Next.js App Router helpers).
123 </p>
124 </section>
125
126 {/* keys ---------------------------------------------------------- */}
127 <section id="keys" className="mt-10 scroll-mt-20 border-t border-[var(--color-border-subtle)] pt-8">
128 <h2 className="font-mono text-lg tracking-tight">get your keys</h2>
129 <p className="mt-2 font-mono text-sm text-[var(--color-text-muted)]">
130 you need two things from the dashboard: your <strong>project id</strong> (looks like{' '}
131 <code>p_01HZ…</code>) and a <strong>public auth key</strong> (looks like{' '}
132 <code>pk_briven_auth_…</code>).
133 </p>
134 <ol className="mt-3 list-inside list-decimal font-mono text-sm text-[var(--color-text-muted)] space-y-1">
135 <li>open your project at <a className="underline" href="https://briven.tech">briven.tech</a>.</li>
136 <li>go to <em>Auth → API keys</em>.</li>
137 <li>click <em>create key</em>, give it a name, set the scope to <code>read-write</code>.</li>
138 <li>copy the <code>pk_briven_auth_…</code> value — it is shown once.</li>
139 </ol>
140
141 <section className="mt-5 rounded-md border border-[var(--color-border-subtle)] bg-[var(--color-surface)] p-4 font-mono text-xs text-[var(--color-text-muted)]">
142 <strong className="text-[var(--color-text)]">which key is which:</strong> the{' '}
143 <code>pk_briven_auth_</code> key is <em>browser-safe</em>. it identifies your tenant
144 and unlocks the end-user sign-in surface only — it can&apos;t read or write your data.
145 ship it to the browser freely. your server <code>brk_*</code> keys are{' '}
146 <strong>not</strong> browser-safe; never put one in client code.
147 </section>
148 </section>
149
150 {/* client -------------------------------------------------------- */}
151 <section id="client" className="mt-10 scroll-mt-20 border-t border-[var(--color-border-subtle)] pt-8">
152 <h2 className="font-mono text-lg tracking-tight">create the client</h2>
153 <p className="mt-2 font-mono text-sm text-[var(--color-text-muted)]">
154 <code>createBrivenAuth</code> returns a stateless client. all auth state lives in the
155 browser cookie set by the service on a successful sign-in, so re-creating the client
156 across renders is safe.
157 </p>
158 <Snippet>{`// lib/auth.ts
159import { createBrivenAuth } from '@briven/auth';
160
161export const auth = createBrivenAuth({
162 projectId: 'p_01HZ...',
163 publicKey: process.env.NEXT_PUBLIC_BRIVEN_AUTH_KEY!, // 'pk_briven_auth_...'
164});`}</Snippet>
165 <p className="mt-3 font-mono text-xs text-[var(--color-text-subtle)]">
166 <span className="text-[var(--color-text)]">options: </span>
167 <code>projectId</code> and <code>publicKey</code> are required.{' '}
168 <code>apiOrigin</code> defaults to <code>https://api.briven.tech</code> (override it
169 only for self-hosted or local dev). <code>authUrl</code> defaults to{' '}
170 <code>https://&lt;projectId&gt;.auth.briven.tech</code> and backs the OAuth redirect.
171 </p>
172 <p className="mt-3 font-mono text-xs text-[var(--color-text-subtle)]">
173 the client exposes <code>signIn</code>, <code>signUp.email</code>,{' '}
174 <code>signOut()</code>, <code>getSession()</code>, and <code>getUser()</code>.
175 </p>
176 </section>
177
178 {/* react --------------------------------------------------------- */}
179 <section id="react" className="mt-10 scroll-mt-20 border-t border-[var(--color-border-subtle)] pt-8">
180 <h2 className="font-mono text-lg tracking-tight">react bindings</h2>
181 <p className="mt-2 font-mono text-sm text-[var(--color-text-muted)]">
182 import from <code>@briven/auth/react</code>. wrap your app in the provider, then read
183 state with the hooks or drop in the prebuilt panel. works in any React 19 environment
184 — no hard Next.js dependency.
185 </p>
186
187 <h3 className="mt-5 font-mono text-sm">provider</h3>
188 <Snippet>{`// app/providers.tsx
189'use client';
190import { BrivenAuthProvider } from '@briven/auth/react';
191import { auth } from '../lib/auth';
192
193export function Providers({ children }: { children: React.ReactNode }) {
194 return <BrivenAuthProvider value={auth}>{children}</BrivenAuthProvider>;
195}`}</Snippet>
196
197 <h3 className="mt-5 font-mono text-sm">hooks</h3>
198 <p className="mt-1 font-mono text-xs text-[var(--color-text-muted)]">
199 <code>useSession()</code> returns <code>{`{ session, isLoading, refresh }`}</code>;{' '}
200 <code>useUser()</code> returns <code>{`{ user, isLoading, refresh }`}</code>. both
201 fetch once on mount — call <code>refresh()</code> after a sign-in or sign-out to
202 re-read.
203 </p>
204 <Snippet>{`'use client';
205import { useSession, useUser } from '@briven/auth/react';
206
207export function AccountBadge() {
208 const { session, isLoading } = useSession();
209 const { user } = useUser();
210
211 if (isLoading) return <span>…</span>;
212 if (!session?.authenticated) return <a href="/sign-in">sign in</a>;
213 return <span>signed in</span>; // don't render user.email into the UI — see security
214}`}</Snippet>
215
216 <h3 className="mt-5 font-mono text-sm">prebuilt panel</h3>
217 <p className="mt-1 font-mono text-xs text-[var(--color-text-muted)]">
218 <code>&lt;BrivenSignIn /&gt;</code> renders email + password, magic link, and your
219 chosen OAuth buttons in one panel. it carries no icon library and no css framework —
220 your app&apos;s styles apply via <code>className</code> and the element classes.
221 </p>
222 <Snippet>{`'use client';
223import { BrivenSignIn } from '@briven/auth/react';
224
225export function SignIn() {
226 return (
227 <BrivenSignIn
228 providers={['google', 'github', 'konnos']}
229 showEmailPassword
230 showMagicLink
231 redirectTo="/dashboard"
232 onSuccess={({ userId }) => console.log('signed in', userId)}
233 className="my-signin"
234 />
235 );
236}`}</Snippet>
237 <p className="mt-3 font-mono text-xs text-[var(--color-text-subtle)]">
238 <span className="text-[var(--color-text)]">props: </span>
239 <code>providers</code> (OAuth buttons to show — empty array hides the section),{' '}
240 <code>showEmailPassword</code> (default true), <code>showMagicLink</code> (default
241 true), <code>redirectTo</code>, <code>onSuccess</code>, and <code>className</code>.
242 </p>
243 </section>
244
245 {/* flows --------------------------------------------------------- */}
246 <section id="flows" className="mt-10 scroll-mt-20 border-t border-[var(--color-border-subtle)] pt-8">
247 <h2 className="font-mono text-lg tracking-tight">sign-in flows</h2>
248 <p className="mt-2 font-mono text-sm text-[var(--color-text-muted)]">
249 if you want your own UI, call the client methods directly. every async method returns
250 a tagged result — <code>{`{ ok: true, ... }`}</code> on success or{' '}
251 <code>{`{ ok: false, code, message }`}</code> on failure — so you never throw on a bad
252 password.
253 </p>
254
255 <h3 className="mt-5 font-mono text-sm">email + password</h3>
256 <Snippet>{`// sign up, then sign in — both return { ok, userId, sessionExpiresAt }
257await auth.signUp.email({ email, password, name: 'Jane' });
258
259const result = await auth.signIn.email({ email, password });
260if (result.ok) {
261 // result.userId, result.sessionExpiresAt
262} else {
263 // result.code: 'invalid_credentials' | 'unverified_email' | 'rate_limited' | ...
264 console.error(result.message);
265}`}</Snippet>
266
267 <h3 className="mt-5 font-mono text-sm">magic link</h3>
268 <p className="mt-1 font-mono text-xs text-[var(--color-text-muted)]">
269 a passwordless flow of its own — <strong>not</strong> an OAuth provider. it emails the
270 user a one-click sign-in link.
271 </p>
272 <Snippet>{`const res = await auth.signIn.magicLink({
273 email,
274 redirectTo: '/dashboard', // where the link lands after verify
275});
276// res.ok === true means the email was sent`}</Snippet>
277
278 <h3 className="mt-5 font-mono text-sm">email OTP</h3>
279 <Snippet>{`// 1. send the code
280await auth.signIn.otpRequest({ email });
281
282// 2. verify the 6-digit code the user typed
283const result = await auth.signIn.otpVerify({ email, otp: '123456' });
284if (result.ok) { /* result.userId */ }`}</Snippet>
285
286 <h3 className="mt-5 font-mono text-sm">OAuth social</h3>
287 <p className="mt-1 font-mono text-xs text-[var(--color-text-muted)]">
288 <code>social()</code> is synchronous — it builds the start URL and hands it back. you
289 redirect the browser to it. provider is one of{' '}
290 <code>&apos;google&apos;</code>, <code>&apos;github&apos;</code>,{' '}
291 <code>&apos;discord&apos;</code>, <code>&apos;microsoft&apos;</code>, or{' '}
292 <code>&apos;konnos&apos;</code>.
293 </p>
294 <Snippet>{`const { redirectUrl } = auth.signIn.social({
295 provider: 'google',
296 redirectTo: '/dashboard',
297});
298window.location.assign(redirectUrl);`}</Snippet>
299
300 <h3 className="mt-5 font-mono text-sm">passkeys (WebAuthn)</h3>
301 <p className="mt-1 font-mono text-xs text-[var(--color-text-muted)]">
302 passwordless sign-in backed by the device&apos;s platform authenticator (Touch ID, Face
303 ID, Windows Hello, a hardware key). two ceremonies, both wrapped for you so you never
304 touch the raw <code>navigator.credentials</code> calls:
305 </p>
306 <Snippet>{`// REGISTER — adds a passkey to the *currently signed-in* user.
307// call this from an account-settings screen, after a normal sign-in.
308const reg = await auth.passkey.register();
309if (reg.ok) { /* passkey saved on this device */ }
310
311// SIGN IN — no password, no email field needed.
312const result = await auth.passkey.signIn();
313if (result.ok) {
314 // result.userId, result.sessionExpiresAt
315} else {
316 // result.code: 'not_enabled' | 'cancelled' | 'unsupported' | ...
317 console.error(result.message);
318}`}</Snippet>
319 <p className="mt-2 font-mono text-xs text-[var(--color-text-subtle)]">
320 passkeys are <strong>per-tenant</strong>: enable under <em>Auth → providers</em>.{' '}
321 <code>auth.passkey.register()</code> needs an active session first (magic link / OTP /
322 password), then Face ID / Touch ID runs in the browser. platform routes:{' '}
323 <code>GET /v1/auth-tenant/passkey/generate-authenticate-options</code> (sign-in) and{' '}
324 <code>GET …/generate-register-options</code> (enrol) — <strong>not</strong> POST on those
325 paths (POST returns 404 by design). verify is{' '}
326 <code>POST …/verify-authentication</code> / <code>POST …/verify-registration</code>. rpID
327 is the parent domain (e.g. <code>briven.tech</code> for apps on{' '}
328 <code>*.briven.tech</code>). WebAuthn needs https or localhost; add every app origin under{' '}
329 <em>Auth → Allowed Domains</em>.
330 </p>
331
332 <section className="mt-5 rounded-md border border-[var(--color-border-subtle)] bg-[var(--color-surface)] p-4 font-mono text-xs text-[var(--color-text-muted)]">
333 <strong className="text-[var(--color-text)]">before these work in production:</strong>{' '}
334 social login (google / github / discord / microsoft / konnos) needs the project owner
335 to set that provider&apos;s client id <em>and</em> secret under <em>Auth → providers</em>
336 in the dashboard — until both are set, the provider stays disabled. magic-link and
337 email-OTP emails send out of the box from the briven fallback sender — to brand them
338 as your own domain, see{' '}
339 <a className="underline" href="#sender-domain">email sender domain</a>.
340 </section>
341 </section>
342
343 {/* two-factor ------------------------------------------------------ */}
344 <section
345 id="two-factor"
346 className="mt-10 scroll-mt-20 border-t border-[var(--color-border-subtle)] pt-8"
347 >
348 <h2 className="font-mono text-lg tracking-tight">two-factor + backup codes</h2>
349 <p className="mt-2 font-mono text-sm text-[var(--color-text-muted)]">
350 when the project has two-factor enabled, password sign-in may return{' '}
351 <code>twoFactorRequired</code>. complete the challenge with a TOTP app code or a
352 single-use <strong>backup recovery code</strong> (for lost phones).
353 </p>
354 <Snippet>{`// after password sign-in returns twoFactorRequired:
355const totp = await auth.twoFactor.verify('123456');
356// or lost phone:
357const backup = await auth.twoFactor.verifyBackupCode('XXXX-XXXX');
358
359// enroll (show codes once after verify):
360await auth.twoFactor.enable(password);
361await auth.twoFactor.verify(appCode);
362const { codes } = await auth.twoFactor.generateBackupCodes(password);
363// save codes offline — each works once`}</Snippet>
364 <p className="mt-3 font-mono text-xs text-[var(--color-text-subtle)]">
365 hosted pages: after password, users land on{' '}
366 <code>/auth/&lt;projectId&gt;/two-factor</code> with a toggle for backup codes.
367 react: <code>TwoFactorSetup</code> + <code>TwoFactorChallenge</code> from{' '}
368 <code>@briven/auth/react</code>.
369 </p>
370 </section>
371
372 {/* testing tokens ------------------------------------------------- */}
373 <section
374 id="testing-tokens"
375 className="mt-10 scroll-mt-20 border-t border-[var(--color-border-subtle)] pt-8"
376 >
377 <h2 className="font-mono text-lg tracking-tight">testing tokens (e2e)</h2>
378 <p className="mt-2 font-mono text-sm text-[var(--color-text-muted)]">
379 for automated tests, mint a <strong>testing token</strong> in the dashboard (or{' '}
380 <code>POST /v1/projects/:id/auth/test-tokens</code> as an admin). it bypasses bot
381 checks, rate limits, and MFA for a short window and is returned <em>once</em> as{' '}
382 <code>briven_test_…</code>.
383 </p>
384 <Snippet>{`// CI / e2e (never commit the raw token)
385const session = await auth.signIn.testToken(process.env.BRIVEN_AUTH_TEST_TOKEN!);
386// → { ok: true, expiresAt } and a real session cookie`}</Snippet>
387 <p className="mt-3 font-mono text-xs text-[var(--color-text-subtle)]">
388 revoke used tokens in the dashboard. production apps for real humans should not rely
389 on testing tokens.
390 </p>
391 </section>
392
393 {/* sender domain -------------------------------------------------- */}
394 <section
395 id="sender-domain"
396 className="mt-10 scroll-mt-20 border-t border-[var(--color-border-subtle)] pt-8"
397 >
398 <h2 className="font-mono text-lg tracking-tight">email sender domain</h2>
399 <p className="mt-2 font-mono text-sm text-[var(--color-text-muted)]">
400 every email briven auth sends for you — magic links, OTP codes, email verification,
401 password resets — has a From: address. by default that is{' '}
402 <code>noreply@briven.tech</code>, which works out of the box with zero setup. when you
403 want those emails branded as your own product (say{' '}
404 <code>noreply@yourapp.com</code>), you set a <strong>sender domain</strong>.
405 </p>
406
407 <h3 className="mt-5 font-mono text-sm">how to set it</h3>
408 <ol className="mt-2 list-inside list-decimal font-mono text-sm text-[var(--color-text-muted)] space-y-1">
409 <li>open your project at <a className="underline" href="https://briven.tech">briven.tech</a>.</li>
410 <li>go to <em>Auth → branding</em>.</li>
411 <li>
412 fill in <em>sender name</em> (the display name, e.g. your product name) and{' '}
413 <em>sender domain</em> — your <strong>root domain</strong>, e.g.{' '}
414 <code>yourapp.com</code>. do <strong>not</strong> enter a subdomain like{' '}
415 <code>auth.yourapp.com</code> unless you really send from that subdomain.
416 </li>
417 <li>save. that&apos;s the whole dashboard side.</li>
418 </ol>
419
420 <h3 className="mt-5 font-mono text-sm">verify the domain (2–3 DNS records)</h3>
421 <p className="mt-1 font-mono text-xs text-[var(--color-text-muted)]">
422 email worldwide runs on one rule: a domain must publicly declare who is allowed to
423 send mail in its name. that declaration lives in your domain&apos;s DNS as SPF and
424 DKIM records. without them, providers like Gmail and Outlook treat mail from your
425 domain as forged — so no platform (briven included) can skip this step. briven hands
426 you the exact records to add at your domain provider; a guided setup wizard is landing
427 in the branding panel.
428 </p>
429
430 <h3 className="mt-5 font-mono text-sm">what happens before you verify</h3>
431 <p className="mt-1 font-mono text-xs text-[var(--color-text-muted)]">
432 nothing breaks — this is the important part. until your domain is verified, briven
433 automatically keeps sending from the <code>noreply@briven.tech</code> fallback (with
434 your sender name), so your users&apos; sign-in emails always arrive. the moment the
435 domain verifies, sends switch to <code>noreply@yourdomain</code> on their own. you
436 never need to time the switch or fear a broken login flow while DNS propagates.
437 </p>
438
439 <section className="mt-5 rounded-md border border-[var(--color-border-subtle)] bg-[var(--color-surface)] p-4 font-mono text-xs text-[var(--color-text-muted)]">
440 <strong className="text-[var(--color-text)]">troubleshooting:</strong> sign-in emails
441 arriving from <code>noreply@briven.tech</code> even though you filled in a sender
442 domain? that means the domain isn&apos;t verified yet — finish adding the DNS records
443 and allow up to an hour for DNS to propagate. emails not arriving at all? check spam
444 first, then <em>Auth → usage</em> for delivery status.
445 </section>
446 </section>
447
448 {/* verify tokens -------------------------------------------------- */}
449 <section
450 id="verify-tokens"
451 className="mt-10 scroll-mt-20 border-t border-[var(--color-border-subtle)] pt-8"
452 >
453 <h2 className="font-mono text-lg tracking-tight">verify users with tokens (JWT + JWKS)</h2>
454 <p className="mt-2 font-mono text-sm text-[var(--color-text-muted)]">
455 <code>getSession()</code> asks the briven auth service every time. when your setup has
456 its own backend — or several apps sharing one sign-in — you often just need a fast
457 local answer to &quot;is this user signed in?&quot; without a network round-trip per
458 request. for that, briven auth issues <strong>verifiable tokens</strong>: short-lived
459 signed JWTs your own code can check against the project&apos;s public keys.
460 </p>
461
462 <h3 className="mt-5 font-mono text-sm">the two endpoints</h3>
463 <ul className="mt-2 list-inside list-disc font-mono text-sm text-[var(--color-text-muted)] space-y-2">
464 <li>
465 <code>GET /v1/auth-tenant/token</code> — requires a signed-in session (the browser
466 cookie). returns <code>{`{ token: "<JWT>" }`}</code>, a short-lived signed JWT for
467 the current user.
468 </li>
469 <li>
470 <code>GET /v1/auth-tenant/jwks</code> — public, no auth. returns the project&apos;s
471 JSON Web Key Set, so your code can verify JWTs locally without calling briven.
472 </li>
473 </ul>
474 <p className="mt-3 font-mono text-xs text-[var(--color-text-subtle)]">
475 both live under <code>https://api.briven.tech</code> and carry your{' '}
476 <code>x-briven-project-id</code> header like every auth request.
477 </p>
478
479 <h3 className="mt-5 font-mono text-sm">verify a token</h3>
480 <p className="mt-1 font-mono text-xs text-[var(--color-text-muted)]">
481 any standard JWT library that understands remote JWKS works — shown here with{' '}
482 <code>jose</code>:
483 </p>
484 <Snippet>{`// in the signed-in browser: mint a token, hand it to your backend
485const res = await fetch('https://api.briven.tech/v1/auth-tenant/token', {
486 headers: {
487 'x-briven-project-id': 'p_01HZ...',
488 authorization: 'Bearer pk_briven_auth_...', // the browser-safe key
489 },
490 credentials: 'include', // sends the session cookie
491});
492const { token } = await res.json();
493
494// on YOUR server: verify locally — no call to briven per request
495import { createRemoteJWKSet, jwtVerify } from 'jose';
496
497const jwks = createRemoteJWKSet(
498 new URL('https://api.briven.tech/v1/auth-tenant/jwks'),
499 { headers: { 'x-briven-project-id': 'p_01HZ...' } },
500);
501
502const { payload } = await jwtVerify(token, jwks);
503// payload.sub is the signed-in user's id`}</Snippet>
504
505 <section className="mt-5 rounded-md border border-[var(--color-border-subtle)] bg-[var(--color-surface)] p-4 font-mono text-xs text-[var(--color-text-muted)]">
506 <strong className="text-[var(--color-text)]">keys and rotation:</strong> signing keys
507 are <em>per-project</em> — a token from one project never verifies in another. key
508 rotation is handled through the JWKS endpoint: when briven rotates a key, the new one
509 appears in the key set, so refetch the JWKS whenever verification hits an unknown key
510 id (libraries like <code>jose</code> do this for you). tokens are short-lived by
511 design — refetch <code>/token</code> on expiry rather than storing one long-term.
512 </section>
513 </section>
514
515 {/* security ------------------------------------------------------ */}
516 <section id="security" className="mt-10 scroll-mt-20 border-t border-[var(--color-border-subtle)] pt-8">
517 <h2 className="font-mono text-lg tracking-tight">security</h2>
518 <ul className="mt-3 list-inside list-disc font-mono text-sm text-[var(--color-text-muted)] space-y-2">
519 <li>
520 the <code>pk_briven_auth_</code> key is browser-safe: it identifies the tenant and
521 unlocks the end-user sign-in surface only. your server <code>brk_*</code> keys are
522 not — keep them server-side. <code>read</code> scope keys cannot POST mutating
523 auth-tenant routes.
524 </li>
525 <li>
526 never log or display an end-user&apos;s email or IP. the <code>User</code> object
527 carries <code>email</code> for the account holder — don&apos;t echo it into a list
528 view, a log line, or an analytics event.
529 </li>
530 <li>
531 sessions live in an http cookie the service sets on sign-in; the SDK sends requests
532 with <code>credentials: &apos;include&apos;</code> so the browser stores and returns
533 it. you don&apos;t handle tokens yourself.
534 </li>
535 <li>
536 password policy (length / character rules / max age / force reset) and rate limits
537 are per-project. production should show <code>redis: ok</code> on{' '}
538 <code>/ready</code> so limits share across servers.
539 </li>
540 <li>
541 new device emails use a hashed browser fingerprint (no raw IPs). SSO redirects after
542 SAML/OIDC only allow listed app origins.
543 </li>
544 </ul>
545
546 <section className="mt-5 rounded-md border border-[var(--color-border-subtle)] bg-[var(--color-surface)] p-4 font-mono text-xs text-[var(--color-text-muted)]">
547 <strong className="text-[var(--color-text)]">wire protocol (you don&apos;t write this):</strong>{' '}
548 every request carries <code>x-briven-project-id: &lt;projectId&gt;</code> and{' '}
549 <code>authorization: Bearer &lt;publicKey&gt;</code> to{' '}
550 <code>https://api.briven.tech/v1/auth-tenant/*</code>. the service resolves your
551 tenant from the header and routes the request. the SDK adds these headers for you.
552 </section>
553 </section>
554
555 {/* agents -------------------------------------------------------- */}
556 <section id="agents" className="mt-10 scroll-mt-20 border-t border-[var(--color-border-subtle)] pt-8">
557 <h2 className="font-mono text-lg tracking-tight">for AI agents</h2>
558 <p className="mt-2 font-mono text-sm text-[var(--color-text-muted)]">
559 when an agent wires auth, it should follow the <code>briven-auth</code>{' '}
560 skill (not invent OAuth). trusted order: link project →{' '}
561 <code>briven auth scaffold</code> → install <code>@briven/auth</code> →
562 human pastes <code>pk_briven_auth_…</code> → hosted page or{' '}
563 <code>BrivenSignIn</code> → human runs the go-live checklist.
564 </p>
565 <ul className="mt-3 list-inside list-disc font-mono text-sm text-[var(--color-text-muted)] space-y-2">
566 <li>
567 MCP helpers are <strong>read-only</strong> today:{' '}
568 <code>auth_config_get</code>, <code>sender_domain_status</code>,{' '}
569 <code>auth_docs_ask</code>. they explain; they do not mint keys or
570 write provider secrets.
571 </li>
572 <li>
573 never put secrets in git or chat. never use <code>brk_</code> in{' '}
574 <code>NEXT_PUBLIC_*</code>.
575 </li>
576 <li>
577 sessions are cookies — prefer <code>getSession()</code> /{' '}
578 <code>useSession()</code>, not home-rolled localStorage tokens.
579 </li>
580 <li>
581 for e2e, use testing tokens (<code>auth.signIn.testToken</code>), not real MFA
582 bypass hacks. for lost-phone recovery, implement backup codes (see{' '}
583 <a className="underline" href="#two-factor">
584 two-factor
585 </a>
586 ).
587 </li>
588 <li>
589 framework hooks: react <code>useUserMetadata</code> / <code>useUserEmails</code>;
590 vue + svelte have matching helpers. pilot: <code>examples/auth-pilot/</code>.
591 </li>
592 </ul>
593 </section>
594
595 <h2 className="mt-12 font-mono text-lg">what to read next</h2>
596 <ul className="mt-3 flex flex-col gap-2 font-mono text-sm">
597 <NextLink
598 href="/sdks"
599 title="client sdks"
600 body="the data clients — @briven/react, svelte, vue — for reactive queries and mutations"
601 />
602 <NextLink
603 href="/api-keys"
604 title="api keys"
605 body="key types, scopes, and rotation across the dashboard"
606 />
607 <NextLink
608 href="/quickstart"
609 title="quickstart"
610 body="from nothing to a live project in five minutes"
611 />
612 </ul>
613 </DocsShell>
614 );
615}
616
617function Snippet({ children }: { children: string }) {
618 return (
619 <pre className="mt-2 overflow-x-auto rounded-md border border-[var(--color-border-subtle)] bg-[var(--color-code-bg)] p-3 font-mono text-xs text-[var(--color-code-text)]">
620 <code>{children}</code>
621 </pre>
622 );
623}
624
625function NextLink({ href, title, body }: { href: string; title: string; body: string }) {
626 return (
627 <li>
628 <a
629 href={href}
630 className="block rounded-md border border-[var(--color-border-subtle)] bg-[var(--color-surface)] p-4 transition hover:border-[var(--color-border)]"
631 >
632 <p className="font-mono text-[var(--color-text)]">{title}</p>
633 <p className="mt-1 font-mono text-xs text-[var(--color-text-muted)]">{body}</p>
634 </a>
635 </li>
636 );
637}