Install @teamhanko/hanko-elements

Once you’ve initialized your Remix app, installing hanko-elements provides you with access to the prebuilt components: hanko-auth and hanko-profile.

npm install @teamhanko/hanko-elements

Add the Hanko API URL

Retrieve the API URL from the Hanko console and place it in 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.

Importing and exporting Hanko functions in a .client.ts file

Since the @teamhanko/hanko-elements package is designed to work on the client side, you’ll need to import the Hanko functions into a .client.ts file. This file will then export them, making them available for use throughout your application.

utils/hanko.client.ts
import { register, Hanko } from "@teamhanko/hanko-elements";

export { register, Hanko };

Add <hanko-auth> component

The <hanko-auth> web component adds a login interface to your app. The following code example defines a HankoAuth component in the login route.

It uses the register function from hanko.client.ts to register <hanko-auth> component with the brower’s CustomElementRegistry. When the authentication flow is completed, a callback is set up to redirect the user to the dashboard.

routes/login.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");
    }, [navigate]);

    useEffect(() => {
        if (hanko) {
            hanko.onAuthFlowCompleted(() => {
                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>
    );
};

const Login = () => {
    return (
        <div>
            <HankoAuth />
        </div>
    )
}

export default Login

By now, your sign-up and sign-in features should be working. You should see an interface similar to this 👇

sign up

Add <hanko-profile> component

The <hanko-profile> component provides an interface, where users can manage their email addresses and passkeys.

app/routes/dashboard.tsx
import { useLoaderData, useNavigate } from "@remix-run/react";

import { Suspense, useEffect, useState } from "react";
import { type Hanko, register } from "~/utils/hanko.client";

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

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

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


    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-profile />
            </Suspense>
        </div>
    )
}

const Dashboard = () => {
    return (
        <div>
            <HankoProfile />
        </div>
    )
}

export default Dashboard

It should look like this 👇

profile page

Implement logout functionality

You can use @teamhanko/hanko-elements to easily manage user logouts. Below we add a logout button to the dashboard page.

app/routes/dashboard.tsx
import { useLoaderData, useNavigate } from "@remix-run/react";

import { Suspense, useEffect, useState } from "react";
import { type Hanko, register } from "~/utils/hanko.client";


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

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

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


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

    // here we define the logout function
    const logout = async () => {
        try {
            await hanko?.user.logout();
            navigate("/login");
            return;
        } catch (error) {
            console.error("Error during logout:", error);
        }
    };

    return (
        <div className="">
            <Suspense fallback={"Loading..."}>
                <hanko-profile />
            </Suspense>
            <div>
                <button onClick={logout}>Logout</button>
            </div>
        </div>
    )
}

const Dashboard = () => {
    return (
        <div>
            <HankoProfile />
        </div>
    )
}

export default Dashboard

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

To add JWT verification with Hanko in your Remix application, we’re using the jose library. However, you’re free to choose any other suitable library.

Below we define a validateJwtAndFetchUserId function which validates the JWT and fetches the user ID. This will also be used to protect routes and ensure only authenticated users can access certain parts of the application.

app/services/auth.server.ts
import { parse } from "cookie";
import { jwtVerify, createRemoteJWKSet } from "jose";

const hankoApiUrl = process.env.HANKO_API_URL;

export async function validateJwtAndFetchUserId(request: Request) {
  const cookies = parse(request.headers.get("Cookie") || "");
  const token = cookies.hanko;

  if (!token) {
    return null;
  }

  const JWKS = createRemoteJWKSet(
    new URL(`${hankoApiUrl}/.well-known/jwks.json`)
  );
  let payload;

  try {
    const verifiedJWT = await jwtVerify(token, JWKS);
    payload = verifiedJWT.payload;
  } catch {
    return null;
  }

  const userID = payload.sub;

  if (!userID) {
    return null;
  }

  return userID;
}

This is how you can use it in your routes:

app/routes/protected.tsx
import { type LoaderFunction, json, redirect } from '@remix-run/node';
import { validateJwtAndFetchUserId } from '../services/auth.server';

export let loader: LoaderFunction = async ({ request }) => {
    const userID = await validateJwtAndFetchUserId(request);

    if (!userID) {
        return redirect('/login');
    }

    return json({ userID });
};

export default function Protected() {
    return (
        <div>
            <h1>Protected</h1>
        </div>
    );
}

Try it yourself

Remix example

Full source code available on our GitHub