In this article, I will show you how you can safely backup your users data in your own database when using an Authentication Provider.
Why keep a backup in the first place? 🤔
Did you ever consciously think that when using an auth provider, you are essentially storing your users' information with them and you do not have access to the user data outside of them (not even in your own database)? 😳
The main purpose of Auth Providers is to abstract away the user authentication logic, but in doing so, you are also completely giving your users' data to them without retaining any control yourself.
What if a new intern joins your auth provider company and mistakenly deletes the production database? This is extremely rare, but the chances are not zero. Not only could they shut down their company, but you would also lose all your users' data. They might have some backup set up to prevent such scenarios, but you never know how things are implemented under the hood in another company.
Many of you don’t even think of this and get started with using one of the providers just because they are easier 🤷♂️ to get up and running.
If you are using one then I am pretty sure that you don’t even have a User table in your database. Did I guess right? 🤨
If this realization hits 🫠, then continue with the article as I show you how to safely keep a backup of the user’s data.
Setting up the Project with Kinde 🚀
ℹ️ If you already have a project that uses an Auth Provider, feel free to skip this section.
I will show you an example in a sample Next.js application with one of the popular auth provider called Kinde.
The steps are going to be fairly the same when using any other providers as well.
Run the following command to bootstrap a new Next.js application with Tailwind, Eslint and Typescript support:
bunx create-next-app@latest --tailwind --eslint --typescript
The above command uses bun as the package manager. If you don’t have it installed, you can go ahead with npm, pnpm or yarn.
Setting up Kinde Authentication
Make sure to have the necessary kinde package installed with the below command:
bun i @kinde-oss/kinde-auth-nextjs
Create a new project in Kinde and copy all the environment variables into .env
file in 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/dashboard
Note the KINDE_POST_LOGIN_REDIRECT_URL
variable. This variable ensures that once the user is authenticated in Kinde, they are redirected to the /dashboard
endpoint.
Make sure to change it according to your needs. Our code assumes that the user is redirected to /dashboard
once they successfully log in.
Now, we need to set up the Kinde Auth Router Handlers. In the app/app/api/auth/[kindeAuth]/route.ts
, add the following code:
import {handleAuth} from "@kinde-oss/kinde-auth-nextjs/server";
export const GET = handleAuth();
This will setup the necessary route handler to add Kinde Authentication to our application.
Setting up the Database Model 🛠️
ℹ️ I will be using MongoDB as the database and Prisma as the ORM. If you prefer any other Prisma alternatives like Drizzle or Mongoose, feel free to proceed with them.
Run the following command to install Prisma as a development dependency:
bun i prisma @prisma/client --save-dev
Now, initialize Prisma with the following command:
bunx prisma init
After you run this command, a new schema.prisma
file should be created in the prisma
folder at the root of your project.
Modify the schema.prisma
file to include a new User model. The fields in the model can vary based on the information your auth provider provides upon successful user creation.
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "mongodb"
url = env("DATABASE_URL")
}
model User {
id String @id @map("_id") @db.String
email String @unique
username String @unique
name String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
// If you are adding this change on top of your exising Prisma Schema,
// you will have the rest of your models here...
Now that we have our model ready, we need to push it to our database. For this, we need the connection URL.
If you already have a connection URL, that's great. If not, and you're following along, create a new cluster in MongoDB Atlas and obtain the database connection string. Then, add a new variable named DATABASE_URL
with the connection string value to the .env
file.
DATABASE_URL=<db-connection-string>
// Rest of the environment variables...
Now, we need to setup the PrismaClient
which we can use to query our database. Create a new file index.ts
in the /src/db
directory with the following lines of code:
import { PrismaClient } from "@prisma/client";
declare global {
// eslint-disable-next-line no-var
var cachedPrisma: PrismaClient;
}
let prisma: PrismaClient;
if (process.env.NODE_ENV === "production") prisma = new PrismaClient();
else {
if (!global.cachedPrisma) global.cachedPrisma = new PrismaClient();
prisma = global.cachedPrisma;
}
export const db = prisma;
In development environment, the code initializes PrismaClient
once and caches it globally to optimize resource usage. In production, it creates a new PrismaClient
instance per request.
Run the following command to push your changes in your schema to the database.
bunx prisma db push
Now, to have the updated types work in the IDE, run the following command to generate new types based on our updated schema.
bunx prisma generate
Now, this is all that we need to setup our application database and the authentication part.
Setting up the Backup 📥
ℹ️ Everything we've done up to this step is about setting up the basic project structure. In this section, we will look into the main logic of how to store user information once the user signs up for the first time in our application.
This is how the user data backup to our database architecture is going to look like:
Every time a new user signs up, they are redirected to the /dashboard
page. There, we check if the user exists in our database. If not, they are redirected to the /auth/callback
endpoint, and the user is created in our database. If they exist, the application continues as usual.
In the root page.tsx
file add these lines of code:
ℹ️ I am using Kinde as an auth provider. The code to check user authentication will vary depending on the one you are using but the logic should be the same. If you are following along, copy and paste this code.
"use client";
import {
LoginLink,
LogoutLink,
useKindeBrowserClient,
} from "@kinde-oss/kinde-auth-nextjs";
export default function Home() {
const { isAuthenticated } = useKindeBrowserClient();
return (
<main className="flex justify-center p-24">
{!isAuthenticated ? (
<LoginLink className="p-10 text-zinc-900 text-2xl font-semibold rounded-lg bg-zinc-100">
Log in
</LoginLink>
) : (
<LogoutLink className="p-10 text-zinc-900 text-2xl font-semibold rounded-lg bg-zinc-100">
Log out
</LogoutLink>
)}
</main>
);
}
This is the home page of our application, with only the login and logout buttons, based on whether the user is authenticated.
Once the user successfully logs in, they will be redirected to the /dashboard
page.
Add the following lines of code in the /app/dashboard/page.tsx
file:
import { db } from "@/db";
import { getKindeServerSession } from "@kinde-oss/kinde-auth-nextjs/server";
import { redirect } from "next/navigation";
const Page = async () => {
const { getUser } = getKindeServerSession();
const user = await getUser();
if (!user?.id) redirect("/api/auth/login");
const userInDB = await db.user.findUnique({
where: {
id: user.id,
},
});
if (!userInDB) redirect("/auth/callback");
return (
<div className="flex flex-col justify-center items-center sm:mt-36 w-full mt-20">
<h1 className="font-semibold text-zinc-900 text-2xl">
You are authenticated
</h1>
<p className="font-medium text-xl text-zinc-700 text-center">
The user has also been created in the DB
</p>
</div>
);
};
export default Page;
We check if the user is authenticated or not. If not, we redirect the user to the Kinde login page.
If they are authenticated but the user does not exist in the database, we redirect them to the /auth/callback
endpoint where a new user is created in our database with the current logged-in user details.
Add the following lines of code in the /src/app/auth/callback/page.tsx
:
import { getKindeServerSession } from "@kinde-oss/kinde-auth-nextjs/server";
import { redirect } from "next/navigation";
import { db } from "@/db";
const Page = async () => {
const { getUser } = getKindeServerSession();
const user = await getUser();
if (!user?.id || !user?.email) redirect("/");
const name =
user?.given_name && user?.family_name
? `${user.given_name} ${user.family_name}`
: user?.given_name || null;
const userInDB = await db.user.findFirst({
where: {
id: user.id,
},
});
if (!userInDB) {
await db.user.create({
data: {
id: user.id,
email: user.email,
...(name && { name }),
},
});
}
redirect("/dashboard");
};
export default Page;
Here, we check if the user is in our database. If they exist, we redirect them to the /dashboard
page. If the user does not exist, we create a new user in our database with their details and then redirect to the /dashboard
page.
That's it! 🎉 These steps ensure that the user details in the auth provider are synced with our database and are not only stored in the auth provider.
Wrap Up! ✨
By now, you have a general idea of how you can backup your user information in your own database when you are using an Authentication Provider.
The documented source code for this article is available here:
https://github.com/shricodev/blogs/tree/main/auth-callback-auth-provider
Thank you so much for reading! 🎉 🫡
Drop down your thoughts in the comment section below. 👇
Top comments (10)
Guys, let me know if you agree with this approach! You can apply it with any of the authentication providers nowadays.
If you are currently using an authentication provider, consider trying this approach to stay on the safer side.
With this approach, the most obvious headache of not having user data backup when using an authentication provider should be solved. 😉
I just built an admin dashboard using Jetstream. Curious how you feel about that in relation to Kindle?
Do you mean Kinde?
I've never had to use any authentication provider, but this article has completely captivated me. The way you started with the idea of 'why there's a need for a backup of user data when using an authentication provider' has to be one of the best few paragraphs I've read in dev.
All your articles feel less like articles and more like full of great humor. Such an engaging intro. Kudos @shricodev! 👏🏻
This is the best compliment I have received lately. Thank you so much @larastewart_engdev. ☺️ BTW, I am also starting out on Azure nowadays. Would love any tips from you.
But doing this will not make duplicacy in the data? should I start using the user data from my database or the auth provider now?
I have some projects which uses Clerk as the authentication provider in my github profile: github.com/shekhar_rajup23
Your question is completely valid. The idea is to continue working on your application as usual, using the user data from the authentication provider. However, use the database solely for user data storage as a backup.
@shekharrr , I was wondering the same thing. However, according to @shricodev 's reply in the thread, we are supposed to simply forgot the user data we have in our database. This now makes sense to me somehow.
We too have found this conceptually helpful.
Personally we run Clerk and are toying with Lucia (once it matures a bit). The webhooks to our DB for sync & backup is a nice touch. Thanks for the writeup!
Some comments may only be visible to logged-in visitors. Sign in to view all comments.