Home / Articles

Google Sign-In with Auth.js and Prisma: Fitting the Next.js Guides Together

Combine the Prisma and Auth.js Next.js guides into one working app with Google sign-in, persisted users and posts tied to the session, including the fixes the docs skip.

Publisher: REACTAPP.TOP10 min read2404 words

This article is published in English.

Sources: Prisma + NextAuth setup for NextJS

Adding accounts and saved data to a Next.js app does not require a separate backend. With Auth.js (the successor to NextAuth) for authentication, Prisma as the ORM and a hosted Postgres database such as Neon, the same Next.js project can handle sign-in, sessions and persistence. Each tool has a decent Next.js guide on its own, but the guides disagree on folder layouts, reference outdated conventions and never show how the pieces connect. This walkthrough follows those guides in order and calls out every adjustment you need, so you end up with Google sign-in, users stored in Postgres and posts attributed to the signed-in user.

Why keep auth and data inside the Next.js app

A common setup for a product that starts as a mostly client-side tool is a separate backend, for example Django with Postgres and a hosted identity provider such as Auth0. That works, but it means a second service to deploy, monitor and pay for. When a small product needs to add something like saved workflows, reusing the Next.js server for authentication and database access keeps hosting costs and moving parts down. The trade-off is that you now own the integration between three libraries, which is what the rest of this article is about.

These packages change quickly. Auth.js for Next.js is still published under a beta tag, and both Prisma and Next.js have shifted defaults across recent major versions. Treat the steps below as a map and confirm the details against the current documentation of each project.

Installing Prisma without the hosted extras

The Prisma guide for Next.js is written around Prisma Postgres, the company's own hosted database. If you plan to use any other Postgres, you can skip @prisma/extension-accelerate, which only matters for that hosted offering.

For a plain Postgres setup you need two installs: the CLI and tsx as development dependencies, and the client plus dotenv as runtime dependencies. The snippet shows both commands on one line; run them one after the other:

npm install prisma tsx --save-dev npm install @prisma/client dotenv

Next, initialize Prisma in the project:

npx prisma init

The guide also mentions a --db flag for init. That flag provisions a Prisma Postgres instance, so leave it off when you bring your own database.

To get a database running locally without installing Postgres, Prisma can start a development server for you:

npx prisma dev

The init step has already written a DATABASE_URL into your .env file that points at this local server. If you already run Postgres locally, or want to point at a Neon branch, replace that value with your own connection string. For a closer look at how Prisma 7 behaves in a Next.js 16 project, including the new config file, see our Prisma 7 setup guide.

Building the example posts app

The rest of the Prisma guide builds a small blog with users and posts. If you are adding Prisma to an existing app, you can skip ahead to the authentication section. Following along is worthwhile, though, because the later steps assume those models exist.

Start by adding the models to schema.prisma as described in the schema step, then load environment variables in the Prisma config as shown in the dotenv step.

Fixing the generated client import path

When you seed the database, the seed script as written imports from the generated prisma folder directly, which fails. The generated code exposes the client under a client entry, so the first import line of the seed script has to point there:

import { PrismaClient, Prisma } from "../app/generated/prisma/client";

The @/app alias works just as well as the relative ../app path.

The same correction applies when you set up the shared Prisma client in lib/prisma.ts:

import { PrismaClient } from '../app/generated/prisma/client'

The guide also switches between the src/app and plain app/ conventions from one step to the next. Pick the one your project actually uses: with a src/ root, the path is ../src/app/generated; without it, ../app/generated. Most "module not found" errors at this stage come from mixing the two.

With the client in place, add the pages the guide describes: the home page query, the posts list, the post detail page and the form for creating posts.

Adding Auth.js

There are two documentation sites for this library: the older NextAuth site and the newer Auth.js reference for Next.js. The Auth.js docs describe the current version and are the ones to follow.

Install the package from its beta channel:

npm install next-auth@beta

Then run npx auth secret. It generates a random AUTH_SECRET, which Auth.js uses to sign and encrypt tokens and cookies, and writes it to .env.local. Since this project already keeps its variables in .env, move the value there and delete .env.local so there is a single source of configuration. Keep that file out of version control.

Now follow the installation steps. The central piece is an auth.ts file at the project root. It calls NextAuth once and exports four helpers that the rest of the app uses: the HTTP handlers, signIn, signOut and auth, which reads the current session:

import NextAuth from "next-auth"

export const { handlers, signIn, signOut, auth } = NextAuth({
  providers: [],
})

The route handler under ./app/api/auth/[...nextauth]/route.ts simply re-exports the GET and POST handlers so Auth.js can serve its sign-in, callback and session endpoints. In the snippet the comment and the export ended up on one line; in your file, put the export on its own line after the import:

import { handlers } from "@/auth" // Referring to the auth.ts we just created export const { GET, POST } = handlers

The installation guide also adds a middleware.ts. Next.js has since renamed middleware to proxy (see the Next.js note on the change), so the file as documented does not match current Next.js. You do not need it for this setup, because every protected page and action below checks the session itself. If you later want request-level protection, adapt it to the proxy convention.

Connecting Google as the sign-in provider

In the Google Cloud Console, create an OAuth client of type Web application. Set the authorized JavaScript origin to localhost:3000 and the redirect URI to localhost:3000/api/auth/callback/google, which is the callback route Auth.js exposes for the Google provider. When you deploy, add the production origin and callback as well. Store the client ID and secret Google gives you in your environment file under the variable names the Auth.js docs specify, never in source code.

The OAuth guide then adds a sign-in component. It is a form whose action is an inline Server Action calling signIn:

import { signIn } from "@/auth"

export function SignIn() {
  return (
    <form
      action={async () => {
        "use server"
        await signIn()
      }}
    >
      <button type="submit">Sign in</button>
    </form>
  )
}

Called without arguments, signIn() sends the user to the built-in page that lists all configured providers. With only Google configured, passing "google" skips that intermediate screen.

A matching sign-out component, from the session management docs, follows the same shape:

import { signOut } from "@/auth"

export function SignOut() {
  return (
    <form
      action={async () => {
        "use server"
        await signOut()
      }}
    >
      <button type="submit">Sign Out</button>
    </form>
  )
}

Putting the buttons in a navigation bar

The docs stop short of showing where these components go. A natural place is a navigation bar that is itself an async Server Component: it calls auth() and renders SignOut when a user is present and SignIn otherwise.

import Link from "next/link";
import { auth } from "../auth"
import SignIn from "./sign-in";
import SignOut from "./sign-out";

export default async function Navigation() {
    const session = await auth();

    return (
        <>
            <nav className="sticky top-0 z-50 bg-white/80 backdrop-blur-md border-b border-gray-200">
                <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
                    <div className="flex items-center h-16">
                        {/* Logo */}
                        <Link href="/" className="flex items-center space-x-2">
                        <span className="text-xl font-semibold text-[#369b6f]">
                            Prisma + NextAuth + Neon
                        </span>
                        </Link>

                        {/* Spacer */}
                        <div className="flex-1"></div>

                        {/* Navigation Links */}
                        <div className="hidden md:flex items-center mr-8">
                        <Link
                            href="/posts"
                            className="text-gray-700 font-medium hover:text-[#369b6f] transition-all duration-200"
                        >
                            Posts
                        </Link>
                        </div>

                        {/* Auth Buttons */}
                        <div className="flex items-center space-x-2 sm:space-x-4">
                        {session?.user ? (
                            <SignOut/>
                        ) : (
                            <SignIn/>
                        )}
                        </div>
                    </div>
                </div>
            </nav>
        </>
    );
}

Watch the imports here: this file uses default imports for SignIn and SignOut, while the components above use named exports. Either change the imports to import { SignIn } from "./sign-in" or export the components as defaults; otherwise the build fails.

Then render the navigation in the root layout so it appears on every page:

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="en">
      <body>
          <Navigation/>
          <main>
            {children}
          </main>
      </body>
    </html>
  );
}

Remember to import Navigation at the top of app/layout.tsx. Once the dev server reloads, a bar with a sign-in or sign-out button sits above every page. Keep in mind that both buttons rely on inline Server Actions, so they work only inside Server Components. A Client Component cannot define server functions inline; for that case you would move the actions into a separate file marked with "use server" and import them.

Persisting users with the Prisma adapter

At this point sign-in works, but nothing about the user is stored in your database. The Prisma adapter connects Auth.js to your Prisma client so that users, linked OAuth accounts and sessions are written to Postgres. Install it:

npm install @auth/prisma-adapter

Then update auth.ts to pass the adapter your shared Prisma client and to register the Google provider:

import NextAuth from "next-auth"
import Google from "next-auth/providers/google"
import { PrismaAdapter } from "@auth/prisma-adapter";
import prisma from "@/lib/prisma";

export const { handlers, signIn, signOut, auth } = NextAuth({
    adapter: PrismaAdapter(prisma),
    providers: [Google],
})

With an adapter configured, Auth.js stores sessions in the database by default instead of in a signed cookie, which lets you revoke sessions server-side at the cost of a database lookup per session check. The Google provider picks up its credentials from environment variables automatically when they follow the Auth.js naming convention.

Reconciling the User model

The adapter needs specific tables and columns, listed in the Prisma adapter schema. Copy them into schema.prisma. This collides with the example app: the Prisma guide created a User model with its own id type, whereas Auth.js expects a string id plus several additional fields. For a fresh example project, the simplest path is to replace the model and rebuild the database from scratch. If you are integrating into an app that already has real users, do not reset anything; plan a proper migration that maps existing users to the new shape. That is a separate problem outside the scope of this walkthrough.

To rebuild cleanly, first stop the seed from running again, since the seed data refers to the old user shape. Delete the seed entry in prisma.config.ts:

seed: `tsx prisma/seed.ts`

Then reset the database, which drops all tables and data:

npx prisma migrate reset

Only run this against a development database. Finally, create a migration for the new schema and apply it:

npx prisma migrate dev

Start the app again (npm run dev) and log in through Google. A new row should appear in the User table, with a related row in Account for the Google link. To inspect the rows, launch npx prisma studio, the visual database browser that ships with Prisma.

Attributing posts to the signed-in user

The example form from the Prisma guide creates posts with a hard-coded authorId. Now that sessions exist, the page can read the session and use the real user id instead. The page below calls auth(), shows a message to anonymous visitors, and passes session.user.id to Prisma when creating the post. It then revalidates the posts list and redirects to it:

export default async function NewPost() {
  const session = await auth();

  async function createPost(formData: FormData) {
    "use server";

    if(!session?.user?.id){
      return;
    }

    const title = formData.get("title") as string;
    const content = formData.get("content") as string;

    await prisma.post.create({
      data: {
        title,
        content,
        authorId: session.user.id,
      },
    });

    revalidatePath("/posts");
    redirect("/posts");
  }

  if(!session?.user){
    return <div>You need to sign in to create a post</div>
  }

  ...

}

One refinement is worth making before this goes anywhere near production. The action reads session from the page render through a closure. A Server Action is a public endpoint that can be invoked directly, so the safer pattern is to call auth() inside the action body and check the result there, making the check run on every submission. We explain why in why Server Actions need authorization inside every function body. It is also a good idea to validate title and content rather than casting them with as string.

Wrapping up

  • Follow the official Prisma and Auth.js guides, but fix the generated client import to point at generated/prisma/client and stay consistent about src/app versus app/.
  • Keep all secrets and connection strings in one environment file that is never committed.
  • Skip the documented middleware.ts; current Next.js uses the proxy convention, and page-level auth() checks are enough here.
  • Adding the Prisma adapter changes the required User shape. Resetting is fine for a demo, but existing data needs a real migration.
  • Check the session inside each Server Action, not only in the page that renders the form.

From here, the natural next steps are sign-in from Client Components and pointing DATABASE_URL at a hosted Postgres instance for deployment.