Skip to main content
React Router 7 (formerly Remix): This guide covers React Router 7, which is the evolution of the Remix framework. If you’re migrating from Remix, the integration patterns are very similar.

Quick Start

Integrate Civic Auth into your React Router 7 application using the following steps (a working example is available in our github examples repo):
Important: Make sure your application is using React Router version ^7.0.0 or higher.
This guide assumes you are using TypeScript. Please adjust the snippets as needed to remove the types if you are using plain JS.

1. Create Auth Route Handlers

Create a catch-all route to handle all authentication-related requests. This route will manage login, logout, callback, and user data endpoints. Create app/routes/auth.$.tsx:
app/routes/auth.$.tsx
// Import from the package with the correct path using the index.ts file
// This ensures the debug initialization in the index.ts file is executed
import { createRouteHandlers } from "@civic/auth/react-router-7";

// Create route handlers with app config
const { createAuthLoader, createAuthAction, getUser, getAuthData } = createRouteHandlers({
  clientId: "demo-client-1"
});

/**
 * Catch-all loader for auth routes (GET requests)
 */
export const loader = createAuthLoader();

/**
 * Catch-all action for auth routes (POST requests)
 */
export const action = createAuthAction();

/**
 * Export getUser and getAuthData functions for use in other parts of the app
 */
export { getUser, getAuthData };

2. Set Up Root Loader

Configure your root route to provide user data to all routes in your application. This enables server-side rendering with authentication state. Update app/root.tsx:
app/root.tsx
import { Links, Meta, Outlet, Scripts, ScrollRestoration } from "react-router";
import type { LinksFunction, LoaderFunction } from "react-router";
import "./tailwind.css";

import { getAuthData } from "~/routes/auth.$";

export const links: LinksFunction = () => [
  { rel: "preconnect", href: "https://fonts.googleapis.com" },
  {
    rel: "preconnect",
    href: "https://fonts.gstatic.com",
    crossOrigin: "anonymous",
  },
  {
    rel: "stylesheet",
    href: "https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap",
  },
];

export const loader: LoaderFunction = async ({ request }) => {
  const authData = await getAuthData(request);

  return {
    // We bootstrap our data through the stack
    ...authData,
  };
};

function Layout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <head>
        <meta charSet="utf-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1" />
        <Meta />
        <Links />
      </head>
      <body>
        {children}
        <ScrollRestoration />
        <Scripts />
      </body>
    </html>
  );
}

export function ErrorBoundary() {
  return (
    <Layout>
      <div className="flex min-h-screen items-center justify-center">
        <div className="text-center">
          <h1 className="text-2xl font-bold text-gray-900">Something went wrong</h1>
          <p className="mt-2 text-gray-600">Please try refreshing the page</p>
        </div>
      </div>
    </Layout>
  );
}

export default function App() {
  return (
    <Layout>
      <Outlet />
    </Layout>
  );
}

Usage

Getting User Information on the Frontend

The React Router 7 integration provides several components and hooks for frontend authentication:

UserButton Component

The UserButton component provides a complete sign-in/sign-out experience:
app/routes/_index.tsx
import { useLoaderData } from "react-router";
import type { LoaderFunctionArgs, MetaFunction } from "react-router";
import { getUser } from "~/routes/auth.$";
import { UserButton } from "@civic/auth/react-router-7/components/UserButton";

export const meta: MetaFunction = () => {
  return [{ title: "My App" }];
};

export async function loader({ request }: LoaderFunctionArgs) {
  const user = await getUser(request);
  return { user };
}

export default function Index() {
  const { user } = useLoaderData<typeof loader>();

  return (
    <div>
      <h1>My App</h1>
      <UserButton
        onSignIn={(user) => console.log("User signed in", user)}
        onSignOut={() => console.log("User signed out")}
      />

      {user && (
        <div>
          <h2>Welcome, {user.name}!</h2>
          <p>Email: {user.email}</p>
        </div>
      )}
    </div>
  );
}

useUser Hook

For accessing user information in components:
MyComponent.tsx
import { useUser } from "@civic/auth/react-router-7";

export function MyComponent() {
  const { user, isLoggedIn } = useUser();

  if (!isLoggedIn) {
    return <div>Please sign in</div>;
  }

  return <div>Hello {user.name}!</div>;
}

Custom Authentication Logic

For custom sign-in buttons and authentication flows:
CustomSignIn.tsx
import { useCallback } from "react";
import { useUser } from "@civic/auth/react-router-7";

export function CustomSignIn() {
  const { signIn, signOut } = useUser();

  const doSignIn = useCallback(() => {
    console.log("Starting sign-in process");
    signIn()
      .then(() => {
        console.log("Sign-in completed successfully");
      })
      .catch((error) => {
        console.error("Sign-in failed:", error);
      });
  }, [signIn]);

  const handleSignOut = async () => {
    await signOut();
  };

  return (
    <div>
      <button onClick={doSignIn}>Sign In</button>
      <button onClick={handleSignOut}>Sign Out</button>
    </div>
  );
}

Protected Routes

React Router 7 supports route-level authentication using loaders. Here’s how to create protected routes that require user authentication:

Creating a Protected Route

Create a protected route loader that checks authentication and redirects unauthenticated users:
app/routes/protected._index.tsx
import { data, redirect, Outlet } from "react-router";
import type { LoaderFunctionArgs, MetaFunction } from "react-router";
import { getUser } from "./auth.$";

export const meta: MetaFunction = () => {
  return [{ title: "Protected Page - Civic Auth Demo" }];
};

export const loader = async ({ request }: LoaderFunctionArgs) => {
  const user = await getUser(request);

  if (!user) {
    // Redirect to home page if user is not authenticated
    return redirect("/");
  }

  return data({ user });
};

export default function ProtectedPage() {
  return (
    <div className="flex h-screen items-center justify-center">
      <Outlet />
    </div>
  );
}

Protected Route Layout

Create the protected route content that displays to authenticated users:
app/routes/protected.tsx
import { Link } from "react-router";
import type { MetaFunction } from "react-router";
import { useUser } from "@civic/auth/react-router-7/useUser";

export const meta: MetaFunction = () => {
  return [{ title: "Protected Page - Civic Auth Demo" }];
};

export default function ProtectedLayout() {
  const { user } = useUser();

  return (
    <div className="flex h-screen items-center justify-center text-white">
      <div className="flex flex-col items-center gap-8 p-8 border rounded-lg shadow-md">
        <h1 className="text-2xl font-bold">Protected Page</h1>

        <div className="text-center">
          <p className="mb-4">Welcome to this protected page, {user?.name || "User"}!</p>
          <p className="text-gray-600 mb-6">This content is only visible to authenticated users.</p>
        </div>

        <div className="flex gap-4">
          <Link to="/" className="px-4 py-2 rounded hover:bg-gray-300">
            Back to Home
          </Link>
          <Link to="/auth/logout" className="px-4 py-2 bg-red-600 text-white rounded hover:bg-red-700">
            Logout
          </Link>
        </div>
      </div>
    </div>
  );
}

How It Works

  1. Route Protection: The protected._index.tsx loader runs on the server before rendering the page
  2. Authentication Check: It uses getUser(request) to check if the user is authenticated
  3. Redirect Logic: If no user is found, it redirects to the home page using redirect("/")
  4. User Data: If authenticated, it passes the user data to the route
  5. Protected Content: The protected.tsx layout displays content only to authenticated users

Alternative Protection Patterns

You can also create a reusable protection utility:
app/utils/requireAuth.ts
import { redirect } from "react-router";
import { getUser } from "~/routes/auth.$";

export async function requireAuth(request: Request) {
  const user = await getUser(request);

  if (!user) {
    throw redirect("/auth/login");
  }

  // Additional admin-specific checks can go here
  // Example: Check if user has admin privileges (implement according to your needs)
  if (!isUserAdmin(user.email)) {
    throw redirect("/");
  }

  return user;
}
Then use it in any protected route:
app/routes/admin.tsx
import type { LoaderFunctionArgs } from "react-router";
import { requireAuth } from "~/utils/requireAuth";

export const loader = async ({ request }: LoaderFunctionArgs) => {
  const user = await requireAuth(request);

  return { user };
};

Advanced Configuration

Civic Auth is a “low-code” solution, so most configuration takes place via the dashboard. Changes you make there will be updated automatically in your integration without any code changes. You can customize the library according to your React Router 7 app’s needs. Configure options when calling createRouteHandlers:
app/routes/auth.$.tsx
import { createRouteHandlers } from "@civic/auth/react-router-7";

const { createAuthLoader, getUser } = createRouteHandlers({
  clientId: "YOUR_CLIENT_ID",
  loginSuccessUrl: "/myCustomSuccessEndpoint",
  callbackUrl: "/api/myroute/callback",
  logoutUrl: "/goodbye",
  baseUrl: "https://myapp.com",
});

Configuration Options

FieldRequiredDefaultExampleDescription
clientIdYes-2cc5633d-2c92-48da-86aa-449634f274b9The key obtained on signup to auth.civic.com
loginSuccessUrlNo-/myCustomSuccessEndpointIn a NextJS app, we will redirect your user to this page once the login is finished. If not set, users will be sent back to the root of your app.
callbackUrlNo/auth/callback/api/myroute/callbackIf you cannot host Civic’s SDK handlers in the default location, you can specify a custom callback route here. This is where you must attach Civic’s GET handler as described here, so Civic can complete the OAuth token exchange. Use loginSuccessUrl to redirect after login.
logoutUrlNo//goodbyeThe path your user will be sent to after a successful log-out.
baseUrlNo-https://myapp.comThe public-facing base URL for your application. Required when deploying behind reverse proxies (Cloudfront + Vercel, AWS ALB, nginx, etc.) to ensure authentication redirects use the correct public domain instead of internal origins.