auth

@briven/auth is a drop-in sign-in client for your briven project. one package gives you email + password, magic links, email OTP, passkeys (WebAuthn), and OAuth social login — plus React hooks and a prebuilt <BrivenSignIn /> panel. the SDK talks to the hosted briven auth service; you never run an auth server yourself.

setup in 10 minutes

the shortest path from zero to a working sign-in. for a browser-only sign-off after this, use the plain-language checklist in the briven repo: AUTH-GO-LIVE-CHECKLIST.md.

  1. open your project at briven.tech Auth → enable auth if it is not already on.
  2. Auth → API keys → create a key → copy pk_briven_auth_… (browser-safe). never put a brk_… key in the browser.
  3. in your app folder, wire the project first:
    # NEW project:   briven setup my-app
    # EXISTING:      briven connect p_…
    briven auth scaffold
    pnpm add @briven/auth
    that writes middleware.ts, lib/auth.ts, and a seeded .env.local if missing. never use setup --project to attach — that flag is gone; use briven connect.
  4. paste the public key into NEXT_PUBLIC_BRIVEN_AUTH_KEY (and the same value into BRIVEN_AUTH_PUBLIC_KEY for middleware).
  5. send users to the hosted page or drop in the panel:
    // hosted (fastest pilot)
    window.location.assign(auth.hostedPageURL('sign-in', '/dashboard'));
    
    // or embedded React panel
    import { BrivenSignIn } from '@briven/auth/react';
    // <BrivenSignIn redirectTo="/dashboard" />
  6. prove it in a real browser: sign up, sign out, sign in, refresh still signed in. optional: wrong-password rejection + rate limit after many failures (production needs redis — /ready must show redis: ok).

starter copy: examples/auth-pilot/ in the briven monorepo.

install

published to public npm — see npm for the current version.

$ npm install @briven/auth

subpaths: @briven/auth (the vanilla client, zero React deps), @briven/auth/react (hooks + the <BrivenSignIn /> component), and @briven/auth/server (Next.js App Router helpers).

get your keys

you need two things from the dashboard: your project id (looks like p_01HZ…) and a public auth key (looks like pk_briven_auth_…).

  1. open your project at briven.tech.
  2. go to Auth → API keys.
  3. click create key, give it a name, set the scope to read-write.
  4. copy the pk_briven_auth_… value — it is shown once.
which key is which: the pk_briven_auth_ key is browser-safe. it identifies your tenant and unlocks the end-user sign-in surface only — it can't read or write your data. ship it to the browser freely. your server brk_* keys are not browser-safe; never put one in client code.

create the client

createBrivenAuth returns a stateless client. all auth state lives in the browser cookie set by the service on a successful sign-in, so re-creating the client across renders is safe.

// lib/auth.ts
import { createBrivenAuth } from '@briven/auth';

export const auth = createBrivenAuth({
  projectId: 'p_01HZ...',
  publicKey: process.env.NEXT_PUBLIC_BRIVEN_AUTH_KEY!, // 'pk_briven_auth_...'
});

options: projectId and publicKey are required. apiOrigin defaults to https://api.briven.tech (override it only for self-hosted or local dev). authUrl defaults to https://<projectId>.auth.briven.tech and backs the OAuth redirect.

the client exposes signIn, signUp.email, signOut(), getSession(), and getUser().

react bindings

import from @briven/auth/react. wrap your app in the provider, then read state with the hooks or drop in the prebuilt panel. works in any React 19 environment — no hard Next.js dependency.

provider

// app/providers.tsx
'use client';
import { BrivenAuthProvider } from '@briven/auth/react';
import { auth } from '../lib/auth';

export function Providers({ children }: { children: React.ReactNode }) {
  return <BrivenAuthProvider value={auth}>{children}</BrivenAuthProvider>;
}

hooks

useSession() returns { session, isLoading, refresh }; useUser() returns { user, isLoading, refresh }. both fetch once on mount — call refresh() after a sign-in or sign-out to re-read.

'use client';
import { useSession, useUser } from '@briven/auth/react';

export function AccountBadge() {
  const { session, isLoading } = useSession();
  const { user } = useUser();

  if (isLoading) return <span>…</span>;
  if (!session?.authenticated) return <a href="/sign-in">sign in</a>;
  return <span>signed in</span>; // don't render user.email into the UI — see security
}

prebuilt panel

<BrivenSignIn />renders email + password, magic link, and your chosen OAuth buttons in one panel. it carries no icon library and no css framework — your app's styles apply via className and the element classes.

'use client';
import { BrivenSignIn } from '@briven/auth/react';

export function SignIn() {
  return (
    <BrivenSignIn
      providers={['google', 'github', 'konnos']}
      showEmailPassword
      showMagicLink
      redirectTo="/dashboard"
      onSuccess={({ userId }) => console.log('signed in', userId)}
      className="my-signin"
    />
  );
}

props: providers (OAuth buttons to show — empty array hides the section), showEmailPassword (default true), showMagicLink (default true), redirectTo, onSuccess, and className.

sign-in flows

if you want your own UI, call the client methods directly. every async method returns a tagged result — { ok: true, ... } on success or { ok: false, code, message } on failure — so you never throw on a bad password.

email + password

// sign up, then sign in — both return { ok, userId, sessionExpiresAt }
await auth.signUp.email({ email, password, name: 'Jane' });

const result = await auth.signIn.email({ email, password });
if (result.ok) {
  // result.userId, result.sessionExpiresAt
} else {
  // result.code: 'invalid_credentials' | 'unverified_email' | 'rate_limited' | ...
  console.error(result.message);
}

magic link

a passwordless flow of its own — not an OAuth provider. it emails the user a one-click sign-in link.

const res = await auth.signIn.magicLink({
  email,
  redirectTo: '/dashboard', // where the link lands after verify
});
// res.ok === true means the email was sent

email OTP

// 1. send the code
await auth.signIn.otpRequest({ email });

// 2. verify the 6-digit code the user typed
const result = await auth.signIn.otpVerify({ email, otp: '123456' });
if (result.ok) { /* result.userId */ }

OAuth social

social() is synchronous — it builds the start URL and hands it back. you redirect the browser to it. provider is one of 'google', 'github', 'discord', 'microsoft', or 'konnos'.

const { redirectUrl } = auth.signIn.social({
  provider: 'google',
  redirectTo: '/dashboard',
});
window.location.assign(redirectUrl);

passkeys (WebAuthn)

passwordless sign-in backed by the device's platform authenticator (Touch ID, Face ID, Windows Hello, a hardware key). two ceremonies, both wrapped for you so you never touch the raw navigator.credentials calls:

// REGISTER — adds a passkey to the *currently signed-in* user.
// call this from an account-settings screen, after a normal sign-in.
const reg = await auth.passkey.register();
if (reg.ok) { /* passkey saved on this device */ }

// SIGN IN — no password, no email field needed.
const result = await auth.passkey.signIn();
if (result.ok) {
  // result.userId, result.sessionExpiresAt
} else {
  // result.code: 'not_enabled' | 'cancelled' | 'unsupported' | ...
  console.error(result.message);
}

passkeys are per-tenant: enable under Auth → providers. auth.passkey.register() needs an active session first (magic link / OTP / password), then Face ID / Touch ID runs in the browser. platform routes: GET /v1/auth-tenant/passkey/generate-authenticate-options (sign-in) and GET …/generate-register-options (enrol) — not POST on those paths (POST returns 404 by design). verify is POST …/verify-authentication / POST …/verify-registration. rpID is the parent domain (e.g. briven.tech for apps on *.briven.tech). WebAuthn needs https or localhost; add every app origin under Auth → Allowed Domains.

before these work in production: social login (google / github / discord / microsoft / konnos) needs the project owner to set that provider's client id and secret under Auth → providersin the dashboard — until both are set, the provider stays disabled. magic-link and email-OTP emails send out of the box from the briven fallback sender — to brand them as your own domain, see email sender domain.

two-factor + backup codes

when the project has two-factor enabled, password sign-in may return twoFactorRequired. complete the challenge with a TOTP app code or a single-use backup recovery code (for lost phones).

// after password sign-in returns twoFactorRequired:
const totp = await auth.twoFactor.verify('123456');
// or lost phone:
const backup = await auth.twoFactor.verifyBackupCode('XXXX-XXXX');

// enroll (show codes once after verify):
await auth.twoFactor.enable(password);
await auth.twoFactor.verify(appCode);
const { codes } = await auth.twoFactor.generateBackupCodes(password);
// save codes offline — each works once

hosted pages: after password, users land on /auth/<projectId>/two-factor with a toggle for backup codes. react: TwoFactorSetup + TwoFactorChallenge from @briven/auth/react.

testing tokens (e2e)

for automated tests, mint a testing token in the dashboard (or POST /v1/projects/:id/auth/test-tokens as an admin). it bypasses bot checks, rate limits, and MFA for a short window and is returned once as briven_test_….

// CI / e2e (never commit the raw token)
const session = await auth.signIn.testToken(process.env.BRIVEN_AUTH_TEST_TOKEN!);
// → { ok: true, expiresAt } and a real session cookie

revoke used tokens in the dashboard. production apps for real humans should not rely on testing tokens.

email sender domain

every email briven auth sends for you — magic links, OTP codes, email verification, password resets — has a From: address. by default that is noreply@briven.tech, which works out of the box with zero setup. when you want those emails branded as your own product (say noreply@yourapp.com), you set a sender domain.

how to set it

  1. open your project at briven.tech.
  2. go to Auth → branding.
  3. fill in sender name (the display name, e.g. your product name) and sender domain — your root domain, e.g. yourapp.com. do not enter a subdomain like auth.yourapp.com unless you really send from that subdomain.
  4. save. that's the whole dashboard side.

verify the domain (2–3 DNS records)

email worldwide runs on one rule: a domain must publicly declare who is allowed to send mail in its name. that declaration lives in your domain's DNS as SPF and DKIM records. without them, providers like Gmail and Outlook treat mail from your domain as forged — so no platform (briven included) can skip this step. briven hands you the exact records to add at your domain provider; a guided setup wizard is landing in the branding panel.

what happens before you verify

nothing breaks — this is the important part. until your domain is verified, briven automatically keeps sending from the noreply@briven.techfallback (with your sender name), so your users' sign-in emails always arrive. the moment the domain verifies, sends switch to noreply@yourdomain on their own. you never need to time the switch or fear a broken login flow while DNS propagates.

troubleshooting: sign-in emails arriving from noreply@briven.techeven though you filled in a sender domain? that means the domain isn't verified yet — finish adding the DNS records and allow up to an hour for DNS to propagate. emails not arriving at all? check spam first, then Auth → usage for delivery status.

verify users with tokens (JWT + JWKS)

getSession()asks the briven auth service every time. when your setup has its own backend — or several apps sharing one sign-in — you often just need a fast local answer to "is this user signed in?" without a network round-trip per request. for that, briven auth issues verifiable tokens: short-lived signed JWTs your own code can check against the project's public keys.

the two endpoints

  • GET /v1/auth-tenant/token — requires a signed-in session (the browser cookie). returns { token: "<JWT>" }, a short-lived signed JWT for the current user.
  • GET /v1/auth-tenant/jwks— public, no auth. returns the project's JSON Web Key Set, so your code can verify JWTs locally without calling briven.

both live under https://api.briven.tech and carry your x-briven-project-id header like every auth request.

verify a token

any standard JWT library that understands remote JWKS works — shown here with jose:

// in the signed-in browser: mint a token, hand it to your backend
const res = await fetch('https://api.briven.tech/v1/auth-tenant/token', {
  headers: {
    'x-briven-project-id': 'p_01HZ...',
    authorization: 'Bearer pk_briven_auth_...', // the browser-safe key
  },
  credentials: 'include', // sends the session cookie
});
const { token } = await res.json();

// on YOUR server: verify locally — no call to briven per request
import { createRemoteJWKSet, jwtVerify } from 'jose';

const jwks = createRemoteJWKSet(
  new URL('https://api.briven.tech/v1/auth-tenant/jwks'),
  { headers: { 'x-briven-project-id': 'p_01HZ...' } },
);

const { payload } = await jwtVerify(token, jwks);
// payload.sub is the signed-in user's id
keys and rotation: signing keys are per-project — a token from one project never verifies in another. key rotation is handled through the JWKS endpoint: when briven rotates a key, the new one appears in the key set, so refetch the JWKS whenever verification hits an unknown key id (libraries like jose do this for you). tokens are short-lived by design — refetch /token on expiry rather than storing one long-term.

security

  • the pk_briven_auth_ key is browser-safe: it identifies the tenant and unlocks the end-user sign-in surface only. your server brk_* keys are not — keep them server-side. read scope keys cannot POST mutating auth-tenant routes.
  • never log or display an end-user's email or IP. the User object carries emailfor the account holder — don't echo it into a list view, a log line, or an analytics event.
  • sessions live in an http cookie the service sets on sign-in; the SDK sends requests with credentials: 'include'so the browser stores and returns it. you don't handle tokens yourself.
  • password policy (length / character rules / max age / force reset) and rate limits are per-project. production should show redis: ok on /ready so limits share across servers.
  • new device emails use a hashed browser fingerprint (no raw IPs). SSO redirects after SAML/OIDC only allow listed app origins.
wire protocol (you don't write this): every request carries x-briven-project-id: <projectId> and authorization: Bearer <publicKey> to https://api.briven.tech/v1/auth-tenant/*. the service resolves your tenant from the header and routes the request. the SDK adds these headers for you.

for AI agents

when an agent wires auth, it should follow the briven-auth skill (not invent OAuth). trusted order: link project → briven auth scaffold → install @briven/auth → human pastes pk_briven_auth_… → hosted page or BrivenSignIn → human runs the go-live checklist.

  • MCP helpers are read-only today: auth_config_get, sender_domain_status, auth_docs_ask. they explain; they do not mint keys or write provider secrets.
  • never put secrets in git or chat. never use brk_ in NEXT_PUBLIC_*.
  • sessions are cookies — prefer getSession() / useSession(), not home-rolled localStorage tokens.
  • for e2e, use testing tokens (auth.signIn.testToken), not real MFA bypass hacks. for lost-phone recovery, implement backup codes (see two-factor).
  • framework hooks: react useUserMetadata / useUserEmails; vue + svelte have matching helpers. pilot: examples/auth-pilot/.

what to read next