Docs

API: @mrbd/auth

Client helpers for MRBD-hosted auth flows.

Raw Markdown

API: @mrbd/auth

Install:

npm install @mrbd/auth

@mrbd/auth starts and completes a glasses-owned auth flow through MRBD-hosted auth services. The phone or computer is used only to enter the user's email at mrbd.link; the glasses browser initiates OTP, verifies OTP, and receives its own session.

Create a client

import { createMrbdAuth } from "@mrbd/auth";

const auth = createMrbdAuth({
  appId: "com.example.my-app",
});

appId identifies the registered MRBD app. The private MRBD auth backend validates this ID, allowed origins, rate limits, pairing state, and Supabase auth integration.

Register an app (and add the origins it is served from) in the developer portal or from the command line with mrbd apps create — see Developer Account (CLI).

Start sign-in

const request = await auth.startSignIn({
  onEvent: (event) => {
    if (event.type === "email_submitted") {
      void auth.sendOtp(event.email);
    }
  },
});

Show request.verificationUrl and request.userCode on the glasses. The user opens the URL on a phone or computer, enters the code, and submits their email.

Verify OTP

After the user receives the email OTP, collect it on the glasses and verify it:

const session = await auth.verifyOtp(otp);

The returned session belongs to the glasses browser. It is not copied from the phone or computer.

Convenience flow

signInWithCode() orchestrates the default sequence while letting the app render its own glasses UI:

const session = await auth.signInWithCode({
  onRequest: (request) => {
    showCodeOnGlasses(request.verificationUrl, request.userCode);
  },
  getOtp: async () => {
    return showOtpNumpadOnGlasses();
  },
});

Direct email sign-in (web and phone)

The flow above is designed for the glasses, which have no keyboard — the phone or computer at mrbd.link is only used to type the email. On a surface that *does* have a keyboard (a phone web app, or a desktop companion app), you can skip device pairing entirely and run a straight email-OTP flow:

await auth.sendEmailOtp("user@example.com");
// collect the 6-digit code the user received by email
const session = await auth.verifyEmailOtp(otp);

sendEmailOtp(email) asks MRBD to email a one-time code, and verifyEmailOtp(token) exchanges it for a session. The session is bound to the same appId and the same MRBD user as the glasses, so a user who signs in on their phone shares one identity (and one set of managed data) with the glasses app. As with the glasses flow, the session belongs to whichever browser completed it; nothing is copied between devices.

Use the glasses pairing flow (startSignIn / signInWithCode) on the glasses, and the direct email flow (sendEmailOtp / verifyEmailOtp) on keyboard surfaces.

Approve a new device from a signed-in one

If a user is already signed in on one device (say their phone), they can sign in a second device (their glasses) without entering a second OTP. The new device starts the normal pairing flow and shows its code; the signed-in device approves that code:

// On the already signed-in device (e.g. the phone):
await auth.approveDevice(userCode); // the code shown on the new device

The new device, which is waiting in startSignIn, automatically receives an approved event and claims its own session — its own access and refresh tokens, independently revocable. Nothing is copied between devices; the approving device only authorizes the grant with its own access token. The new device still proves possession of the device secret it generated, so the visible code alone can never produce a session.

React

@mrbd/auth/react provides ready-made components for React apps (React 18+):

import { MrbdAuthProvider, MrbdAuthGate, useMrbdAuth } from "@mrbd/auth/react";

<MrbdAuthProvider appId="com.example.my-app">
  <MrbdAuthGate>
    <SignedInApp />
  </MrbdAuthGate>
</MrbdAuthProvider>;
  • MrbdAuthProvider creates and shares the client and tracks session state.
  • MrbdAuthGate shows the built-in MrbdSignInScreen when signed out and renders its children when signed in. Pass a custom fallback to use a different sign-in UI.
  • MrbdSignInScreen (glasses) displays the verification URL and code, then collects the email OTP.
  • MrbdEmailSignInScreen (web/phone) collects the user's email and the one-time code directly, using the sendEmailOtp / verifyEmailOtp flow. Use it as the gate fallback on keyboard surfaces.
  • MrbdApproveDeviceScreen (signed-in surface) collects the code shown on another device and calls approveDevice so that device can sign in without its own OTP.
  • MrbdOtpNumpad is a 600x600, D-pad-navigable numeric pad used by the sign-in screens.
  • useMrbdAuth() exposes { client, session, status, refresh, signOut }.

create-mrbd-app scaffolds a working app/sign-in/page.tsx using these components — the glasses view uses MrbdSignInScreen and the web view uses MrbdEmailSignInScreen.

Customize the sign-in UI

The built-in screens are a convenience, not a requirement. You own the look of sign-in — there are three levels of control, from quick re-theming to fully custom UI. All of them keep MRBD's hosted identity, so @mrbd/data and @mrbd/storage keep working with the resulting session.

1. Re-theme the built-in screens

Every default screen renders from a small set of design tokens. Override the ones you care about — app-wide on the provider, or per-screen — and the rest fall back to the defaults:

<MrbdAuthProvider
  appId="com.example.my-app"
  theme={{
    colorBackground: "#06070a",
    colorPrimary: "#ff5c8a",
    colorPrimaryText: "#0a0a0f",
    colorAccent: "#ffd166",
    radiusMedium: 12,
    fontFamily: "var(--font-sans)",
  }}
>
  <MrbdAuthGate>
    <SignedInApp />
  </MrbdAuthGate>
</MrbdAuthProvider>;

See MrbdAuthTheme for the full token list (colors, radii, and fontFamily). defaultMrbdAuthTheme holds the defaults, and createMrbdAuthStyles(theme) / useMrbdAuthStyles(override?) expose the resolved styles if you want to reuse them in your own components.

2. Swap in your own screen

MrbdAuthGate renders MrbdSignInScreen when signed out, but you can pass any fallback:

<MrbdAuthGate fallback={<MyCustomSignIn />}>
  <SignedInApp />
</MrbdAuthGate>;

3. Roll your own UI with headless hooks

For full control over the markup, build on the headless hooks. They expose the exact flow state machine behind the built-in screens with no styling, so you render whatever you want and just drive the calls. MRBD still owns the OTP, identity, and tokens.

useMrbdEmailSignIn() — keyboard surfaces (phone/desktop web):

"use client";
import { useMrbdEmailSignIn } from "@mrbd/auth/react";
import { useState } from "react";

function CustomEmailSignIn() {
  const { phase, email, error, sendOtp, verifyOtp, resend } = useMrbdEmailSignIn({
    onSignedIn: (session) => console.log("signed in", session.userId),
  });
  const [value, setValue] = useState("");

  if (phase === "otp" || phase === "verifying") {
    return (
      <form onSubmit={(e) => { e.preventDefault(); verifyOtp(value); }}>
        <p>Code sent to {email}</p>
        <input value={value} onChange={(e) => setValue(e.target.value)} />
        <button type="submit">Verify</button>
        <button type="button" onClick={() => resend()}>Resend</button>
        {error && <p role="alert">{error}</p>}
      </form>
    );
  }

  return (
    <form onSubmit={(e) => { e.preventDefault(); sendOtp(value); }}>
      <input type="email" value={value} onChange={(e) => setValue(e.target.value)} />
      <button type="submit">Send code</button>
      {error && <p role="alert">{error}</p>}
    </form>
  );
}

useMrbdDeviceSignIn() — the glasses pairing flow. It starts automatically (pass { autoStart: false } to defer), exposes request.verificationUrl + request.userCode to display, sends the OTP on its own once the user submits their email on their phone, and gives you verifyOtp(code) for the code the user reads back on the glasses:

const { phase, request, error, verifyOtp } = useMrbdDeviceSignIn();
// phase: "starting" | "await_email" | "sending_otp" | "otp" | "verifying" | "done" | "error"

useMrbdApproveDevice() — the "approve another device" flow for an already-signed-in surface, exposing { phase, canApprove, error, approve, reset }.

If even the hooks are more than you need, drop to the vanilla client (useMrbdAuth().client, or createMrbdAuth()) and call startSignIn / sendEmailOtp / verifyEmailOtp directly.

Sessions

  • getSession() reads the stored session.
  • refreshSession() refreshes the stored session when a refresh token is available.
  • signOut() revokes the session through MRBD auth services and removes local storage.
  • onAuthStateChange(callback) subscribes to local session changes.

By default, sessions are stored in localStorage under an app-specific key. Pass storage: null to disable persistence.

Verify sessions on your backend

The session's accessToken is an MRBD-signed JWT scoped to your app: its audience (aud) is your appId, and its subject (sub) is the user id. A token minted through another app will not verify against your appId, so you can trust it as proof of a signed-in user for *your* app specifically.

Verify it on your server with @mrbd/auth/server, which checks the signature against MRBD's published keys (/.well-known/jwks.json) and enforces the audience:

import { createMrbdTokenVerifier } from "@mrbd/auth/server";

// Create once and reuse so the public keys stay cached.
const verifier = createMrbdTokenVerifier({ appId: "com.example.my-app" });

// In a request handler:
const token = request.headers.get("authorization")?.replace(/^Bearer /, "");
const { userId, email, scope } = await verifier.verify(token);

verify() throws an MrbdAuthError (code invalid_session) when the token is missing, expired, signed by an unknown key, or was issued for a different app. Never trust the userId/appId fields of the stored session object without verifying the token — those are only labels until the JWT is checked.

The refresh token returned in the session is an opaque, app-bound MRBD token; the underlying Supabase session is never exposed to clients. Refreshing or revoking always goes through the MRBD auth backend.

Security model

The public package never talks directly to Supabase. It calls the private MRBD auth backend, which owns pairing, email OTP, Supabase integration, app registration, token issuance, rate limits, and audit logs.

Access tokens are app-scoped JWTs signed by MRBD (audience = your appId); the backend never returns raw Supabase tokens to clients. Each registered app must declare its allowed origins, and the backend rejects auth flows from any other origin.

Realtime pairing events are used for flow status and email handoff. Sensitive session credentials are fetched over HTTPS, not delivered over WebSocket or SSE.

Legal requirements for your app

Because MRBD authenticates real people on your behalf, you have obligations to your end users. When you accept the MRBD Developer Terms in the portal, you also agree to the Privacy Policy and Data Processing Addendum, under which **you are the data controller** for your end users and **MRBD is your processor** for managed authentication.

What this means in practice:

  • **A privacy policy is required.** When you register an app in the developer portal, you must either provide your own privacy policy URL or let MRBD host a generated one for your app. The pairing screen shows end users a link to your privacy policy (and MRBD's) before they submit their email.
  • **A terms of service is optional.** Provide your own URL or opt into an MRBD-hosted generated version.
  • **Keep your details accurate.** The publisher name and legal contact email you register are used to fill any MRBD-generated documents and are surfaced to end users for data requests.

MRBD-hosted documents live at https://mrbd.io/legal/app/<app-id>/privacy (and /terms). MRBD's own policies are at mrbd.io/legal.