SDKs
React

Frontend SDK: React Hooks

The Optare React SDK provides hooks and utilities for managing authentication state in your frontend application. These hooks make it easy to access user data, check authentication status, and control access to UI elements.

Installation

The React hooks are included in the Better Auth client:

pnpm add better-auth

Configuration

Create your auth client in lib/auth-client.ts:

import { createAuthClient } from "better-auth/react";
 
export const authClient = createAuthClient({
  baseURL: process.env.NEXT_PUBLIC_APP_URL || "http://localhost:3000",
});
 
export const { useSession, signIn, signUp, signOut } = authClient;

Core Hooks

useSession

Returns the current user's session, including authentication status, user data, organization, and role.

Signature:

function useSession(): {
  data: Session | null;
  isPending: boolean;
  error: Error | null;
}

Session Object:

interface Session {
  user: {
    id: string;
    name: string;
    email: string;
    image?: string;
    emailVerified: boolean;
  };
  organization?: {
    id: string;
    name: string;
    slug: string;
  };
  role?: "owner" | "admin" | "member";
  expiresAt: string;
}

Example:

import { useSession } from "~/lib/auth-client";
 
export function UserProfile() {
  const { data: session, isPending, error } = useSession();
 
  if (isPending) {
    return <div>Loading...</div>;
  }
 
  if (error) {
    return <div>Error: {error.message}</div>;
  }
 
  if (!session) {
    return <div>Not signed in</div>;
  }
 
  return (
    <div>
      <h2>Welcome, {session.user.name}!</h2>
      <p>Email: {session.user.email}</p>
      {session.organization && (
        <p>Organization: {session.organization.name}</p>
      )}
      {session.role && <p>Role: {session.role}</p>}
    </div>
  );
}

Authentication Functions

signIn.social

Initiates OAuth login with Optare.

Signature:

function signIn.social(options: {
  provider: "optare";
  callbackURL?: string;
}): Promise<void>

Example:

import { signIn } from "~/lib/auth-client";
 
export function SignInButton() {
  const handleSignIn = async () => {
    await signIn.social({
      provider: "optare",
      callbackURL: "/dashboard",
    });
  };
 
  return (
    <button onClick={handleSignIn}>
      Sign in with Optare
    </button>
  );
}

signOut

Signs out the current user and clears their session.

Signature:

function signOut(options?: {
  callbackURL?: string;
}): Promise<void>

Example:

import { signOut } from "~/lib/auth-client";
 
export function SignOutButton() {
  const handleSignOut = async () => {
    await signOut({
      callbackURL: "/",
    });
  };
 
  return (
    <button onClick={handleSignOut}>
      Sign Out
    </button>
  );
}

Common Patterns

Protecting Components

Only render content if the user is authenticated:

import { useSession } from "~/lib/auth-client";
 
export function AdminPanel() {
  const { data: session } = useSession();
 
  if (!session) {
    return <div>Please sign in to access this page.</div>;
  }
 
  return (
    <div>
      <h1>Admin Panel</h1>
      {/* Admin content */}
    </div>
  );
}

Role-Based Rendering

Show different UI based on the user's role:

import { useSession } from "~/lib/auth-client";
 
export function TeamSettings() {
  const { data: session } = useSession();
 
  return (
    <div>
      <h1>Team Settings</h1>
      
      {/* Everyone can see basic info */}
      <div>
        <p>Organization: {session?.organization?.name}</p>
      </div>
 
      {/* Only admins and owners can invite members */}
      {(session?.role === "admin" || session?.role === "owner") && (
        <div>
          <h2>Invite Members</h2>
          <InviteMemberForm />
        </div>
      )}
 
      {/* Only owners can delete the organization */}
      {session?.role === "owner" && (
        <div>
          <h2>Danger Zone</h2>
          <DeleteOrganizationButton />
        </div>
      )}
    </div>
  );
}

Loading States

Handle loading and error states gracefully:

import { useSession } from "~/lib/auth-client";
 
export function Dashboard() {
  const { data: session, isPending, error } = useSession();
 
  if (isPending) {
    return (
      <div className="flex items-center justify-center min-h-screen">
        <div className="animate-spin rounded-full h-12 w-12 border-b-2 border-gray-900"></div>
      </div>
    );
  }
 
  if (error) {
    return (
      <div className="flex items-center justify-center min-h-screen">
        <div className="text-red-600">
          <h2>Error loading session</h2>
          <p>{error.message}</p>
        </div>
      </div>
    );
  }
 
  if (!session) {
    return <Navigate to="/sign-in" />;
  }
 
  return (
    <div>
      <h1>Dashboard</h1>
      {/* Dashboard content */}
    </div>
  );
}

Protected Route Wrapper

Create a reusable component for protected routes:

import { useSession } from "~/lib/auth-client";
import { Navigate } from "@remix-run/react"; // or next/navigation
import { ReactNode } from "react";
 
interface ProtectedRouteProps {
  children: ReactNode;
  requiredRole?: "owner" | "admin" | "member";
}
 
export function ProtectedRoute({ children, requiredRole }: ProtectedRouteProps) {
  const { data: session, isPending } = useSession();
 
  if (isPending) {
    return <div>Loading...</div>;
  }
 
  if (!session) {
    return <Navigate to="/sign-in" />;
  }
 
  if (requiredRole) {
    const roleHierarchy = { owner: 3, admin: 2, member: 1 };
    const userRoleLevel = roleHierarchy[session.role || "member"];
    const requiredRoleLevel = roleHierarchy[requiredRole];
 
    if (userRoleLevel < requiredRoleLevel) {
      return <Navigate to="/dashboard" />;
    }
  }
 
  return <>{children}</>;
}

Usage:

// In Next.js App Router
export default function AdminPage() {
  return (
    <ProtectedRoute requiredRole="admin">
      <div>
        <h1>Admin Dashboard</h1>
        {/* Admin content */}
      </div>
    </ProtectedRoute>
  );
}

User Avatar Component

Build a user menu with session data:

import { useSession, signOut } from "~/lib/auth-client";
import { useState } from "react";
 
export function UserMenu() {
  const { data: session } = useSession();
  const [isOpen, setIsOpen] = useState(false);
 
  if (!session) {
    return null;
  }
 
  return (
    <div className="relative">
      <button
        onClick={() => setIsOpen(!isOpen)}
        className="flex items-center space-x-2"
      >
        {session.user.image ? (
          <img
            src={session.user.image}
            alt={session.user.name}
            className="w-8 h-8 rounded-full"
          />
        ) : (
          <div className="w-8 h-8 rounded-full bg-gray-300 flex items-center justify-center">
            {session.user.name[0].toUpperCase()}
          </div>
        )}
        <span>{session.user.name}</span>
      </button>
 
      {isOpen && (
        <div className="absolute right-0 mt-2 w-48 bg-white rounded-md shadow-lg py-1">
          <div className="px-4 py-2 text-sm text-gray-700 border-b">
            <p className="font-medium">{session.user.name}</p>
            <p className="text-gray-500">{session.user.email}</p>
          </div>
          
          {session.organization && (
            <div className="px-4 py-2 text-sm text-gray-700 border-b">
              <p className="text-gray-500">Organization</p>
              <p className="font-medium">{session.organization.name}</p>
              <p className="text-xs text-gray-400 capitalize">{session.role}</p>
            </div>
          )}
 
          <button
            onClick={() => signOut()}
            className="block w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-100"
          >
            Sign out
          </button>
        </div>
      )}
    </div>
  );
}

Best Practices

1. Always Check Loading State

Before rendering based on session data, check isPending:

const { data: session, isPending } = useSession();
 
if (isPending) {
  return <LoadingSpinner />;
}
 
// Now safe to check session
if (!session) {
  return <Navigate to="/sign-in" />;
}

2. Handle Null Sessions

The session can be null even after loading completes:

const { data: session } = useSession();
 
// ✅ Good - safely access nested properties
const orgName = session?.organization?.name || "No organization";
 
// ❌ Bad - will crash if session is null
const orgName = session.organization.name;

3. Cache Session Data

The useSession hook automatically caches session data. Don't create multiple instances of the auth client.

// ✅ Good - single auth client exported
// lib/auth-client.ts
export const authClient = createAuthClient({ ... });
export const { useSession } = authClient;
 
// ❌ Bad - creates new client instance
function MyComponent() {
  const client = createAuthClient({ ... });
  const { data } = client.useSession();
}

4. Use TypeScript

The SDK is fully typed. Use TypeScript to catch errors at compile time:

import { useSession } from "~/lib/auth-client";
 
export function Example() {
  const { data: session } = useSession();
  
  // TypeScript knows session.role can only be specific values
  if (session?.role === "owner") {
    // Owner-only logic
  }
}

Next Steps

UI Components

The @optare/optareid-react package includes pre-built UI components for common authentication flows.

<SignIn />

A complete sign-in form that handles email/password and social login.

import { SignIn } from '@optare/optareid-react';
 
export function LoginPage() {
  return (
    <div className="login-container">
      <SignIn
        onSuccess={(token) => console.log('Logged in!', token)}
        onError={(err) => console.error('Login failed', err)}
      />
    </div>
  );
}

<UserProfile />

Displays the current user's profile and a sign-out button.

import { UserProfile, useOptare } from '@optare/optareid-react';
 
export function Header() {
  const { auth } = useOptare();
  const user = { name: 'Alice', email: 'alice@example.com' }; // Get from auth.getMe()
 
  return (
    <header>
      <UserProfile
        user={user}
        onSignOut={() => auth.signOut()}
      />
    </header>
  );
}