Using Hanko Auth with Supabase and Prisma

In this guide, you'll learn how you can use Hanko with Supabase as your Database along with Prisma. It focuses on how to add new user info, like user ID and email to the Supabase Database, when they sign up.

Let's kick things off by setting up a new Next.js 14 project.

pnpm dlx create-next-app@latest

After cleaning up the boilerplate code, it's now time to add authentication to your app, for that we'll be using Hanko. So, let's begin.

Setup Hanko

Install @teamhanko/hanko-elements

pnpm add @teamhanko/hanko-elements

Add the Hanko API URL to .env file

Retrieve the API URL from the Hanko console and place it in your .env file.

NEXT_PUBLIC_HANKO_API_URL=https://f4****-4802-49ad-8e0b-3d3****ab32.hanko.io

Add <hanko-auth> Component

"use client";

import { useEffect, useCallback, useState } from "react";
import { useRouter } from "next/navigation";
import { register, Hanko } from "@teamhanko/hanko-elements";

const hankoApi = process.env.NEXT_PUBLIC_HANKO_API_URL;

export default function HankoAuth() {
  const router = useRouter();

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

  useEffect(() => {
    import("@teamhanko/hanko-elements").then(({ Hanko }) =>
      setHanko(new Hanko(hankoApi))
    );
  }, []);

  const redirectAfterLogin = useCallback(() => {
    // successfully logged in, redirect to a page in your application
    router.replace("/dashboard");
  }, [router]);

  useEffect(
    () =>
      hanko?.onAuthFlowCompleted(() => {
        redirectAfterLogin();
      }),
    [hanko, redirectAfterLogin]
  );

  useEffect(() => {
    register(hankoApi).catch((error) => {
      // handle error
    });
  }, []);

  return <hanko-auth />;
}

Add <hanko-profile> component

"use client"

import { useEffect } from "react";
import { register } from "@teamhanko/hanko-elements";

const hankoApi = process.env.NEXT_PUBLIC_HANKO_API_URL;

export default function HankoProfile() {
  useEffect(() => {
    register(hankoApi).catch((error) => {
      // handle error
    });
  }, []);

return <hanko-profile />;
}

Create a Logout Button

"use client";

import { useState, useEffect } from "react";
import { useRouter } from "next/navigation";
import { Hanko } from "@teamhanko/hanko-elements";

const hankoApi = process.env.NEXT_PUBLIC_HANKO_API_URL;

export function LogoutBtn() {
  const router = useRouter();
  const [hanko, setHanko] = useState<Hanko>();

  useEffect(() => {
    import("@teamhanko/hanko-elements").then(({ Hanko }) =>
      setHanko(new Hanko(hankoApi ?? ""))
    );
  }, []);

  const logout = async () => {
    try {
      await hanko?.user.logout();
      router.push("/login");
      router.refresh();
      return;
    } catch (error) {
      console.error("Error during logout:", error);
    }
  };

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

That's it, you've now authentication added to your app along with user management. Navigate to our detailed Next.js guide for more insights.

Setup Supabase

Create an account on Supabase

Sign up on Supabase, if you haven't yet.

Create a new project

Once you've created an account, you'll be redirected to the dashboard. Click on 'New Project' to create a new project.

Create users table

Now, in the project dashboard, choose 'Database' and create a new 'users' table. We'll be using this table to add new user info, on signup. Along with the default 'id' column, add 'user_id' and 'email' columns.

Get URL , service_role key and Connection String from Supabase and add it to your project

Now in the project dashboard, select 'Settings' from the left sidebar and then click on 'API'. Copy the 'URL' and 'service_role' key and paste them into your .env.local file.

To get the connection string, navigate to 'Database' in the left sidebar, and choose the 'URI' option from connection string, copy it and paste as 'DATABASE_URL' in .env. Replace [YOUR-PASSWORD] with your DB password. You'll need this when working with Prisma.

This is how the .env looks now.

NEXT_PUBLIC_HANKO_API_URL=https://f4****-4802-49ad-8e0b-3d3****ab32.hanko.io

NEXT_PUBLIC_SUPABASE_PROJECT_URL=YOUR_SUPABASE_PROJECT_URL
NEXT_PUBLIC_SUPABASE_SERVICE_ROLE_KEY=YOUR_SUPABASE_SERVICE_ROLE_KEY

DATABASE_URL=YOUR_CONNECTION_STRING

Install and setup Supabase JavaScript Client

Install Supabase to your app.

pnpm add @supabase/supabase-js

Now, its time to set it up. In order to make it reusable, create a lib folder and add this inside of supabase.ts file.

import { createClient } from "@supabase/supabase-js";

export const supabase = createClient(
  process.env.NEXT_PUBLIC_SUPABASE_PROJECT_URL!,
  process.env.NEXT_PUBLIC_SUPABASE_SERVICE_ROLE_KEY!
);

Setup Prisma

Install Prisma

pnpm add prisma --save-dev

Invoke Prisma CLI

pnpm dlx prisma

Next, set up your Prisma project by creating your Prisma schema file template with the following command

pnpm prisma init

Install and setup Prisma Client

pnpm add @prisma/client

Similar to Supabase Client, to make Prisma Client available globally be, create a `prisma.ts` file inside of lib folder and add the code there.

import { PrismaClient } from "@prisma/client";

declare global {
  // eslint-disable-next-line no-var
  var prisma: PrismaClient | undefined;
}

let prisma: PrismaClient;

if (typeof window === "undefined") {
  if (process.env.NODE_ENV === "production") {
    prisma = new PrismaClient();
  } else {
    if (!global.prisma) {
      global.prisma = new PrismaClient();
    }

    prisma = global.prisma;
  }
}
// @ts-ignore
export default prisma;

Bring everything together

Create User Model

In your schema.prisma, define the User model to manage user data. It includes an auto-incrementing id, a unique userId, an optional email, and timestamps for createdAt and updatedAt. Map this model to the 'users' table in your database.


generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

model User {
  id      Int     @id @default(autoincrement())
  userId  String  @unique @map(name: "user_id")
  email   String?

  createdAt DateTime @default(now()) @map(name: "created_at")
  updatedAt DateTime @updatedAt @map(name: "updated_at")

  @@map(name: "users")
}

To test if everything works correctly, run the following command to create a migration

pnpm prisma migrate dev --name init

If everything works, you should now see this

Your database is now in sync with your schema.

Time to get back to code, now you'll create an api to add new user info to the 'users' table. Start by creating a folder named api, then inside it, make a subfolder called create-user. In this subfolder, create a file named route.ts and add the following code to it.

import { NextResponse } from 'next/server'

import prisma from '@/lib/prisma'

export async function POST(req: Request, res: Response) {
  try {
    const user = await req.json()

    if (!user) {
      return new NextResponse('Unauthorized', { status: 401 })
    }

    let supauser

    try {
      supauser = await prisma.user.findUnique({
        where: {
          userId: user.id,
        },
      })
      if (!supauser) {
        supauser = await prisma.user.create({
          data: {
            userId: user.id,
            email: user.email,
          },
        })
      }
    } catch (error) {
      console.error(error)
    }

    const user_data = {
      ...user,
      id: supauser?.id,
    }

    return new NextResponse(JSON.stringify(user_data), { status: 200 })
  } catch (error) {
    console.error('[CREATEUSER_ERROR]', error)
    return new NextResponse('Internal Error', { status: 500 })
  }
}

Now, time to modify HankoAuth component to user the above API as soon as onAuthFlowCompleted is triggered.

"use client";

import { useEffect, useCallback, useState } from "react";
import { useRouter } from "next/navigation";
import { register, Hanko } from "@teamhanko/hanko-elements";

const hankoApi = process.env.NEXT_PUBLIC_HANKO_API_URL || "";

export default function HankoAuth() {
const router = useRouter();

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

useEffect(() => {
    import("@teamhanko/hanko-elements").then(({ Hanko }) =>
    setHanko(new Hanko(hankoApi))
    );
}, []);

const redirectAfterLogin = useCallback(() => {
    // successfully logged in, redirect to a page in your application
    router.replace("/dashboard");
}, [router]);

useEffect(
    () =>
    hanko?.onAuthFlowCompleted(async () => {
        // here we use the api we created to add a new user to users table
        const user = await hanko.user.getCurrent();

        const fetchData = async () => {
        if (!user) {
            console.error("No user data");
            return;
        }
        try {
            const response = await fetch("/api/create-user", {
            method: "POST",
            body: JSON.stringify(user),
            });

            if (!response.ok)
            throw new Error(`HTTP error! status: ${response.status}`);
        } catch (error) {
            console.error("Fetch Error: ", error);
        }
        };
        await fetchData();
        redirectAfterLogin();
    }),
    [hanko, redirectAfterLogin]
);

useEffect(() => {
    register(hankoApi).catch((error) => {
    // handle error
    });
}, []);

return <hanko-auth />;
}

That's all! With everything set up, every time a new user signs up, their details will automatically appear in your Supabase dashboard's table. To see this in action and experiment further, check out this repository and give it a try yourself.

Did you find this article valuable?

Support Ashutosh Bhadauriya by becoming a sponsor. Any amount is appreciated!