Set up a Next.js app with Drizzle ORM and Kinde Auth

By Peter Phanouvong — Published

Drizzle is a powerful, open-source ORM that we can’t get enough of at Kinde, so we put together this kit to show you how to get the two working together. Between Next.js, Drizzle, and Kinde you’ll have a powerful back end to get your application started.

This guide will show you how to set up a Next.js app with Drizzle and Kinde Auth.

We will cover the following:

  • Setting up a Next.js app with TypeScript and Tailwind
  • Integrating Kinde with the Next.js app to handle user authentication
  • Saving user data to a local Sqlite3 database with Drizzle

If you ever see this pointing finger emoji “👉” it means there is something for you to do.

Setting up a Next.js app

Link to this section

Setting up a Next.js app with TypeScript and Tailwind. You can find the official documentation for setting up a Next.js app in the Next.js Installation Guide.

create-next-app

Link to this section

The Next.js team recommends creating a new Next.js app using create-next-app, which sets up everything automatically for you.

👉 Run the following in your terminal.

npx create-next-app@latest

On installation, you will see the following prompts:

What is your project named? my-app
Would you like to use TypeScript? No / Yes
Would you like to use ESLint? No / Yes
Would you like to use Tailwind CSS? No / Yes
Would you like to use `src/` directory? No / Yes
Would you like to use App Router? (recommended) No / Yes
Would you like to customize the default import alias? No / Yes

👉 Keep pressing Enter or Return and you will have a Next.js app setup with TypeScript and Tailwind

You can now get your app up and running.

👉 Navigate to the project directory and run:

npm run dev

You should see something like this:

Integrating Kinde

Link to this section

Integrating Kinde with the Next.js app to handle user authentication. You can find the official documentation for this setup on the Kinde Next.js App Router SDK page.

👉 Run the following in your terminal.

npm i @kinde-oss/kinde-auth-nextjs

Setting up your Kinde app

Link to this section

👉 Create a Kinde account. You can register here - https://app.kinde.com/register

👉 Once logged in, head to Settings > Applications > [Your app] > View details and add your callback URLs. It should look like this:

  • Allowed callback URLs: http://localhost:3000/api/auth/kinde_callback

  • Allowed logout redirect URLs: http://localhost:3000

👉 Press the “Save” button.

Configuring your Next.js app with the Kinde SDK

Link to this section

Environment variables

Link to this section

We need to set some environment variables to connect our Next.js app to our Kinde app.

👉 Create a .env file in the root directory of your project.

KINDE_CLIENT_ID=<your_kinde_client_id>
KINDE_CLIENT_SECRET=<your_kinde_client_secret>
KINDE_ISSUER_URL=https://<your_kinde_subdomain>.kinde.com

KINDE_SITE_URL=http://localhost:3000
KINDE_POST_LOGOUT_REDIRECT_URL=http://localhost:3000
KINDE_POST_LOGIN_REDIRECT_URL=http://localhost:3000

👉 Replace the placeholders with your actual client ID, client secret and issuer URL.

You can find these on the Application page on Kinde.

For these .env file variables to take effect, make sure to restart your dev server by stopping the server in your terminal and then re-running npm run dev

To get Kinde Auth working with our app, we will need to set some authentication endpoints within our app.

👉 Create the file src/app/api/auth/[kindeAuth]/route.ts .

// route.ts

import {handleAuth} from "@kinde-oss/kinde-auth-nextjs/server";

export const GET = handleAuth();

Testing it out

Link to this section

Now let’s register, login and logout!

We are going to use getUser and isAuthenticated from the getKindeServerSession util function provided by Kinde to see the current user’s authentication status.

Kinde also provides LoginLink, RegisterLink and LogoutLink which can be used to facilitate authentication in your app.

👉 Update app/page.tsx to look something like this:

import {
    RegisterLink,
    LoginLink,
    getKindeServerSession,
    LogoutLink
} from "@kinde-oss/kinde-auth-nextjs/server";

export default function Home() {
    const {getUser, isAuthenticated} = getKindeServerSession();
    const user = await getUser();

    return (
        <main className="flex min-h-screen flex-col items-center justify-between p-24">
            {isAuthenticated() ? (
                <div>
                    <p className="mb-8">Well, well, well, it isn&apos;t...</p>
                    <p className="font-medium text-lg">
                        {user.given_name} {user.family_name}
                    </p>
                    <pre className="mt-4 bg-slate-950 text-cyan-200 p-4 font-mono text-sm rounded-sm">
                        {JSON.stringify(user, null, 2)}
                    </pre>
                    <LogoutLink className="inline-block mt-8 underline text-blue-500">
                        Logout
                    </LogoutLink>
                </div>
            ) : (
                <div className="mb-32 grid text-center lg:mb-0 lg:grid-cols-4 lg:text-left">
                    <LoginLink className="group rounded-lg border border-transparent px-5 py-4 transition-colors hover:border-gray-300 hover:bg-gray-100 hover:dark:border-neutral-700 hover:dark:bg-neutral-800/30">
                        <h2 className={`mb-3 text-2xl font-semibold`}>
                            Login{" "}
                            <span className="inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none">
                                -&gt;
                            </span>
                        </h2>
                        <p className={`m-0 max-w-[30ch] text-sm opacity-50`}>
                            Log into an existing Lumon account.
                        </p>
                    </LoginLink>

                    <RegisterLink className="group rounded-lg border border-transparent px-5 py-4 transition-colors hover:border-gray-300 hover:bg-gray-100 hover:dark:border-neutral-700 hover:dark:bg-neutral-800/30">
                        <h2 className={`mb-3 text-2xl font-semibold`}>
                            Register{" "}
                            <span className="inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none">
                                -&gt;
                            </span>
                        </h2>
                        <p className={`m-0 max-w-[30ch] text-sm opacity-50`}>
                            Create a new Lumon account.
                        </p>
                    </RegisterLink>
                </div>
            )}
        </main>
    );
}

You should now be able to register, login, and logout of your app!

Saving user data to a local Sqlite3 database with Drizzle.

You can find the official documentation for what I am going to cover in the DrizzleORM docs.

👉 Run this in your terminal:

npm install drizzle-orm better-sqlite3
npm i -D drizzle-kit

Setting up the database

Link to this section

👉 Create the file db/schema.ts in the root directory - in this file you can define your database schema:

// db/schema.ts

import {sqliteTable, text, integer} from "drizzle-orm/sqlite-core";

export const users = sqliteTable("users", {
    id: text("id").primaryKey(),
    firstName: text("first_name"),
    lastName: text("last_name"),
    email: text("email")
});

// OR ALTERNATIVELY
// export const users = sqliteTable("users", {
//	 id: integer("id").primaryKey(),
//   kindeId: text("kinde_id"),
//   firstName: text("first_name"),
//   lastName: text("last_name"),
//   email: text("email"),
// });

Here I have set up a users table which we will populate later.

The reason why I am using text for the id is because Kinde provides an id string rather than an integer. Instead of just one ID, you could also track a kindeId as well as your normal id

👉 Create the file db/index.ts in the root directory - in this file, we will set up and sqlite3 database and connect it to Drizzle.

// db/index.ts

import {drizzle, BetterSQLite3Database} from "drizzle-orm/better-sqlite3";
import Database from "better-sqlite3";
import {migrate} from "drizzle-orm/better-sqlite3/migrator";

const sqlite = new Database("sqlite.db");
export const db: BetterSQLite3Database = drizzle(sqlite);

migrate(db, {migrationsFolder: "drizzle"});

👉 Create the file drizzle.config.ts in the root directory.

// drizzle.config.ts

import type {Config} from "drizzle-kit";

export default {
    schema: "./db/schema.ts",
    out: "./drizzle",
    driver: "better-sqlite",
    dbCredentials: {
        url: "sqlite.db"
    }
} satisfies Config;

👉 Update tsconfig.json - change the “target” from es5 to es6. This is because Drizzle does not yet support es5.

// tsconfig.json

{
  "compilerOptions": {
    "target": "es6",
    ...

👉 Get the database up and running by running this in your terminal:

npx drizzle-kit generate:sqlite

You should see this:

❯ npx drizzle-kit generate:sqlite
drizzle-kit: v0.19.5
drizzle-orm: v0.27.2

No config path provided, using default 'drizzle.config.ts'
Reading config file '/Users/xxx/drizzle-1/drizzle.config.ts'
2 tables
songs 3 columns 0 indexes 0 fks
users 4 columns 0 indexes 0 fks

To explore the data in your database you can run npx drizzle-kit studio

Saving users to the database

Link to this section

On successful login, we are going to check if the user coming back to us from Kinde is in our local database. If not, we will add them to the local database.

👉 Update the .env file:

KINDE_POST_LOGIN_REDIRECT_URL=http://localhost:3000/api/auth/success

After login, Kinde will redirect to our app’s API route at /api/auth/success

Make sure to restart the dev server after updating the environment variables.

👉 Create the API route /api/auth/success/route.ts file:

import {db} from "@/db";
import {users} from "@/db/schema";
import {getKindeServerSession} from "@kinde-oss/kinde-auth-nextjs/server";
import {eq} from "drizzle-orm";
import {NextResponse} from "next/server";

export async function GET() {
    // check if user exists
    const {getUser, getOrganization} = getKindeServerSession();
    const user = await getUser();
    const {orgCode} = await getOrganization();

    if (!user || user == null || !user.id)
        throw new Error("something went wrong with authentication" + user);

    const dbUser = db.select().from(users).where(eq(users.id, user.id)).get();
    // or const dbUser = await db.select().from(users).where(eq(users.id, user.id));

    if (!dbUser) {
        db.insert(users)
            .values({
                id: user.id,
                firstName: user.given_name,
                lastName: user.given_name,
                email: user.email
            })
            .run();

        // or
        // await db.insert(users)
        // .values({
        //   id: user.id,
        //   firstName: user.given_name,
        //   lastName: user.given_name,
        //   email: user.email,
        // });
    }

    return NextResponse.redirect("http://localhost:3000/");
}

Now if you login or register, your database should get populated with those users!

To double check, you can check the data in your database by running npx drizzle-kit studio - look out for the new users in the users table

That’s all you need to get set up with Next.js, Drizzle and Kinde.

In the future, I’ll cover some interesting use cases for this set up, like multi-tenancy and roles and permissions. For now, let us know how you go on Twitter, and if you have any issues contact support@kinde.com or post them in our community.