diff --git a/README.md b/README.md
index 798725e860..797d5c7418 100644
--- a/README.md
+++ b/README.md
@@ -31,8 +31,9 @@
- Data Persistence
- [Neon Serverless Postgres](https://vercel.com/marketplace/neon) for saving chat history and user data
- [Vercel Blob](https://vercel.com/storage/blob) for efficient file storage
-- [Auth.js](https://authjs.dev)
- - Simple and secure authentication
+- [BetterAuth](https://www.better-auth.com)
+ - Modern, type-safe authentication with email/password support
+ - Built-in session management with customizable password hashing
## Model Providers
diff --git a/app/(auth)/actions.ts b/app/(auth)/actions.ts
index 024ff518ed..c18633053a 100644
--- a/app/(auth)/actions.ts
+++ b/app/(auth)/actions.ts
@@ -1,19 +1,18 @@
-"use server";
+"use server"
-import { z } from "zod";
-
-import { createUser, getUser } from "@/lib/db/queries";
-
-import { signIn } from "./auth";
+import { z } from "zod"
+import { auth } from "@/lib/auth"
+import { createUser, getUser } from "@/lib/db/queries"
+import { headers } from "next/headers"
const authFormSchema = z.object({
email: z.string().email(),
password: z.string().min(6),
-});
+})
export type LoginActionState = {
- status: "idle" | "in_progress" | "success" | "failed" | "invalid_data";
-};
+ status: "idle" | "in_progress" | "success" | "failed" | "invalid_data"
+}
export const login = async (
_: LoginActionState,
@@ -23,23 +22,26 @@ export const login = async (
const validatedData = authFormSchema.parse({
email: formData.get("email"),
password: formData.get("password"),
- });
+ })
- await signIn("credentials", {
- email: validatedData.email,
- password: validatedData.password,
- redirect: false,
- });
+ const response = await auth.api.signInEmail({
+ body: validatedData,
+ headers: await headers(),
+ asResponse: true
+ })
- return { status: "success" };
+ if (!response.ok) {
+ return { status: "failed" }
+ }
+
+ return { status: "success" }
} catch (error) {
if (error instanceof z.ZodError) {
- return { status: "invalid_data" };
+ return { status: "invalid_data" }
}
-
- return { status: "failed" };
+ return { status: "failed" }
}
-};
+}
export type RegisterActionState = {
status:
@@ -48,8 +50,8 @@ export type RegisterActionState = {
| "success"
| "failed"
| "user_exists"
- | "invalid_data";
-};
+ | "invalid_data"
+}
export const register = async (
_: RegisterActionState,
@@ -59,26 +61,33 @@ export const register = async (
const validatedData = authFormSchema.parse({
email: formData.get("email"),
password: formData.get("password"),
- });
+ })
- const [user] = await getUser(validatedData.email);
+ // Check if user exists
+ const [existingUser] = await getUser(validatedData.email)
+ if (existingUser) {
+ return { status: "user_exists" }
+ }
+
+ // Create user in database first
+ await createUser(validatedData.email, validatedData.password)
- if (user) {
- return { status: "user_exists" } as RegisterActionState;
+ // Then sign them in
+ const response = await auth.api.signInEmail({
+ body: validatedData,
+ headers: await headers(),
+ asResponse: true
+ })
+
+ if (!response.ok) {
+ return { status: "failed" }
}
- await createUser(validatedData.email, validatedData.password);
- await signIn("credentials", {
- email: validatedData.email,
- password: validatedData.password,
- redirect: false,
- });
- return { status: "success" };
+ return { status: "success" }
} catch (error) {
if (error instanceof z.ZodError) {
- return { status: "invalid_data" };
+ return { status: "invalid_data" }
}
-
- return { status: "failed" };
+ return { status: "failed" }
}
-};
+}
diff --git a/app/(auth)/api/auth/[...nextauth]/route.ts b/app/(auth)/api/auth/[...nextauth]/route.ts
deleted file mode 100644
index 588ff6a5ca..0000000000
--- a/app/(auth)/api/auth/[...nextauth]/route.ts
+++ /dev/null
@@ -1,2 +0,0 @@
-// biome-ignore lint/performance/noBarrelFile: "Required"
-export { GET, POST } from "@/app/(auth)/auth";
diff --git a/app/(auth)/api/auth/guest/route.ts b/app/(auth)/api/auth/guest/route.ts
index dca565c5ab..40881ce8a6 100644
--- a/app/(auth)/api/auth/guest/route.ts
+++ b/app/(auth)/api/auth/guest/route.ts
@@ -1,21 +1,51 @@
-import { NextResponse } from "next/server";
-import { getToken } from "next-auth/jwt";
-import { signIn } from "@/app/(auth)/auth";
-import { isDevelopmentEnvironment } from "@/lib/constants";
+import { headers } from "next/headers";
+import { NextResponse } from "next/server"
+import { auth } from "@/lib/auth"
+import { createGuestUser } from "@/lib/db/queries"
export async function GET(request: Request) {
- const { searchParams } = new URL(request.url);
- const redirectUrl = searchParams.get("redirectUrl") || "/";
+ const { searchParams } = new URL(request.url)
+ const redirectUrl = searchParams.get("redirectUrl") || "/"
- const token = await getToken({
- req: request,
- secret: process.env.AUTH_SECRET,
- secureCookie: !isDevelopmentEnvironment,
- });
+ try {
+ // Check if already authenticated
+ const session = await auth.api.getSession({
+ headers: request.headers
+ })
- if (token) {
- return NextResponse.redirect(new URL("/", request.url));
- }
+ if (session) {
+ return NextResponse.redirect(new URL("/", request.url))
+ }
+
+ // Create guest user
+ const [guestUser] = await createGuestUser()
+
+ // Sign in the guest user programmatically
+ // This approach manually creates a session for the guest user
+ const signInResponse = await auth.api.signInEmail({
+ body: {
+ email: guestUser.email!,
+ password: "guest-password", // Use a consistent guest password
+ },
+ asResponse: true
+ })
+
+ if (signInResponse.ok) {
+ // Forward the session cookies from the sign-in response
+ const response = NextResponse.redirect(new URL(redirectUrl, request.url))
- return signIn("guest", { redirect: true, redirectTo: redirectUrl });
+ // Copy auth cookies from the sign-in response
+ const authCookies = signInResponse.headers.get('set-cookie')
+ if (authCookies) {
+ response.headers.set('set-cookie', authCookies)
+ }
+
+ return response
+ }
+
+ throw new Error('Guest sign-in failed')
+ } catch (error) {
+ console.error('Guest authentication error:', error)
+ return NextResponse.redirect(new URL("/login", request.url))
+ }
}
diff --git a/app/(auth)/auth.config.ts b/app/(auth)/auth.config.ts
deleted file mode 100644
index b8bc9e1f17..0000000000
--- a/app/(auth)/auth.config.ts
+++ /dev/null
@@ -1,13 +0,0 @@
-import type { NextAuthConfig } from "next-auth";
-
-export const authConfig = {
- pages: {
- signIn: "/login",
- newUser: "/",
- },
- providers: [
- // added later in auth.ts since it requires bcrypt which is only compatible with Node.js
- // while this file is also used in non-Node.js environments
- ],
- callbacks: {},
-} satisfies NextAuthConfig;
diff --git a/app/(auth)/auth.ts b/app/(auth)/auth.ts
deleted file mode 100644
index dbebb1d98d..0000000000
--- a/app/(auth)/auth.ts
+++ /dev/null
@@ -1,95 +0,0 @@
-import { compare } from "bcrypt-ts";
-import NextAuth, { type DefaultSession } from "next-auth";
-import type { DefaultJWT } from "next-auth/jwt";
-import Credentials from "next-auth/providers/credentials";
-import { DUMMY_PASSWORD } from "@/lib/constants";
-import { createGuestUser, getUser } from "@/lib/db/queries";
-import { authConfig } from "./auth.config";
-
-export type UserType = "guest" | "regular";
-
-declare module "next-auth" {
- interface Session extends DefaultSession {
- user: {
- id: string;
- type: UserType;
- } & DefaultSession["user"];
- }
-
- // biome-ignore lint/nursery/useConsistentTypeDefinitions: "Required"
- interface User {
- id?: string;
- email?: string | null;
- type: UserType;
- }
-}
-
-declare module "next-auth/jwt" {
- interface JWT extends DefaultJWT {
- id: string;
- type: UserType;
- }
-}
-
-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" };
- },
- }),
- ],
- 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;
- },
- },
-});
diff --git a/app/(auth)/login/page.tsx b/app/(auth)/login/page.tsx
index 1140cbe14d..1ba05f644d 100644
--- a/app/(auth)/login/page.tsx
+++ b/app/(auth)/login/page.tsx
@@ -2,7 +2,7 @@
import Link from "next/link";
import { useRouter } from "next/navigation";
-import { useSession } from "next-auth/react";
+import { useSession } from "@/lib/auth-client";
import { useActionState, useEffect, useState } from "react";
import { AuthForm } from "@/components/auth-form";
@@ -23,7 +23,7 @@ export default function Page() {
}
);
- const { update: updateSession } = useSession();
+ const { data: session, isPending } = useSession();
useEffect(() => {
if (state.status === "failed") {
@@ -38,11 +38,10 @@ export default function Page() {
});
} else if (state.status === "success") {
setIsSuccessful(true);
- updateSession();
router.refresh();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
- }, [state.status, router.refresh, updateSession]);
+ }, [state.status, router]);
const handleSubmit = (formData: FormData) => {
setEmail(formData.get("email") as string);
diff --git a/app/(auth)/register/page.tsx b/app/(auth)/register/page.tsx
index 950f67b96b..295333f8c3 100644
--- a/app/(auth)/register/page.tsx
+++ b/app/(auth)/register/page.tsx
@@ -2,7 +2,7 @@
import Link from "next/link";
import { useRouter } from "next/navigation";
-import { useSession } from "next-auth/react";
+import { useSession } from "@/lib/auth-client";
import { useActionState, useEffect, useState } from "react";
import { AuthForm } from "@/components/auth-form";
import { SubmitButton } from "@/components/submit-button";
@@ -22,7 +22,7 @@ export default function Page() {
}
);
- const { update: updateSession } = useSession();
+ const { data: session, isPending } = useSession();
useEffect(() => {
if (state.status === "user_exists") {
@@ -38,11 +38,10 @@ export default function Page() {
toast({ type: "success", description: "Account created successfully!" });
setIsSuccessful(true);
- updateSession();
router.refresh();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
- }, [state.status, router.refresh, updateSession]);
+ }, [state.status, router]);
const handleSubmit = (formData: FormData) => {
setEmail(formData.get("email") as string);
diff --git a/app/(chat)/api/chat/[id]/stream/route.ts b/app/(chat)/api/chat/[id]/stream/route.ts
index 48352e9763..c076948af1 100644
--- a/app/(chat)/api/chat/[id]/stream/route.ts
+++ b/app/(chat)/api/chat/[id]/stream/route.ts
@@ -1,6 +1,7 @@
+import { headers } from "next/headers";
import { createUIMessageStream, JsonToSseTransformStream } from "ai";
import { differenceInSeconds } from "date-fns";
-import { auth } from "@/app/(auth)/auth";
+import { auth } from "@/lib/auth";
import {
getChatById,
getMessagesByChatId,
@@ -28,7 +29,7 @@ export async function GET(
return new ChatSDKError("bad_request:api").toResponse();
}
- const session = await auth();
+ const session = await auth.api.getSession({ headers: await headers() });
if (!session?.user) {
return new ChatSDKError("unauthorized:chat").toResponse();
diff --git a/app/(chat)/api/chat/route.ts b/app/(chat)/api/chat/route.ts
index 8808ea6fff..f750daf110 100644
--- a/app/(chat)/api/chat/route.ts
+++ b/app/(chat)/api/chat/route.ts
@@ -1,3 +1,4 @@
+import { headers } from "next/headers";
import { geolocation } from "@vercel/functions";
import {
convertToModelMessages,
@@ -16,7 +17,7 @@ import {
import type { ModelCatalog } from "tokenlens/core";
import { fetchModels } from "tokenlens/fetch";
import { getUsage } from "tokenlens/helpers";
-import { auth, type UserType } from "@/app/(auth)/auth";
+import { auth, type UserType } from "@/lib/auth";
import type { VisibilityType } from "@/components/visibility-selector";
import { entitlementsByUserType } from "@/lib/ai/entitlements";
import type { ChatModel } from "@/lib/ai/models";
@@ -107,13 +108,13 @@ export async function POST(request: Request) {
selectedVisibilityType: VisibilityType;
} = requestBody;
- const session = await auth();
+ const session = await auth.api.getSession({ headers: await headers() });
if (!session?.user) {
return new ChatSDKError("unauthorized:chat").toResponse();
}
- const userType: UserType = session.user.type;
+ const userType: UserType = /^guest-\d+$/.test(session.user.email) ? "guest" : "regular";
const messageCount = await getMessageCountByUserId({
id: session.user.id,
@@ -315,7 +316,7 @@ export async function DELETE(request: Request) {
return new ChatSDKError("bad_request:api").toResponse();
}
- const session = await auth();
+ const session = await auth.api.getSession({ headers: await headers() });
if (!session?.user) {
return new ChatSDKError("unauthorized:chat").toResponse();
diff --git a/app/(chat)/api/document/route.ts b/app/(chat)/api/document/route.ts
index 0ea78ff553..382f3d6145 100644
--- a/app/(chat)/api/document/route.ts
+++ b/app/(chat)/api/document/route.ts
@@ -1,4 +1,5 @@
-import { auth } from "@/app/(auth)/auth";
+import { headers } from "next/headers";
+import { auth } from "@/lib/auth";
import type { ArtifactKind } from "@/components/artifact";
import {
deleteDocumentsByIdAfterTimestamp,
@@ -18,7 +19,7 @@ export async function GET(request: Request) {
).toResponse();
}
- const session = await auth();
+ const session = await auth.api.getSession({ headers: await headers() });
if (!session?.user) {
return new ChatSDKError("unauthorized:document").toResponse();
@@ -50,7 +51,7 @@ export async function POST(request: Request) {
).toResponse();
}
- const session = await auth();
+ const session = await auth.api.getSession({ headers: await headers() });
if (!session?.user) {
return new ChatSDKError("not_found:document").toResponse();
@@ -103,7 +104,7 @@ export async function DELETE(request: Request) {
).toResponse();
}
- const session = await auth();
+ const session = await auth.api.getSession({ headers: await headers() });
if (!session?.user) {
return new ChatSDKError("unauthorized:document").toResponse();
diff --git a/app/(chat)/api/files/upload/route.ts b/app/(chat)/api/files/upload/route.ts
index 4e4e4f3caf..c26114fb05 100644
--- a/app/(chat)/api/files/upload/route.ts
+++ b/app/(chat)/api/files/upload/route.ts
@@ -1,8 +1,9 @@
+import { headers } from "next/headers";
import { put } from "@vercel/blob";
import { NextResponse } from "next/server";
import { z } from "zod";
-import { auth } from "@/app/(auth)/auth";
+import { auth } from "@/lib/auth";
// Use Blob instead of File since File is not available in Node.js environment
const FileSchema = z.object({
@@ -18,7 +19,7 @@ const FileSchema = z.object({
});
export async function POST(request: Request) {
- const session = await auth();
+ const session = await auth.api.getSession({ headers: await headers() });
if (!session) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
diff --git a/app/(chat)/api/history/route.ts b/app/(chat)/api/history/route.ts
index 412f10f6ed..658102397a 100644
--- a/app/(chat)/api/history/route.ts
+++ b/app/(chat)/api/history/route.ts
@@ -1,5 +1,6 @@
+import { headers } from "next/headers";
import type { NextRequest } from "next/server";
-import { auth } from "@/app/(auth)/auth";
+import { auth } from "@/lib/auth";
import { getChatsByUserId } from "@/lib/db/queries";
import { ChatSDKError } from "@/lib/errors";
@@ -17,7 +18,7 @@ export async function GET(request: NextRequest) {
).toResponse();
}
- const session = await auth();
+ const session = await auth.api.getSession({ headers: await headers() });
if (!session?.user) {
return new ChatSDKError("unauthorized:chat").toResponse();
diff --git a/app/(chat)/api/suggestions/route.ts b/app/(chat)/api/suggestions/route.ts
index 8801004eff..ae932b14aa 100644
--- a/app/(chat)/api/suggestions/route.ts
+++ b/app/(chat)/api/suggestions/route.ts
@@ -1,4 +1,5 @@
-import { auth } from "@/app/(auth)/auth";
+import { headers } from "next/headers";
+import { auth } from "@/lib/auth";
import { getSuggestionsByDocumentId } from "@/lib/db/queries";
import { ChatSDKError } from "@/lib/errors";
@@ -13,7 +14,7 @@ export async function GET(request: Request) {
).toResponse();
}
- const session = await auth();
+ const session = await auth.api.getSession({ headers: await headers() });
if (!session?.user) {
return new ChatSDKError("unauthorized:suggestions").toResponse();
diff --git a/app/(chat)/api/vote/route.ts b/app/(chat)/api/vote/route.ts
index 2c0ce3f788..28a9ef0323 100644
--- a/app/(chat)/api/vote/route.ts
+++ b/app/(chat)/api/vote/route.ts
@@ -1,4 +1,5 @@
-import { auth } from "@/app/(auth)/auth";
+import { headers } from "next/headers";
+import { auth } from "@/lib/auth";
import { getChatById, getVotesByChatId, voteMessage } from "@/lib/db/queries";
import { ChatSDKError } from "@/lib/errors";
@@ -13,7 +14,7 @@ export async function GET(request: Request) {
).toResponse();
}
- const session = await auth();
+ const session = await auth.api.getSession({ headers: await headers() });
if (!session?.user) {
return new ChatSDKError("unauthorized:vote").toResponse();
@@ -49,7 +50,7 @@ export async function PATCH(request: Request) {
).toResponse();
}
- const session = await auth();
+ const session = await auth.api.getSession({ headers: await headers() });
if (!session?.user) {
return new ChatSDKError("unauthorized:vote").toResponse();
diff --git a/app/(chat)/chat/[id]/page.tsx b/app/(chat)/chat/[id]/page.tsx
index f208fac603..2b886497fe 100644
--- a/app/(chat)/chat/[id]/page.tsx
+++ b/app/(chat)/chat/[id]/page.tsx
@@ -1,7 +1,8 @@
+import { headers } from "next/headers";
import { cookies } from "next/headers";
import { notFound, redirect } from "next/navigation";
-import { auth } from "@/app/(auth)/auth";
+import { auth } from "@/lib/auth";
import { Chat } from "@/components/chat";
import { DataStreamHandler } from "@/components/data-stream-handler";
import { DEFAULT_CHAT_MODEL } from "@/lib/ai/models";
@@ -17,7 +18,7 @@ export default async function Page(props: { params: Promise<{ id: string }> }) {
notFound();
}
- const session = await auth();
+ const session = await auth.api.getSession({ headers: await headers() });
if (!session) {
redirect("/api/auth/guest");
diff --git a/app/(chat)/layout.tsx b/app/(chat)/layout.tsx
index c5e44e5a8b..fb37d7433d 100644
--- a/app/(chat)/layout.tsx
+++ b/app/(chat)/layout.tsx
@@ -3,7 +3,8 @@ import Script from "next/script";
import { AppSidebar } from "@/components/app-sidebar";
import { DataStreamProvider } from "@/components/data-stream-provider";
import { SidebarInset, SidebarProvider } from "@/components/ui/sidebar";
-import { auth } from "../(auth)/auth";
+import { headers } from "next/headers";
+import { auth } from "@/lib/auth";
export const experimental_ppr = true;
@@ -12,7 +13,10 @@ export default async function Layout({
}: {
children: React.ReactNode;
}) {
- const [session, cookieStore] = await Promise.all([auth(), cookies()]);
+ const [session, cookieStore] = await Promise.all([
+ auth.api.getSession({ headers: await headers() }),
+ cookies()
+ ]);
const isCollapsed = cookieStore.get("sidebar_state")?.value !== "true";
return (
diff --git a/app/(chat)/page.tsx b/app/(chat)/page.tsx
index 225d2acaff..1bceb07586 100644
--- a/app/(chat)/page.tsx
+++ b/app/(chat)/page.tsx
@@ -1,13 +1,14 @@
+import { headers } from "next/headers";
import { cookies } from "next/headers";
import { redirect } from "next/navigation";
import { Chat } from "@/components/chat";
import { DataStreamHandler } from "@/components/data-stream-handler";
import { DEFAULT_CHAT_MODEL } from "@/lib/ai/models";
import { generateUUID } from "@/lib/utils";
-import { auth } from "../(auth)/auth";
+import { auth } from "@/lib/auth";
export default async function Page() {
- const session = await auth();
+ const session = await auth.api.getSession({ headers: await headers() });
if (!session) {
redirect("/api/auth/guest");
diff --git a/app/api/auth/[...all]/route.ts b/app/api/auth/[...all]/route.ts
new file mode 100644
index 0000000000..3ae740d1a3
--- /dev/null
+++ b/app/api/auth/[...all]/route.ts
@@ -0,0 +1,4 @@
+import { toNextJsHandler } from "better-auth/next-js"
+import { auth } from "@/lib/auth"
+
+export const { POST, GET } = toNextJsHandler(auth)
\ No newline at end of file
diff --git a/app/layout.tsx b/app/layout.tsx
index 66db5da925..16d56cb91f 100644
--- a/app/layout.tsx
+++ b/app/layout.tsx
@@ -4,7 +4,6 @@ import { Toaster } from "sonner";
import { ThemeProvider } from "@/components/theme-provider";
import "./globals.css";
-import { SessionProvider } from "next-auth/react";
export const metadata: Metadata = {
metadataBase: new URL("https://chat.vercel.ai"),
@@ -79,7 +78,7 @@ export default function RootLayout({
enableSystem
>
- {children}
+ {children}