Skip to main content
The Vercel AI Chatbot uses Auth.js (NextAuth.js) for authentication, supporting both credential-based login and guest access for quick experimentation.

Authentication setup

Authentication is configured using Auth.js with a credentials provider:
app/(auth)/auth.ts
import NextAuth from "next-auth";
import Credentials from "next-auth/providers/credentials";
import { compare } from "bcrypt-ts";
import { getUser, createGuestUser } from "@/lib/db/queries";
import { authConfig } from "./auth.config";

export const {
  handlers: { GET, POST },
  auth,
  signIn,
  signOut,
} = NextAuth({
  ...authConfig,
  providers: [
    Credentials({
      credentials: {},
      async authorize({ email, password }: any) {
        const users = await getUser(email);

        if (users.length === 0) {
          await compare(password, DUMMY_PASSWORD);
          return null;
        }

        const [user] = users;

        if (!user.password) {
          await compare(password, DUMMY_PASSWORD);
          return null;
        }

        const passwordsMatch = await compare(password, user.password);

        if (!passwordsMatch) {
          return null;
        }

        return { ...user, type: "regular" };
      },
    }),
    Credentials({
      id: "guest",
      credentials: {},
      async authorize() {
        const [guestUser] = await createGuestUser();
        return { ...guestUser, type: "guest" };
      },
    }),
  ],
});

Auth configuration

The base auth configuration is defined separately:
app/(auth)/auth.config.ts
import type { NextAuthConfig } from "next-auth";

export const authConfig = {
  pages: {
    signIn: "/login",
    newUser: "/",
  },
  providers: [
    // Providers are added later in auth.ts since they require bcrypt
    // which is only compatible with Node.js environments
  ],
  callbacks: {},
} satisfies NextAuthConfig;

User types

The system supports two types of users:
Regular users create accounts with email and password:
app/(auth)/auth.ts
export type UserType = "guest" | "regular";

declare module "next-auth" {
  interface Session extends DefaultSession {
    user: {
      id: string;
      type: UserType;
    } & DefaultSession["user"];
  }

  interface User {
    id?: string;
    email?: string | null;
    type: UserType;
  }
}

Session management

Auth.js callbacks handle session and JWT token management:
app/(auth)/auth.ts
callbacks: {
  jwt({ token, user }) {
    if (user) {
      token.id = user.id as string;
      token.type = user.type;
    }
    return token;
  },
  session({ session, token }) {
    if (session.user) {
      session.user.id = token.id;
      session.user.type = token.type;
    }
    return session;
  },
}

JWT token interface

app/(auth)/auth.ts
declare module "next-auth/jwt" {
  interface JWT extends DefaultJWT {
    id: string;
    type: UserType;
  }
}

User registration

New users can register with email and password:
app/(auth)/actions.ts
import { createUser } from "@/lib/db/queries";
import { generateHashedPassword } from "@/lib/db/utils";

export async function register({
  email,
  password,
}: {
  email: string;
  password: string;
}) {
  const hashedPassword = generateHashedPassword(password);
  
  try {
    await createUser(email, hashedPassword);
    return { success: true };
  } catch (error) {
    return { success: false, error: "Failed to create account" };
  }
}

Password hashing

lib/db/utils.ts
import { hash } from "bcrypt-ts";

export function generateHashedPassword(password: string) {
  return hash(password, 10);
}

Login page

The login page provides both credential and guest login options:
app/(auth)/login/page.tsx
import { signIn } from "@/app/(auth)/auth";

export default function LoginPage() {
  return (
    <div className="flex h-screen w-screen items-center justify-center">
      <div className="flex flex-col gap-4">
        <form
          action={async (formData: FormData) => {
            "use server";
            await signIn("credentials", {
              email: formData.get("email"),
              password: formData.get("password"),
              redirect: true,
              redirectTo: "/",
            });
          }}
        >
          <input name="email" type="email" placeholder="Email" />
          <input name="password" type="password" placeholder="Password" />
          <button type="submit">Sign In</button>
        </form>
        
        <form
          action={async () => {
            "use server";
            await signIn("guest", {
              redirect: true,
              redirectTo: "/",
            });
          }}
        >
          <button type="submit">Continue as Guest</button>
        </form>
      </div>
    </div>
  );
}

Protected routes

Authenticate API routes using the auth() helper:
app/(chat)/api/chat/route.ts
import { auth } from "@/app/(auth)/auth";
import { ChatbotError } from "@/lib/errors";

export async function POST(request: Request) {
  const session = await auth();

  if (!session?.user) {
    return new ChatbotError("unauthorized:chat").toResponse();
  }

  const userId = session.user.id;
  const userType = session.user.type;

  // Proceed with authenticated request...
}

User entitlements

Different user types have different usage limits:
lib/ai/entitlements.ts
import type { UserType } from "@/app/(auth)/auth";

export const entitlementsByUserType: Record<
  UserType,
  { maxMessagesPerDay: number }
> = {
  guest: {
    maxMessagesPerDay: 10,
  },
  regular: {
    maxMessagesPerDay: 100,
  },
};

Enforcing rate limits

app/(chat)/api/chat/route.ts
import { entitlementsByUserType } from "@/lib/ai/entitlements";
import { getMessageCountByUserId } from "@/lib/db/queries";

const userType: UserType = session.user.type;

const messageCount = await getMessageCountByUserId({
  id: session.user.id,
  differenceInHours: 24,
});

if (messageCount > entitlementsByUserType[userType].maxMessagesPerDay) {
  return new ChatbotError("rate_limit:chat").toResponse();
}

Sign out

Users can sign out using the sign out form:
components/sign-out-form.tsx
import { signOut } from "@/app/(auth)/auth";

export function SignOutForm() {
  return (
    <form
      action={async () => {
        "use server";
        await signOut({
          redirectTo: "/login",
        });
      }}
    >
      <button type="submit">Sign Out</button>
    </form>
  );
}

Database schema

User data is stored in PostgreSQL:
lib/db/schema.ts
import { pgTable, uuid, varchar } from "drizzle-orm/pg-core";

export const user = pgTable("User", {
  id: uuid("id").primaryKey().notNull().defaultRandom(),
  email: varchar("email", { length: 64 }).notNull(),
  password: varchar("password", { length: 64 }),
});

export type User = InferSelectModel<typeof user>;
Guest users are automatically cleaned up after a period of inactivity to maintain database health.

Security considerations

Password hashing

All passwords are hashed using bcrypt with 10 salt rounds

Timing attack protection

Dummy password comparison prevents timing attacks

Bot detection

Requests are validated using BotID to prevent abuse

Rate limiting

IP-based and user-based rate limiting protects resources

Bot detection

app/(chat)/api/chat/route.ts
import { checkBotId } from "botid/server";

const botResult = await checkBotId();

if (botResult.isBot) {
  return new ChatbotError("unauthorized:chat").toResponse();
}

Data persistence

Learn about user data storage

Chat interface

Understand authenticated chat features

Build docs developers (and LLMs) love