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:
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
Guest users
Regular users create accounts with email and password: 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 ;
}
}
Guest users are created automatically without credentials: export async function createGuestUser () {
const email = `guest- ${ Date . now () } ` ;
const password = generateHashedPassword ( generateUUID ());
try {
return await db . insert ( user ). values ({ email , password }). returning ({
id: user . id ,
email: user . email ,
});
} catch ( _error ) {
throw new ChatbotError (
"bad_request:database" ,
"Failed to create guest user"
);
}
}
Session management
Auth.js callbacks handle session and JWT token management:
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
declare module "next-auth/jwt" {
interface JWT extends DefaultJWT {
id : string ;
type : UserType ;
}
}
User registration
New users can register with email and password:
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
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:
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:
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