KeyloomKeyloom
Keyloom Auth is currently in beta. Feedback and contributions are welcome!
Getting started

UI Integration Patterns

General UI integration patterns for Keyloom - server-side session reading, client components, authentication flows, and design patterns.

UI Integration Patterns

General UI integration patterns for building authentication interfaces with Keyloom. These patterns work with any UI library or custom components.

Prerequisites

  • Keyloom handler configured at /api/auth/[...keyloom]
  • React components or Next.js pages
  • Optional: UI library (Tailwind CSS, Chakra UI, etc.)

Server-side session reading

Layout with session

app/layout.tsx (Next.js App Router)
import { getServerSession } from "@keyloom/nextjs";
import config from "@/keyloom.config";
import { NavBar } from "@/components/NavBar";

export default async function Layout({
  children,
}: {
  children: React.ReactNode;
}) {
  const session = await getServerSession(config);

  return (
    <html>
      <body>
        <NavBar user={session?.user} />
        <main>{children}</main>
      </body>
    </html>
  );
}

Protected page

app/dashboard/page.tsx
import { getServerSession } from "@keyloom/nextjs";
import { redirect } from "next/navigation";
import config from "@/keyloom.config";

export default async function DashboardPage() {
  const session = await getServerSession(config);

  if (!session) {
    redirect("/auth/signin");
  }

  return (
    <div>
      <h1>Welcome, {session.user.name}!</h1>
      <p>Email: {session.user.email}</p>
    </div>
  );
}

API route with session

app/api/profile/route.ts
import { getServerSession } from "@keyloom/nextjs";
import config from "@/keyloom.config";

export async function GET() {
  const session = await getServerSession(config);

  if (!session) {
    return Response.json({ error: "Unauthorized" }, { status: 401 });
  }

  return Response.json({
    user: session.user,
    sessionId: session.id,
  });
}

Authentication UI patterns

Sign-in page

app/auth/signin/page.tsx
import { SignInForm } from "@/components/SignInForm";

export default function SignInPage() {
  return (
    <div className="min-h-screen flex items-center justify-center">
      <div className="max-w-md w-full space-y-8">
        <div>
          <h2 className="text-3xl font-bold text-center">
            Sign in to your account
          </h2>
        </div>
        <SignInForm />
      </div>
    </div>
  );
}

Sign-in form component

components/SignInForm.tsx
export function SignInForm() {
  return (
    <div className="space-y-4">
      <a
        href="/api/auth/oauth/github/start?callbackUrl=/dashboard"
        className="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-gray-900 hover:bg-gray-800"
      >
        Continue with GitHub
      </a>

      <a
        href="/api/auth/oauth/google/start?callbackUrl=/dashboard"
        className="w-full flex justify-center py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50"
      >
        Continue with Google
      </a>

      <a
        href="/api/auth/oauth/microsoft/start?callbackUrl=/dashboard"
        className="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700"
      >
        Continue with Microsoft
      </a>
    </div>
  );
}
components/NavBar.tsx
import type { User } from "@keyloom/core";

interface NavBarProps {
  user?: User | null;
}

export function NavBar({ user }: NavBarProps) {
  return (
    <nav className="bg-white shadow">
      <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
        <div className="flex justify-between h-16">
          <div className="flex items-center">
            <h1 className="text-xl font-semibold">My App</h1>
          </div>

          <div className="flex items-center space-x-4">
            {user ? (
              <>
                <span>Welcome, {user.name}</span>
                <a
                  href="/api/auth/signout?callbackUrl=/"
                  className="text-gray-500 hover:text-gray-700"
                >
                  Sign out
                </a>
              </>
            ) : (
              <a
                href="/auth/signin"
                className="text-blue-600 hover:text-blue-800"
              >
                Sign in
              </a>
            )}
          </div>
        </div>
      </div>
    </nav>
  );
}

Client-side components

Profile component with hooks

components/Profile.tsx
"use client";

import { useSession } from "@keyloom/react";

export function Profile() {
  const { data: session, status } = useSession();

  if (status === "loading") {
    return <div className="animate-pulse">Loading...</div>;
  }

  if (status === "unauthenticated") {
    return (
      <div className="text-center">
        <p>Please sign in to view your profile</p>
        <a href="/auth/signin" className="text-blue-600 hover:underline">
          Sign in
        </a>
      </div>
    );
  }

  return (
    <div className="bg-white shadow rounded-lg p-6">
      <div className="flex items-center space-x-4">
        {session.user.image && (
          <img
            src={session.user.image}
            alt="Profile"
            className="w-16 h-16 rounded-full"
          />
        )}
        <div>
          <h2 className="text-xl font-semibold">{session.user.name}</h2>
          <p className="text-gray-600">{session.user.email}</p>
        </div>
      </div>
    </div>
  );
}

Organization switcher (RBAC)

components/OrgSwitcher.tsx
"use client";

import { useRBAC } from "@keyloom/react";

export function OrgSwitcher() {
  const { activeOrg, organizations, switchOrg } = useRBAC();

  if (!organizations?.length) return null;

  return (
    <div className="relative">
      <select
        value={activeOrg?.id || ""}
        onChange={(e) => switchOrg(e.target.value)}
        className="block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500"
      >
        {organizations.map((org) => (
          <option key={org.id} value={org.id}>
            {org.name}
          </option>
        ))}
      </select>
    </div>
  );
}

Design patterns

Loading states

components/LoadingSpinner.tsx
export function LoadingSpinner() {
  return (
    <div className="flex justify-center items-center p-4">
      <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
    </div>
  );
}

Error boundaries

components/AuthErrorBoundary.tsx
"use client";

import { useEffect, useState } from "react";

interface AuthErrorBoundaryProps {
  children: React.ReactNode;
  fallback?: React.ReactNode;
}

export function AuthErrorBoundary({
  children,
  fallback,
}: AuthErrorBoundaryProps) {
  const [hasError, setHasError] = useState(false);

  useEffect(() => {
    const handleError = (event: ErrorEvent) => {
      if (event.error?.code?.includes("AUTH")) {
        setHasError(true);
      }
    };

    window.addEventListener("error", handleError);
    return () => window.removeEventListener("error", handleError);
  }, []);

  if (hasError) {
    return (
      fallback || (
        <div className="text-center p-8">
          <h2 className="text-xl font-semibold text-red-600">
            Authentication Error
          </h2>
          <p className="mt-2 text-gray-600">
            Please try refreshing the page or signing in again.
          </p>
          <button
            onClick={() => setHasError(false)}
            className="mt-4 px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700"
          >
            Retry
          </button>
        </div>
      )
    );
  }

  return <>{children}</>;
}

Performance tips

  • Use server-side session reading for initial page loads
  • Implement proper loading states for better UX
  • Cache user profile data appropriately
  • Use React.memo for expensive components

Accessibility

  • Include proper ARIA labels on authentication buttons
  • Ensure keyboard navigation works
  • Provide screen reader friendly loading states
  • Use semantic HTML elements

See also

Next steps

  • Build your authentication UI components
  • Implement proper error handling and loading states
  • Add responsive design for mobile users
  • Test accessibility with screen readers

How is this guide?