Migrating NextAuth V3 to V4 with Prisma

How to migrate your NextAuth database with Prisma to the latest and greatest

NextAuth (github, website) is an open source library for managing your user authentication and authorization flow. It has a number of plugins including Google Auth, Auth0, Github and user/password login where the data is stored in a database. Using it with Prisma is a breeze because there is a Prisma adapter. After making the necessary changes to your Prisma schema file and performing a migration, you will be ready to add user login functionality to the frontend of your website.

If you are interested in how to set up a new project, NextAuth has a guide here that follows all the steps that you need.

The purpose of this article is to walk through a particular problem I had when I updated Source Compare, with the latest versions of NextAuth (v3->v4) and Prisma(v2->v3). Source Compare is a collaborative universal image version control app that I created for managing web assets. The problem was that the Prisma migration was attempting to drop the entire user's database because it was changing from users to User. With this guide that I highlight below, I was able to safely migrate everything over to User without losing any records.

Migrate to Prisma v3

The upgrade guide for Prisma v3 was very helpful at identifying what needed to be changed. First I installed the latest packages:

npm install prisma@3 @prisma/client@3

When looking through the upgrade guide, I primarily needed to be concerned about the referential actions for my project. In older versions of Prisma v2, when you'd delete something you'd get an error if there were child records associated with the item that you were trying to delete. I actually had a fairly nested set of models for Source Compare, projects -> items -> commits -> images and these limitations meant that in earlier versions of Prisma v2, when deleting a project, I'd first delete all the images, then commits, all the way up the hierarchy until I finally got to projects. This was a little bit of an inconvenience but it had the advantage that the potential issues I might have run into had I relied on the cascading behavior from later versions of Prisma v2 didn't apply. This meant I was able to update to Prisma v3 without needing to rework my code, but if you find that you need to make changes you will find that they center around defining what the referential actions should be for each of your models (SetNull vs OnDelete). Now that referential actions exist though, I'll be sure to later go back and update these messy calls I had to do with simple cascading delete functionality that Prisma now has.

Migrate NextAuth v4

Now that Prisma has been straightened out, we need to move onto NextAuth v4. This proved to be significantly more challenging, so before going into the details, for those who are nervous about this upgrade, there is a legacy adapter that you can always use which will maintain the database schema that you already have. For myself, I wanted to make sure I was using the latest and greatest because it has been noted that the legacy adapter will not receive future updates. Unfortunately, the updated Prisma adapter has a number of fields and tables that have been changed so we will need to perform several Prisma migrations in order to make sure everything is updated properly.

What to do if you run into problems

Before we actually change anything, first a note about the importance of backing up your production database before doing anything. To help put you in the mindset of this process not being perfectly seamless initially, I'm placing this problems section first because it's super important that you read it before you make any potentially destructive changes to your database.

You should always test this set of steps on a clone of your production database locally. This means you should run a sql dump of your database, copy it locally, restore it and update your prisma connection strings to your local instance. Do not test in production without making backups. Also, the changes you need to make should be roughly the same as what I highlight below but due to other differences in your schema, you can't just copy and paste the models that are listed below. Instead, you will need to integrate them with your models.

For the first pass of this, my preference would be to actually start with a clean database where the migrations have been applied up to the two that we discuss here. Then create a user and do some sample actions with that user so you create records for the different models in your schema. Next, you can attempt to do the two migrations below and see if it works smoothly. If it doesn't, stash your changes, and then reset your git commit back to the state before you applied the migrations. You can then repeat creating the user and records again, tweak the migration and try it again.

It took me a number of tries to allow the migrations to cleanly apply. I knew I was in trouble when the Prisma migration was asking me if I wanted to drop tables or fields I wasn't comfortable with. I'm forgetting at the moment how it actually would prompt you about migrations where the names changed- if you are finding that it is erroneously stating that it will drop the tables or fields even though you are actually renaming them based on the migration commands then you'll know you can ignore those.

Once you can cleanly apply the two migrations below to your test data, you can then restore your production database locally and try the same thing. If that works as well, you can then attempt to migrate the production database. Since you have a backup from running the database dump, you can always restore that if you run into problems.

Back to NextAuth v4 migration

Now that we've done all the preparation needed to have a proper backup we can attempt the NextAuth v4 migration. These updates are nice because they standardize the database names used so that they have the same capitalization and pluralization scheme that Prisma uses. The problem is that when you make the changes to the Prisma schema, you will get warnings that rather than changing the names, Prisma will actually drop and recreate the tables and fields. Yikes!

A way around destructively dropping your tables and fields is to create your own custom migration. You can do this by running the following command:

prisma migrate dev --create-only

This will allow you to create a migration that's empty. You can make the migration something like "changed user table names". After creating the migration, I filled in the sql file with the following:


ALTER TABLE "accounts" RENAME TO "Account";

ALTER TABLE "sessions" RENAME TO "Session";

ALTER TABLE "Account" RENAME CONSTRAINT "accounts_pkey" TO "Account_pkey";

ALTER TABLE "Session" RENAME CONSTRAINT "sessions_pkey" TO "Session_pkey";

ALTER TABLE "User" RENAME CONSTRAINT "users_pkey" TO "User_pkey";

ALTER TABLE "Account" RENAME COLUMN "user_id" TO "userId";
ALTER TABLE "Account" RENAME COLUMN "provider_id" TO "provider";
ALTER TABLE "Account" RENAME COLUMN "provider_type" TO "type";
ALTER TABLE "Account" RENAME COLUMN "access_token_expires" TO "expiresAt";
ALTER TABLE "Account" RENAME COLUMN "provider_account_id" TO "providerAccountId";

ALTER TABLE "Session" RENAME COLUMN "user_id" TO "userId";
ALTER TABLE "Session" RENAME COLUMN "session_token" TO "sessionToken";

ALTER TABLE "User" RENAME COLUMN "email_verified" TO "emailVerified";

Then after saving that migration file, I went ahead and made the changes to the prisma schema that they outline here:

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

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

model Account {
  id                 String  @id @default(cuid())
  userId             String
  type               String
  provider           String
  providerAccountId  String
  refresh_token      String?  @db.Text
  access_token       String?  @db.Text
  expires_at         Int?
  token_type         String?
  scope              String?
  id_token           String?  @db.Text
  session_state      String?

  user User @relation(fields: [userId], references: [id], onDelete: Cascade)

  @@unique([provider, providerAccountId])

model Session {
  id           String   @id @default(cuid())
  sessionToken String   @unique
  userId       String
  expires      DateTime
  user         User     @relation(fields: [userId], references: [id], onDelete: Cascade)

model User {
  id            String    @id @default(cuid())
  name          String?
  email         String?   @unique
  emailVerified DateTime?
  image         String?
  accounts      Account[]
  sessions      Session[]

model VerificationToken {
  identifier String
  token      String   @unique
  expires    DateTime

  @@unique([identifier, token])

These schema details are in addition to all of the other schema details that you've added, so in my case I have other parameters in my user model and have other models such as Project that aren't shown here for simplicity.

We've now created one migration that we haven't run and made additional changes up and above those sql changes by editing the Prisma schema file. To incorporate those new Prisma schema changes, we need to create a second migration to incorporate those changes in properly. Run:

prisma migrate dev --create-only

This should apply all those changes into a new migration. Mine created the following sql file:

DROP INDEX "accounts.compound_id_unique";

DROP INDEX "providerAccountId";

DROP INDEX "providerId";

DROP INDEX "userId";

DROP INDEX "sessions.access_token_unique";

ALTER TABLE "Account" DROP COLUMN "compound_id",
DROP COLUMN "created_at",
DROP COLUMN "expiresAt",
DROP COLUMN "updated_at",
ADD COLUMN     "expires_at" INTEGER,
ADD COLUMN     "id_token" TEXT,
ADD COLUMN     "oauth_token" TEXT,
ADD COLUMN     "oauth_token_secret" TEXT,
ADD COLUMN     "scope" TEXT,
ADD COLUMN     "session_state" TEXT,
ADD COLUMN     "token_type" TEXT;

ALTER TABLE "Session" DROP COLUMN "access_token",
DROP COLUMN "created_at",
DROP COLUMN "updated_at";

DROP TABLE "verification_requests";

CREATE TABLE "VerificationToken" (
    "identifier" TEXT NOT NULL,
    "token" TEXT NOT NULL,
    "expires" TIMESTAMP(3) NOT NULL

CREATE UNIQUE INDEX "VerificationToken_token_key" ON "VerificationToken"("token");

CREATE UNIQUE INDEX "VerificationToken_identifier_token_key" ON "VerificationToken"("identifier", "token");

CREATE UNIQUE INDEX "Account_provider_providerAccountId_key" ON "Account"("provider", "providerAccountId");



ALTER INDEX "sessions.session_token_unique" RENAME TO "Session_sessionToken_key";

ALTER INDEX "users.email_unique" RENAME TO "User_email_key";

Now you can run prisma migrate dev and you should see that it cleanly applies all the changes necessary to your database.

That's it! If you find use cases where this guide isn't sufficient, let me know and I'll be happy to add to this guide but it worked well for me.

Learn something new? Share it with the world!

There is more where that came from!

Drop your email in the box below and we'll let you know when we publish new stuff. We respect your email privacy, we will never spam you and you can unsubscribe anytime.