This guide covers how to add Blitz Auth to a Next.js app. We’ll go over setting up Blitz with Next.js, creating a basic auth logic, and using Blitz Auth features inside of Next’s pages and API handlers.
Should you get stuck while working through the guide, refer to this repo.
We’ll start by creating a brand new Next application with
create-next-app:
npx create-next-app@latestYou can then run yarn dev and open http://localhost:3000 in your
browser to check it out.
As the next step, we need to install Blitz.js packages:
blitz — Blitz’s core package consisting of Blitz CLI, utilities and
core functions used by other Blitz’s packages.@blitzjs/next — Next.js framework adapter required to initialize Blitz
in a Next project.@blitzjs/auth — the Auth plugin that we’ll explore in this guide.yarn add blitz @blitzjs/next @blitzjs/authIn order to have Blitz plugins working in your app, we need two
configuration files — one for the client side and one for the server side.
Let’s create an src directory and start with the former.
blitz-client.ts fileCreate a new file — blitz-client.ts and add the following content:
// src/blitz-client.ts:
import { AuthClientPlugin } from "@blitzjs/auth"
import { setupBlitzClient } from "@blitzjs/next"
export const authConfig = {
  cookiePrefix: "blitz-auth-with-next-app",
}
export const { withBlitz } = setupBlitzClient({
  plugins: [AuthClientPlugin(authConfig)],
})Here, we use setupBlitzClient from the @blitzjs/next and provide a
configuration for the plugins we want to use. In our case, we’re only
interested in the Auth plugin. The only thing we need to configure is the
cookie prefix, and for the rest — we’ll rely on Blitz’s defaults. The
reason for having authConfig as a separate variable is so that we can
export it and reuse in the server setup.
blitz-server.ts fileNow we can proceed with the server-side configuration. In the same
directory, we’ll add blitz-server.ts file:
// src/blitz-server.ts
import { setupBlitzServer } from "@blitzjs/next"
import {
  AuthServerPlugin,
  PrismaStorage,
  simpleRolesIsAuthorized,
} from "@blitzjs/auth"
import { BlitzLogger } from "blitz"
import db from "../prisma"
import { authConfig } from "./blitz-client"
export const { gSSP, gSP, api } = setupBlitzServer({
  plugins: [
    AuthServerPlugin({
      ...authConfig,
      storage: PrismaStorage(db),
      isAuthorized: simpleRolesIsAuthorized,
    }),
  ],
})In this file, we have slightly more things going on:
setupBlitzServer from @blitzjs/next (similar as we did in the
client config file)storage : Blitz stores session information in the database, so we
need to specify how Blitz Auth can access the storage.isAuthorized: a function that determines what user roles are
authorized to access pages and other protected code. We’ll use
simpleRoleIsAuthorized provided by Blitz Auth. You can read more
about it here:
https://blitzjs.com/docs/authorization#is-authorized-adapters.types.ts fileAfter creating the blitz-server.ts file, you might have noticed some
TypeScript issues around storage property in the Blitz Auth
configuration. That’s because Blitz uses types augmentation to set how the
Session object should look like. Let’s create a types.ts file at the
root of your project:
import { SimpleRolesIsAuthorized } from "@blitzjs/auth"
import { User } from "./prisma"
export type Role = "ADMIN" | "USER"
declare module "@blitzjs/auth" {
  export interface Session {
    isAuthorized: SimpleRolesIsAuthorized<Role>
    PublicData: {
      userId: User["id"]
      email: string
    }
  }
}The Blitz Auth provides a session-based auth system, so we need a database to store the session information. New Blitz apps have a Prisma setup by default, but the package is database agnostic, so we'll go over two options in this guide: with and without using Prisma.
Go to Without Prisma section if you don't want to use Prisma.
First, we’ll install prisma and @prisma/client:
yarn add prisma @prisma/clientAnd we’ll use its CLI to initialize a new Prisma client:
    yarn prisma init --datasource-provider sqliteWe also need to create a new Prisma client. For that, create an index.ts
file in the prisma directory with the following content:
// prisma/index.ts
import { PrismaClient } from "@prisma/client"
export * from "@prisma/client"
const db = new PrismaClient()
export default dbNext, we will update the prisma/schema.prisma file by adding User,
Session, and Token database models:
model User {
  id             Int      @id @default(autoincrement())
  email          String   @unique
  hashedPassword String?
  sessions Session[]
}
model Session {
  id                 Int       @id @default(autoincrement())
  expiresAt          DateTime?
  handle             String    @unique
  hashedSessionToken String?
  antiCSRFToken      String?
  publicData         String?
  privateData        String?
  user   User? @relation(fields: [userId], references: [id])
  userId Int?
}To apply the changes to the database and generate Prisma’s TypeScript
types, run: yarn prisma migrate dev.
Go to the next step: adding auth logic.
The storage property in the Blitz Auth configuration accepts
an object that implements the SessionConfigMethods interface.
The methods are:
getSessiongetSessionscreateSessionupdateSessiondeleteSessionYou can use any database or API, but in this guide, we’ll show a Redis example:
import IoRedis from "ioredis"
import { setupBlitz } from "@blitzjs/next"
import {
  AuthServerPlugin,
  simpleRolesIsAuthorized,
  SessionModel,
  Session,
} from "@blitzjs/auth"
const dbs: Record<string, IoRedis.Redis | undefined> = {
  default: undefined,
  auth: undefined,
}
export function getRedis(): IoRedis.Redis {
  if (dbs.default) {
    return dbs.default
  }
  return (dbs.default = createRedis(0))
}
export function getAuthRedis(): IoRedis.Redis {
  if (dbs.auth) {
    return dbs.auth
  }
  return (dbs.auth = createRedis(1))
}
export function createRedis(db: number) {
  return new IoRedis({
    port: 6379,
    host: "localhost",
    keepAlive: 60,
    keyPrefix: "auth:",
    db,
  })
}
const { gSSP, gSP, api } = setupBlitz({
  plugins: [
    AuthServerPlugin({
      cookiePrefix: "blitz-app-prefix",
      isAuthorized: simpleRolesIsAuthorized,
      storage: {
        createSession: (session: SessionModel): Promise<SessionModel> => {
          return new Promise<SessionModel>((resolve, reject) => {
            getAuthRedis().set(
              `token:${session.handle}`,
              JSON.stringify(session),
              (err) => {
                if (err) {
                  reject(err)
                } else {
                  getAuthRedis().lpush(
                    `device:${String(session.userId)}`,
                    session.handle
                  )
                  resolve(session)
                }
              }
            )
          })
        },
        deleteSession(handle: string): Promise<SessionModel> {
          return new Promise<SessionModel>((resolve, reject) => {
            getAuthRedis()
              .get(`token:${handle}`)
              .then((result) => {
                if (result) {
                  const session = JSON.parse(result) as SessionModel
                  const userId = session.userId as unknown as string
                  getAuthRedis().lrem(userId, 0, handle).catch(reject)
                }
                getAuthRedis().del(handle, (err) => {
                  if (err) {
                    reject(err)
                  } else {
                    resolve({ handle })
                  }
                })
              })
          })
        },
        getSession(handle: string): Promise<SessionModel | null> {
          return new Promise<SessionModel | null>((resolve, reject) => {
            getAuthRedis()
              .get(`token:${handle}`)
              .then((data: string | null) => {
                if (data) {
                  resolve(JSON.parse(data))
                } else {
                  resolve(null)
                }
              })
              .catch(reject)
          })
        },
        getSessions(
          userId: Session.PublicData["userId"]
        ): Promise<SessionModel[]> {
          return new Promise<SessionModel[]>((resolve, reject) => {
            getAuthRedis()
              .lrange(`device:${String(userId)}`, 0, -1)
              .then((result) => {
                if (result) {
                  resolve(
                    result.map((handle) => {
                      return this.getSession(handle)
                    })
                  )
                } else {
                  resolve([])
                }
              })
              .catch(reject)
          })
        },
        updateSession(
          handle: string,
          session: Partial<SessionModel>
        ): Promise<SessionModel> {
          return new Promise<SessionModel>((resolve, reject) => {
            getAuthRedis()
              .get(`token:${handle}`)
              .then((result) => {
                if (result) {
                  const oldSession = JSON.parse(result) as SessionModel
                  const merge = Object.assign(oldSession, session)
                  getAuthRedis()
                    .set(`token:${handle}`, JSON.stringify(merge))
                    .catch(reject)
                }
                reject(new Error("cant update session"))
              })
          })
        },
      },
    }),
  ],
})Now that the setup part is done, we can proceed to implement a simple auth logic using Blitz Auth. This guide will cover sign-up, log-in, and log-out.
pages/api/signup.ts fileWe’ll start by creating a new API Route called signup. Inside, we’ll use
api function that we got from setupBlitzServer function in
src/blitz-server.ts. It’s a wrapper around Next’s API handlers that
provides access to the Blitz’s ctx object, which contains auth-related
methods and properties.
Inside the handler, we’ll use SecurePassword from @blitzjs/auth to
hash the password provided by user. Next, we’ll create a new user in the
database and create a new authenticated session with the $create method.
The object we provide to session.$create is the public data. It contains
the same properties we specified in the types.ts file. Last but not
least, we’ll send a response to the client.
Note: there’s no error handling because I’m trying to keep this guide minimal and focus only on how to setup Blitz Auth with Next.js. Before using it in your applications, you should extend and modify it accordingly.
// pages/api/signup.ts
import { SecurePassword } from "@blitzjs/auth"
import { api } from "../../src/blitz-server"
import db from "../../prisma"
const signup = api(async (req, res, ctx) => {
  // TODO: you can add a runtime validation (e.g. with zod) to ensure password length
  const hashedPassword = await SecurePassword.hash(req.body.password)
  const email = req.body.email
  const user = await db.user.create({
    data: { email, hashedPassword, role: "user" },
    select: { id: true, name: true, email: true, role: true },
  })
  await ctx.session.$create({
    userId: user.id,
    email: user.email,
    role: "USER",
  })
  res
    .status(200)
    .json({ userId: ctx.session.userId, ...user, email: req.query.email })
})
export default signup/signup fetch call to signup form’s onSubmit methodAs we have the backend logic for sign-up, we can call the new endpoint from the client, e.g. when user submits a sign-up form. The call will look like this:
import { getAntiCSRFToken } from "@blitzjs/auth"
// handling requests
const antiCSRFToken = getAntiCSRFToken()
await fetch("/api/signup", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "anti-csrf": antiCSRFToken,
  },
  body: JSON.stringify({ email, password }),
})One thing to notice here is the usage of getAntiCSRFToken. When making a
request from the client to an API route, we need to include anti-csrf
header. You can read more about it
here.
pages/api/login.tsAs before, we’ll add a new API route. Inside, we’ll add an
authenticateUser function to verify the login credentials, and if
correct, we’ll create a new authenticated session as we did in the signup
handler.
// pages/api/login.ts
import { SecurePassword } from "@blitzjs/auth"
import { AuthenticationError } from "blitz"
import db from "../../prisma"
import { api } from "../../src/blitz-server"
export const authenticateUser = async (
  email: string,
  password: string
) => {
  const user = await db.user.findFirst({ where: { email } })
  if (!user) throw new AuthenticationError()
  const result = await SecurePassword.verify(
    user.hashedPassword,
    password
  )
  if (result === SecurePassword.VALID_NEEDS_REHASH) {
    // Upgrade hashed password with a more secure hash
    const improvedHash = await SecurePassword.hash(password)
    await db.user.update({
      where: { id: user.id },
      data: { hashedPassword: improvedHash },
    })
  }
  const { hashedPassword, ...rest } = user
  return rest
}
const login = api(async (req, res, ctx) => {
  const email = req.body.email
  const password = req.body.password
  const user = await authenticateUser(email, password)
  await ctx.session.$create({
    email: user.email,
    userId: user.id,
    role: "USER",
  })
  res
    .status(200)
    .json({ email: req.query.email, userId: ctx.session.userId })
})
export default login/login fetch call to login form’s onSubmit methodOn the client side, you need to send a request to the /api/login in a
similar way as in case of signup.
import { getAntiCSRFToken } from "@blitzjs/auth"
const antiCSRFToken = getAntiCSRFToken()
await fetch("/api/login", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "anti-csrf": antiCSRFToken,
  },
  body: JSON.stringify({ email, password }),
})pages/api/logout.tsOne last thing we’ll do on the API side is to add a logout handler. Inside
we’ll use a $revoke function which removes the session and logs the user
out.
// pages/api/logout.ts
import { api } from "../../src/blitz-server"
const logout = api(async (_req, res, ctx) => {
  await ctx.session.$revoke()
  res.status(200).json({ message: "Logged out" })
})
export default logoutgetServerSideProps, and getStaticPropsWe already saw how to use Blitz Auth session’s methods in the Next.js API
Routes. We can do so by accessing the Blitz context provided as third
argument in an api wrapper:
// my-api-routes.ts
import { api } from "src/blitz-server"
const handler = api(async (req, res, ctx) => {
  const session = ctx.session
  // session.$authorize
  // session.$setPublicData
  // etc.
})A similar thing can be done in the case of getServerSideProps and
getStaticProps. createBlitzServer returns gSSP, and gSP functions
that are wrappers for getServerSideProps and getStaticProps. Example
usage:
// MyPage.tsx
import { gSSP } from "src/blitz-server"
export const getServerSideProps = gSSP(async ({ ctx }) => {
  const session = ctx.session
  // session.$authorize
  // session.$setPublicData
  // etc.
})Blitz Auth provides a
useSession()`` hook that returns PublicDatawithisLoading`` property.
This hook can be used anywhere in your application.
Note: useSession() uses suspense by default, so you need a <Suspense>
component above it in the tree. Or you can set
useSession({suspense: false}) to disable suspense.
Here's an example usage:
import { useSession } from "@blitzjs/auth"
function Component() {
  const session = useSession()
  const userId = session.userId
  const role = session.role
  return /*... */
}What we did so far is only a part of Blitz Auth features. We’ll also
quickly explore how to protect the pages. Blitz Auth allows you to add
authenticate or redirectAuthenticatedTo properties on your pages or
layouts. To be able to use them, we need to wrap the App component with
the withBlitz HOC. If you don’t have it, add a new _app.tsx file to
the pages directory. In this file, we have to wrap the App component
with the withBlitz.
import { AppProps } from "next/app"
import React from "react"
import { withBlitz } from "../src/blitz-client"
function App({ Component, pageProps }: AppProps) {
  return <Component {...pageProps} />
}
export default withBlitz(App)Note: withBlitz currently doesn’t work with the new Next 13 layouts.
Until we add a support for it, you’ll still have to use the old
pages/_app.tsx.
Now, let’s go back to our pages. In the signup.tsx and login.tsx,
we’ll use redirectAuthenticatedTo:
SignupPage.redirectAuthenticatedTo = "/user"
export default SignupPageAnd in login.tsx:
LoginPage.redirectAuthenticatedTo = "/user"
export default LoginPageIn the user.tsx, we’ll use authenticate and redirect to the login
page if a user attempting to visit it is not authenticated.
UserPage.authenticate = { redirectTo: "/login" }
export default UserPageif you’re interested in exploring other Blitz Auth’s features, it also exports a bunch of hooks and utilities that are worth checking out: docs.
This guide covered setting up Prisma for Blitz Auth, adding Blitz Auth to a new Next.js app, and implementing a basic auth flow. We also explored how to use Blitz Auth to protect pages.
If you want to see a full Blitz app with sign-up, login, logout, reset
password you can check out an
example in Blitz’s repo
or run npx blitz new my-new-blitz-app to generate a new production-ready
Blitz app.
A repository with setup from this guide is available here.
To learn more about Blitz.js, you can take a look at the following resources:
And if you have any feedback reach out to us on Discord or GitHub.