AuthenSeeDocs

UI Components

What ships in @rebellion-systems/authensee-ui — primitives, question inputs, SDK React glue, and turnkey flows. How to compose them.

UI Components

The @rebellion-systems/authensee-ui package is a thin React component library that pairs with the SDK. These components drive the on-device ceremony and are what the AuthenSee hosted pages render; they're published for AuthenSee's first-party surfaces and React Native parity. Two composition styles:

  1. Turnkey — drop in <EnrollmentFlow> / <AuthenticationFlow> and get a styled multi-step UI for free.
  2. Composed — wrap your app in <AuthenSeeProvider> and assemble your own flows from the primitives + question inputs.

First-party / hosted-runtime components. Like the SDK methods, these only work served from AuthenSee's own origin. If you're integrating from your own app, use the hosted popup instead — see the embed guide.

Hosted pages (auth.authensee.com) uses the composed path because it has its own brand chrome and demo routes — but anyone integrating AuthenSee can pick whichever path fits their UX.

Install

pnpm add @rebellion-systems/authensee-embed

Use the embed package from your browser app to launch the hosted flow:

import { open } from "@rebellion-systems/authensee-embed";

What's in the package

@rebellion-systems/authensee-ui
├── theme/         AuthenSeeThemeProvider, useTheme(), DEFAULT_THEME, themeToCssVars
├── primitives/    Button, Card, ProgressBar, Logo, ErrorDisplay, SuccessDisplay
├── questions/     TextInput, RecognitionGrid, DoodleCanvas, SequenceInput, MapPicker,
│                  QuestionInput (dispatcher)
├── sdk/           AuthenSeeProvider, useAuthenSee(), useProofProgress()
├── flows/         EnrollmentFlow, AuthenticationFlow, QuestionStep
└── styles/        globals.css — design-token CSS variables

Everything is themeable via the same AuthenSeeTheme shape — see the theming guide for full details.

Theme

The theme provider plumbs CSS custom properties (--authensee-primary, etc.) into the DOM so every component picks up your brand colors without prop drilling.

import { AuthenSeeProvider } from "@rebellion-systems/authensee-ui";
 
<AuthenSeeProvider
  sessionToken="sess_…"
  serverUrl="https://api.authensee.com"
  theme={{
    primary: "#FF7B1A",
    background: "#000",
    foreground: "#FFF",
    fontHeading: "Space Grotesk, sans-serif",
  }}
>
  {children}
</AuthenSeeProvider>

AuthenSeeProvider wraps both the theme provider and the SDK provider. If you only want the theme (e.g., to style a marketing page that doesn't need the SDK), use AuthenSeeThemeProvider directly.

Default theme is the orange/black brand palette mirroring authensee.com; themeToCssVars() is exported if you need to compute the variable map yourself.

Primitives

Brand-styled brutalist building blocks. Every primitive responds to the active theme via CSS variables.

ComponentPurposeKey props
<Button>Primary / secondary / ghost actionsvariant, size, loading, disabled, onClick
<Card>Content panel with brand border + paddingclassName, children
<ProgressBar>Step indicator (1 of N)current, total, label
<Logo>The AuthenSee mark; respects theme.logoSrc overrideclassName, alt
<ErrorDisplay>Structured error rendering keyed on AuthenSeeErrorCodecode, message, onRetry
<SuccessDisplay>Success statetitle, message

Example:

import { Button, Card, ProgressBar, ErrorDisplay } from "@rebellion-systems/authensee-ui";
 
<Card>
  <ProgressBar current={2} total={3} label="Step 2 of 3 — Recall your answer" />
  {error ? (
    <ErrorDisplay code="INVALID_PROOF" message="Try again" onRetry={handleRetry} />
  ) : (
    <Button variant="primary" loading={busy} onClick={handleSubmit}>
      Authenticate →
    </Button>
  )}
</Card>

Question inputs

One component per QuestionTypeId. Each component takes a questionText (optional prompt label) and an onChange(answer | null) callback. The callback fires with the typed AnswerData discriminated union — pass that straight into sdk.enroll() / sdk.authenticate().

ComponentQuestionTypeIdCaptures
<TextInput>EPISODIC_TEXT (1)Free-text answer (autocomplete/spellcheck off — answers are sensitive)
<RecognitionGrid>RECOGNITION (2)One of N option tiles (with optional images)
<DoodleCanvas>DOODLE (3)A 16×16 bitmap drawing
<SequenceInput>SEQUENCE (4)Ordered list of symbol IDs
<MapPicker>MAP_ROUTE (8)Four (lat, lng) points rasterized to Web Mercator buckets

If you don't know the typeId at compile time (e.g., it comes from a server-driven question schema), use the dispatcher:

import { QuestionInput } from "@rebellion-systems/authensee-ui";
 
<QuestionInput
  typeId={question.typeId}
  questionText={question.text}
  options={question.options}
  symbols={question.symbols}
  onChange={(answer) => setAnswer(answer)}
/>

It picks the right concrete component from the typeId.

SDK glue

Three hooks/contexts that connect the UI to the underlying SDK class:

<AuthenSeeProvider>

Wraps your tree. Initialises an AuthenSee SDK instance with the sessionToken you pass in and exposes it via context. Composing also sets up theme + state for proof-progress events.

import { AuthenSeeProvider } from "@rebellion-systems/authensee-ui";
 
<AuthenSeeProvider
  sessionToken={session.sessionToken}
  serverUrl={session.serverUrl}
  theme={session.theme}
  debug={false}
>
  {children}
</AuthenSeeProvider>

useAuthenSee()

Returns { sdk, isInitialized, persona, error } from anywhere in the tree:

import { useAuthenSee } from "@rebellion-systems/authensee-ui";
 
function EnrolButton() {
  const { sdk, isInitialized } = useAuthenSee();
  if (!isInitialized) return <p>Initializing…</p>;
  return (
    <Button onClick={() => sdk.enroll(/* … */)}>
      Continue
    </Button>
  );
}

useProofProgress()

Subscribes to proof.generating / proof.complete / auth.success / auth.failure events emitted by the SDK and exposes them as React state — useful for spinners, progress bars, and lifecycle UX.

import { useProofProgress } from "@rebellion-systems/authensee-ui";
 
function ProofSpinner() {
  const { phase, provingTimeMs, error } = useProofProgress();
  if (phase === "generating") return <p>Generating proof…</p>;
  if (phase === "complete") return <p>Proof in {provingTimeMs}ms ✓</p>;
  if (phase === "failure") return <ErrorDisplay code={error.code} message={error.message} />;
  return null;
}

Turnkey flows

Multi-step orchestrators that compose the primitives + question inputs + SDK calls + WebAuthn ceremony into a single ready-to-drop-in UI. Two are exported:

  • <EnrollmentFlow> — walks the user through their question, runs the WebAuthn registration ceremony, calls sdk.enroll() with the aggregate scheme commitment.
  • <AuthenticationFlow> — re-asks the user's question, drives the WebAuthn assertion via the SDK's callback, calls sdk.authenticate().

Plus <QuestionStep>, the building block both flows use internally if you want to assemble your own multi-step.

Both flows take a passkey: PasskeyHandlers prop. The fastest way to get one is the browserPasskey() factory from the /passkey sub-path:

import { AuthenSeeProvider, EnrollmentFlow } from "@rebellion-systems/authensee-ui";
import { browserPasskey } from "@rebellion-systems/authensee-ui/passkey";
 
const passkey = browserPasskey({ rpId: "auth.acme.com", rpName: "Acme" });
 
<AuthenSeeProvider sessionToken="sess_…" serverUrl="https://api.authensee.com">
  <EnrollmentFlow
    externalUserId="user_123"
    questions={[
      { typeId: 1, text: "If you had to name a future cat, what would you call it?" },
    ]}
    passkey={passkey}
    onComplete={(result) => console.log("Enrolled:", result.merkleRoot)}
    onError={(err) => console.error(err)}
  />
</AuthenSeeProvider>

<AuthenticationFlow> takes the same passkey object — the flow uses passkey.sign instead of passkey.create:

import { AuthenticationFlow } from "@rebellion-systems/authensee-ui";
 
<AuthenSeeProvider sessionToken="sess_…" serverUrl="https://api.authensee.com">
  <AuthenticationFlow
    externalUserId="user_123"
    questions={[{ typeId: 1, text: "Recall the future-cat name" }]}
    passkey={passkey}
    onComplete={(result) => console.log("JWT:", result.token)}
    onError={(err) => console.error(err.code, err.message)}
  />
</AuthenSeeProvider>

For V1 the question list must contain exactly one question (the passkey_question_v1 scheme aggregates one answer + one passkey signature in the same proof). Multi-question schemes will be a future scheme/circuit pair, not a backward-compatible knob.

browserPasskey() — the default browser ceremony

Returns a single PasskeyHandlers object exposing both .create (registration) and .sign (authentication). The implementation:

  • Builds PublicKeyCredentialCreationOptions / RequestOptions with userVerification: "required" (the V1 circuit asserts the UV bit).
  • Pulls the P-256 public key out of the SPKI bytes returned by getPublicKey().
  • Parses the DER-encoded ECDSA signature into raw r || s and applies low-s normalisation (the V1 verifier rejects high-s).

Lives on a separate sub-path so the DOM-bound code doesn't get pulled into the main bundle for non-browser callers (React Native, SSR-only consumers).

Custom ceremonies

For non-browser runtimes (React Native), custom RP layouts, or any other reason you'd want to bypass browserPasskey, implement the PasskeyHandlers interface directly. The types are re-exported from the main barrel:

import type { PasskeyHandlers, CreatedPasskey, PasskeyCreateArgs } from "@rebellion-systems/authensee-ui";
 
const reactNativePasskey: PasskeyHandlers = {
  rpId: "auth.acme.com",
  rpName: "Acme",
  create: async ({ challengeBytes, userId, userName }) => {
    // your react-native-passkeys glue here
    return { credentialId, pubkeyX, pubkeyY };
  },
  sign: async (challengeBytes) => {
    // your react-native-passkeys glue here
    return { signature, authenticatorData, clientDataJSON, expectedOrigin, rpId, actionHash };
  },
};
 
<EnrollmentFlow passkey={reactNativePasskey} ... />

Composed pattern

If you want full control over layout — a multi-step wizard that doesn't fit the turnkey shape, custom progress chrome, etc. — wrap with <AuthenSeeProvider> and drop in the primitives + question inputs yourself. Hosted-pages (auth.authensee.com) uses this pattern because it has its own brand UX.

"use client";
 
import {
  AuthenSeeProvider,
  Button,
  TextInput,
  ErrorDisplay,
  useAuthenSee,
} from "@rebellion-systems/authensee-ui";
import { browserPasskey } from "@rebellion-systems/authensee-ui/passkey";
 
function EnrolPage({ session }: { session: { token: string; serverUrl: string; externalUserId: string } }) {
  return (
    <AuthenSeeProvider sessionToken={session.token} serverUrl={session.serverUrl}>
      <EnrolForm externalUserId={session.externalUserId} />
    </AuthenSeeProvider>
  );
}
 
function EnrolForm({ externalUserId }: { externalUserId: string }) {
  const { sdk, isInitialized } = useAuthenSee();
  const [answer, setAnswer] = React.useState("");
  const [error, setError] = React.useState<string | null>(null);
  const [busy, setBusy] = React.useState(false);
 
  const onSubmit = async () => {
    if (!sdk) return;
    setBusy(true);
    try {
      await sdk.identify(externalUserId);
 
      const passkey = browserPasskey({
        rpId: window.location.hostname,
        rpName: "Acme",
      });
      const challengeBytes = new Uint8Array(32);
      crypto.getRandomValues(challengeBytes);
      const created = await passkey.create({
        userId: sdk.getPersona()!.id,
        userName: externalUserId,
        challengeBytes,
      });
 
      await sdk.enroll("security_questions", {
        questions: [{ text: "Name a future cat", answer }],
        passkey: {
          credentialId: created.credentialId,
          pubkeyX: created.pubkeyX,
          pubkeyY: created.pubkeyY,
          rpId: window.location.hostname,
        },
      });
    } catch (err) {
      setError(err instanceof Error ? err.message : String(err));
    } finally {
      setBusy(false);
    }
  };
 
  if (!isInitialized) return <p>Initializing…</p>;
  return (
    <div>
      <TextInput questionText="Name a future cat" onChange={(a) => setAnswer(a?.value ?? "")} />
      <Button variant="primary" onClick={onSubmit} loading={busy} disabled={!answer.trim()}>
        Continue →
      </Button>
      {error && <ErrorDisplay code="INVALID_PROOF" message={error} />}
    </div>
  );
}

For authentication, replace passkey.create with passkey.sign and pass it through to sdk.authenticate({ answers, passkey: { sign: passkey.sign } }).

Where things ship

ConcernLives inWhy
Hosted-flow launch and callback relay@rebellion-systems/authensee-embedProvider-facing browser integration
Provider session creation and result exchange@rebellion-systems/authensee-sdkServer-only provider SDK
Theme tokens, primitives, question inputs, turnkey flowsAuthenSee hosted runtimeOperated by AuthenSee for hosted flows
WebAuthn DOM helpers (navigator.credentials.create / .get)@rebellion-systems/authensee-ui/passkey (browser) — or your own implementation of PasskeyHandlers for other runtimesCentralised so the byte-level WebAuthn parsing (DER, SPKI, low-s normalisation) lives in one place; the sub-path keeps DOM code out of non-browser bundles
Session validation, demo launching, hosted-pages routingauthnc-hosted-pages (private to AuthenSee)Service-specific to the operated reference deployment. Not published as a library.

React Native

React Native runtime components are not a public provider integration surface yet. Mobile providers should launch hosted flows and exchange result codes from their backend, matching the hosted-pages model.

On this page