Create a Remix application

Create a new Remix application using the official starter template. Run the following command to create a new Remix application:
npx create-remix@latest

Install @teamhanko/hanko-elements

The Hanko Elements package provides pre-built authentication components (hanko-auth, hanko-profile) and a JavaScript SDK. Install the package:
cd project-name
npm install @teamhanko/hanko-elements

Setup your Hanko project

Create a Hanko project in the cloud console to get your API URL. Go to the Hanko console and create a project for this application.
Set the APP URL to your development URL (typically http://localhost:5173/ for Remix).

Add the Hanko API URL

Add your Hanko API URL to your environment file. In Remix, environment variables without a public prefix are server-side only. Retrieve the API URL from the Hanko console and add it to your .env file:
.env
HANKO_API_URL=https://f4****-4802-49ad-8e0b-3d3****ab32.hanko.io
If you are self-hosting you need to provide the URL of your running Hanko backend.

Create client-side utilities

The @teamhanko/hanko-elements package only works client-side. Use Remix’s .client.ts file convention to ensure code only runs in the browser. Create a client-side utility file:
utils/hanko.client.ts
import { register, Hanko } from "@teamhanko/hanko-elements";

export { register, Hanko };

Create Hanko components

Create a components folder with two files: HankoAuth.tsx and HankoProfile.tsx.

Hanko Auth

The HankoAuth component provides login and registration functionality. It uses Remix’s loader to pass the API URL from server to client, and handles the onSessionCreated event for post-login navigation. For more information see the Auth Component documentation.
components/HankoAuth.tsx
import { useNavigate, useLoaderData } from "@remix-run/react";
import { Suspense, useCallback, useEffect, useState } from "react";
import { register, type Hanko } from "../utils/hanko.client";

export const loader = () => {
    return { hankoUrl: process.env.HANKO_API_URL }; 
};

const HankoAuth = () => {
  const [hanko, setHanko] = useState<Hanko>();
  const navigate = useNavigate();

  const data = useLoaderData<typeof loader>();
  const hankoUrl = data.hankoUrl || '';

  const redirectAfterLogin = useCallback(() => {
      navigate("/dashboard");//Path user gets navigated to once they log in
  }, [navigate]);

  useEffect(() => {
      if (hanko) {
          hanko.onSessionCreated(() => {
              // Successfully Logged In
              redirectAfterLogin();
          });
      }
  }, [hanko, redirectAfterLogin]);

  useEffect(() => {
      register(hankoUrl)
          .catch((error: Error) => {
              console.error(error.message);
          })
          .then((result) => {
              if (result) {
                  setHanko(result.hanko);
              }
          });
  }, [hankoUrl]);

  return (
      <div className="">
          <Suspense fallback={"Loading..."}>
              <hanko-auth />
          </Suspense>
      </div>
  );
};

export default HankoAuth;

Hanko profile

The HankoProfile component allows users to manage their email addresses and passkeys. For more information see the Profile Component documentation.
components/HankoProfile.tsx
import { useLoaderData } from "@remix-run/react";
import { Suspense, useEffect, } from "react";
import { register } from "../utils/hanko.client";

export const loader = () => {
    return { hankoUrl: process.env.HANKO_API_URL };
};

const HankoProfile = () => {
    const data = useLoaderData<typeof loader>();
    const hankoUrl = data.hankoUrl || '';

    useEffect(() => {
        register(hankoUrl).catch((error) => {
            // handle error
            console.log(error);
          });
    });

    return(
        <Suspense fallback={"Loading..."}>
            <hanko-profile />
        </Suspense>
    )
}
  
export default HankoProfile;

Setup your routes

Create your routes in the /routes folder. You’ll need _index.tsx for the login page and dashboard.tsx for the user dashboard. Create the main login route:
routes/_index.tsx
import HankoAuth from "../components/HankoAuth";

export const loader = () => {
  return { hankoUrl: process.env.HANKO_API_URL }; 
};

export default function Index() {
  return (
    <>
      <HankoAuth></HankoAuth>
    </>
  );
}
Create the dashboard route:
routes/dashboard.tsx
import HankoProfile from "../components/HankoProfile";

export const loader = () => {
  return { hankoUrl: process.env.HANKO_API_URL }; 
};

export default function Dashboard() {
  return (
    <>
      <HankoProfile></HankoProfile>
    </>
  );
}
By now you should be able to go to / to see the <HankoAuth>, and to /dashboard to see the <HankoProfile>. They should look something like this👇
sign upprofile page
If HankoAuth is not loading please restart the application as it might’ve not loaded the .env correctly the first time.

HankoProfile will only look like the picture while you are logged in.

Implement logout functionality

Create a logout button using the Hanko SDK’s logout method. Create LogoutButton.tsx:
components/LogoutButton.tsx
import { useLoaderData, useNavigate } from "@remix-run/react";
import { useEffect, useState, } from "react";
import { type Hanko, } from "../utils/hanko.client";


export const loader = () => {
    return { hankoUrl: process.env.HANKO_API_URL };
};

const LogoutButton = () => {

    const data = useLoaderData<typeof loader>();
    const hankoUrl = data.hankoUrl || '';

    const navigate = useNavigate();
    const [hanko, setHanko] = useState<Hanko>();
  
    useEffect(() => {
      import("@teamhanko/hanko-elements").then(({ Hanko }) =>
        setHanko(new Hanko(hankoUrl ?? ""))
      );
    }, []);

    const logout = async () => {
        try {
            await hanko?.logout();

            navigate("/");//Path the user will be redirected to once logged out
            
            return;
        } catch (error) {
            console.error("Error during logout:", error);
        }
    };

    return <button onClick={logout}>Logout</button>;
}
  
export default LogoutButton;

Customize component styles

You can customize the appearance of hanko-auth and hanko-profile components using CSS variables and parts. Refer to our customization guide.

Securing routes

Protect routes by validating session tokens in loader functions. Create a server-side authentication utility to handle session validation. Create app/services/auth.server.ts:
app/services/auth.server.ts
import { parse } from "cookie";

export async function ValidateCurrentSession(request: Request, hankoUrl: string){

    const cookies = parse(request.headers.get("Cookie") || "");
    const token = cookies.hanko;

    const validationOptions = { 
      method: 'POST',
      headers: {'Content-Type': 'application/json'},
      body: `{"session_token":"${token}"}`
    }
    try {
    const response = await fetch(hankoUrl + '/sessions/validate', validationOptions);

    if (!response.ok) throw new Error('Session validation failed');
    
    const verifiedResponse = await response.json();

    return verifiedResponse.is_valid
    
    } catch (error) {
    console.log(error)
    return false;
    }
}

Use the authentication service in your protected routes. Update your dashboard route to validate sessions:
app/routes/dashboard.tsx
import HankoProfile from "../components/HankoProfile";
import LogoutButton from "../components/LogoutButton";

import { type LoaderFunction, redirect } from "@remix-run/node";
import { ValidateCurrentSession } from "../services/auth.server";

export const loader: LoaderFunction = async ({request}) => {
  const hankoUrl = process.env.HANKO_API_URL || "";
  const validated = await ValidateCurrentSession(request , hankoUrl);

  if(!validated){
    return redirect("/");//Path to redirect to if user is not authenticated
  }

  return { hankoUrl: hankoUrl }; 
};

export default function Dashboard() {
  return (
    <>
      <HankoProfile></HankoProfile>
      <LogoutButton></LogoutButton>
    </>
  );
}

Try it yourself

Remix example

Full source code available on our GitHub