├── src ├── db │ ├── schema │ │ ├── index.ts │ │ └── auth │ │ │ ├── index.ts │ │ │ ├── verification.ts │ │ │ ├── session.ts │ │ │ ├── user.ts │ │ │ └── account.ts │ └── index.ts ├── app │ ├── favicon.ico │ ├── fonts │ │ ├── GeistVF.woff │ │ └── GeistMonoVF.woff │ ├── api │ │ └── auth │ │ │ └── [...all] │ │ │ └── route.ts │ ├── (routes) │ │ ├── (auth) │ │ │ ├── signin │ │ │ │ ├── validate.ts │ │ │ │ ├── page.tsx │ │ │ │ └── form.tsx │ │ │ ├── components │ │ │ │ ├── input-start-icon.tsx │ │ │ │ ├── button-signout.tsx │ │ │ │ ├── gender-radio-group.tsx │ │ │ │ └── input-password.tsx │ │ │ └── signup │ │ │ │ ├── page.tsx │ │ │ │ ├── validate.ts │ │ │ │ └── form.tsx │ │ └── (home) │ │ │ └── page.tsx │ ├── layout.tsx │ └── globals.css ├── lib │ ├── utils.ts │ └── auth │ │ ├── get-session.ts │ │ ├── client.ts │ │ ├── password.ts │ │ ├── usernames.ts │ │ └── server.ts ├── routes.ts ├── providers │ └── index.tsx ├── components │ └── ui │ │ ├── label.tsx │ │ ├── sonner.tsx │ │ ├── input.tsx │ │ ├── radio-group.tsx │ │ ├── button.tsx │ │ └── form.tsx └── proxy.ts ├── .prettierrc.json ├── .eslintrc.json ├── postcss.config.mjs ├── public ├── vercel.svg ├── window.svg ├── file.svg ├── globe.svg └── next.svg ├── next.config.ts ├── drizzle.config.ts ├── env.example ├── components.json ├── .gitignore ├── tsconfig.json ├── LICENSE ├── package.json └── README.md /src/db/schema/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./auth" 2 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["prettier-plugin-tailwindcss"] 3 | } -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["next/core-web-vitals", "next/typescript"] 3 | } 4 | -------------------------------------------------------------------------------- /src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jabirdev/nextjs-better-auth/HEAD/src/app/favicon.ico -------------------------------------------------------------------------------- /src/app/fonts/GeistVF.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jabirdev/nextjs-better-auth/HEAD/src/app/fonts/GeistVF.woff -------------------------------------------------------------------------------- /postcss.config.mjs: -------------------------------------------------------------------------------- 1 | const config = { 2 | plugins: ["@tailwindcss/postcss"], 3 | }; 4 | 5 | export default config; 6 | -------------------------------------------------------------------------------- /src/app/fonts/GeistMonoVF.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jabirdev/nextjs-better-auth/HEAD/src/app/fonts/GeistMonoVF.woff -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/db/schema/auth/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./account"; 2 | export * from "./session"; 3 | export * from "./user"; 4 | export * from "./verification"; 5 | -------------------------------------------------------------------------------- /next.config.ts: -------------------------------------------------------------------------------- 1 | import type { NextConfig } from "next"; 2 | 3 | const nextConfig: NextConfig = { 4 | /* config options here */ 5 | }; 6 | 7 | export default nextConfig; 8 | -------------------------------------------------------------------------------- /src/app/api/auth/[...all]/route.ts: -------------------------------------------------------------------------------- 1 | import { auth } from "@/lib/auth/server"; 2 | import { toNextJsHandler } from "better-auth/next-js"; 3 | 4 | export const { POST, GET } = toNextJsHandler(auth); 5 | -------------------------------------------------------------------------------- /src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { clsx, type ClassValue } from "clsx" 2 | import { twMerge } from "tailwind-merge" 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)) 6 | } 7 | -------------------------------------------------------------------------------- /src/db/index.ts: -------------------------------------------------------------------------------- 1 | import * as schema from "./schema"; 2 | import postgres from "postgres"; 3 | import { drizzle } from "drizzle-orm/postgres-js"; 4 | 5 | const client = postgres(process.env.DATABASE_URL!); 6 | export const db = drizzle(client, { schema }); 7 | -------------------------------------------------------------------------------- /src/lib/auth/get-session.ts: -------------------------------------------------------------------------------- 1 | import { headers } from "next/headers"; 2 | import { cache } from "react"; 3 | import { auth } from "./server"; 4 | 5 | export const getServerSession = cache(async () => { 6 | return await auth.api.getSession({ headers: await headers() }); 7 | }); 8 | -------------------------------------------------------------------------------- /src/routes.ts: -------------------------------------------------------------------------------- 1 | export const publicRoutes: string[] = ["/", "/about"]; 2 | 3 | export const authRoutes: string[] = ["/signin", "/signup", "/forgot-password"]; 4 | 5 | export const apiAuthPrefix: string = "/api/auth"; 6 | 7 | export const DEFAULT_LOGIN_REDIRECT: string = "/"; 8 | -------------------------------------------------------------------------------- /drizzle.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "drizzle-kit"; 2 | 3 | export default defineConfig({ 4 | schema: "./src/db/schema/index.ts", 5 | out: "./drizzle", 6 | dialect: "postgresql", 7 | dbCredentials: { 8 | url: process.env.DIRECT_URL!, 9 | }, 10 | verbose: true, 11 | strict: true, 12 | }); 13 | -------------------------------------------------------------------------------- /src/app/(routes)/(auth)/signin/validate.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | 3 | export const SignInSchema = z.object({ 4 | username: z.string().min(4, { message: "Username is required" }), 5 | password: z 6 | .string() 7 | .min(6, { message: "Password lenght at least 6 characters" }), 8 | }); 9 | 10 | export type SignInValues = z.infer; 11 | -------------------------------------------------------------------------------- /public/window.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/file.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/providers/index.tsx: -------------------------------------------------------------------------------- 1 | import { Toaster } from "@/components/ui/sonner"; 2 | import NextTopLoader from "nextjs-toploader"; 3 | 4 | export default function Providers({ children }: { children: React.ReactNode }) { 5 | return ( 6 | <> 7 | 8 | {children} 9 | 10 | 11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /src/lib/auth/client.ts: -------------------------------------------------------------------------------- 1 | import { createAuthClient } from "better-auth/react"; 2 | import { usernameClient } from "better-auth/client/plugins"; 3 | import { nextCookies } from "better-auth/next-js"; 4 | 5 | export const { signIn, signUp, signOut, useSession, getSession } = 6 | createAuthClient({ 7 | baseURL: process.env.NEXT_PUBLIC_BASE_URL!, 8 | emailAndPassword: { 9 | enabled: true, 10 | }, 11 | plugins: [usernameClient(), nextCookies()], 12 | }); 13 | -------------------------------------------------------------------------------- /env.example: -------------------------------------------------------------------------------- 1 | # Connect to Supabase via connection pooling with Supavisor. 2 | DATABASE_URL="postgres://postgres.[YOUR_DATABASE]:[YOUR_PASSWORD]@aws-0-ap-southeast-1.pooler.supabase.com:6543/postgres?pgbouncer=true" 3 | 4 | # Direct connection to the database. Used for migrations. 5 | DIRECT_URL="postgres://postgres.[YOUR_DATABASE]:[YOUR_PASSWORD]@aws-0-ap-southeast-1.pooler.supabase.com:5432/postgres" 6 | 7 | # Better Auth 8 | BETTER_AUTH_SECRET="secret_key" 9 | NEXT_PUBLIC_BASE_URL="http://localhost:3000" -------------------------------------------------------------------------------- /src/db/schema/auth/verification.ts: -------------------------------------------------------------------------------- 1 | import { pgTable, text, timestamp } from "drizzle-orm/pg-core"; 2 | 3 | export const verification = pgTable("verification", { 4 | id: text("id").primaryKey(), 5 | identifier: text("identifier").notNull(), 6 | value: text("value").notNull(), 7 | expiresAt: timestamp("expiresAt").notNull(), 8 | createdAt: timestamp("createdAt").defaultNow(), 9 | updatedAt: timestamp("updatedAt") 10 | .defaultNow() 11 | .$onUpdate(() => new Date()), 12 | }).enableRLS(); 13 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "", 8 | "css": "src/app/globals.css", 9 | "baseColor": "neutral", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils", 16 | "ui": "@/components/ui", 17 | "lib": "@/lib", 18 | "hooks": "@/hooks" 19 | }, 20 | "iconLibrary": "lucide" 21 | } -------------------------------------------------------------------------------- /src/lib/auth/password.ts: -------------------------------------------------------------------------------- 1 | import z from "zod/v4"; 2 | 3 | export const passwordSchema = z 4 | .string() 5 | .min(8, { 6 | message: "Password must be at least 8 characters long.", 7 | }) 8 | .regex(/[A-Z]/, { 9 | message: "Password must contain at least one uppercase letter.", 10 | }) 11 | .regex(/[a-z]/, { 12 | message: "Password must contain at least one lowercase letter.", 13 | }) 14 | .regex(/[0-9]/, { 15 | message: "Password must contain at least one number.", 16 | }) 17 | .regex(/[^A-Za-z0-9]/, { 18 | message: "Password must contain at least one symbol.", 19 | }); 20 | -------------------------------------------------------------------------------- /src/db/schema/auth/session.ts: -------------------------------------------------------------------------------- 1 | import { pgTable, text, timestamp } from "drizzle-orm/pg-core"; 2 | import { user } from ".."; 3 | 4 | export const session = pgTable("session", { 5 | id: text("id").primaryKey(), 6 | expiresAt: timestamp("expiresAt").notNull(), 7 | token: text("token").notNull().unique(), 8 | createdAt: timestamp("createdAt").defaultNow(), 9 | updatedAt: timestamp("updatedAt") 10 | .defaultNow() 11 | .$onUpdate(() => new Date()), 12 | ipAddress: text("ipAddress"), 13 | userAgent: text("userAgent"), 14 | userId: text("userId") 15 | .notNull() 16 | .references(() => user.id), 17 | }).enableRLS(); 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.* 7 | .yarn/* 8 | !.yarn/patches 9 | !.yarn/plugins 10 | !.yarn/releases 11 | !.yarn/versions 12 | 13 | # testing 14 | /coverage 15 | 16 | # next.js 17 | /.next/ 18 | /out/ 19 | 20 | # production 21 | /build 22 | 23 | # misc 24 | .DS_Store 25 | *.pem 26 | 27 | # debug 28 | npm-debug.log* 29 | yarn-debug.log* 30 | yarn-error.log* 31 | 32 | # env files (can opt-in for commiting if needed) 33 | .env* 34 | 35 | # vercel 36 | .vercel 37 | 38 | # typescript 39 | *.tsbuildinfo 40 | next-env.d.ts 41 | -------------------------------------------------------------------------------- /src/app/(routes)/(auth)/components/input-start-icon.tsx: -------------------------------------------------------------------------------- 1 | import { LucideIcon } from "lucide-react"; 2 | 3 | export default function InputStartIcon({ 4 | children, 5 | icon: Icon, 6 | }: { 7 | children: React.ReactNode; 8 | icon: LucideIcon; 9 | }) { 10 | return ( 11 |
12 |
13 | {children} 14 |
15 |
17 |
18 |
19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /src/lib/auth/usernames.ts: -------------------------------------------------------------------------------- 1 | export const restrictedUsernames = [ 2 | "admin", 3 | "administrator", 4 | "root", 5 | "superadmin", 6 | "system", 7 | "null", 8 | "undefined", 9 | "support", 10 | "help", 11 | "contact", 12 | "info", 13 | "official", 14 | "owner", 15 | "moderator", 16 | "mod", 17 | "staff", 18 | "team", 19 | "server", 20 | "api", 21 | "email", 22 | "security", 23 | "test", 24 | "user", 25 | "users", 26 | "username", 27 | "guest", 28 | "webmaster", 29 | "manager", 30 | "operator", 31 | "dev", 32 | "developer", 33 | "emasaji", 34 | "me", 35 | "you", 36 | "bot", 37 | "god", 38 | "jesus", 39 | "allah", 40 | "cakfan", 41 | ]; 42 | -------------------------------------------------------------------------------- /src/components/ui/label.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as LabelPrimitive from "@radix-ui/react-label" 5 | 6 | import { cn } from "@/lib/utils" 7 | 8 | function Label({ 9 | className, 10 | ...props 11 | }: React.ComponentProps) { 12 | return ( 13 | 21 | ) 22 | } 23 | 24 | export { Label } 25 | -------------------------------------------------------------------------------- /src/db/schema/auth/user.ts: -------------------------------------------------------------------------------- 1 | import { boolean, pgTable, text, timestamp } from "drizzle-orm/pg-core"; 2 | 3 | export const user = pgTable("user", { 4 | id: text("id").primaryKey(), 5 | name: text("name").notNull(), 6 | username: text("username").unique(), 7 | displayUsername: text("display_username"), 8 | email: text("email").notNull().unique(), 9 | emailVerified: boolean("emailVerified").notNull(), 10 | image: text("image"), 11 | role: text("role").default("member").notNull(), 12 | gender: boolean("gender").notNull(), 13 | createdAt: timestamp("createdAt").defaultNow(), 14 | updatedAt: timestamp("updatedAt") 15 | .defaultNow() 16 | .$onUpdate(() => new Date()), 17 | }).enableRLS(); 18 | 19 | export type UserType = typeof user.$inferSelect; 20 | -------------------------------------------------------------------------------- /src/app/(routes)/(auth)/components/button-signout.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useState } from "react"; 4 | import { signOut } from "@/lib/auth/client"; 5 | import { redirect } from "next/navigation"; 6 | import { Button } from "@/components/ui/button"; 7 | 8 | export default function SignOutButton() { 9 | const [isPending, setIsPending] = useState(false); 10 | 11 | const onSignOut = async () => { 12 | setIsPending(true); 13 | await signOut({ 14 | fetchOptions: { 15 | onSuccess: () => { 16 | setIsPending(false); 17 | redirect("/"); 18 | }, 19 | }, 20 | }); 21 | }; 22 | 23 | return ( 24 | 27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /src/db/schema/auth/account.ts: -------------------------------------------------------------------------------- 1 | import { pgTable, text, timestamp } from "drizzle-orm/pg-core"; 2 | import { user } from ".."; 3 | 4 | export const account = pgTable("account", { 5 | id: text("id").primaryKey(), 6 | accountId: text("accountId").notNull(), 7 | providerId: text("providerId").notNull(), 8 | userId: text("userId") 9 | .notNull() 10 | .references(() => user.id), 11 | accessToken: text("accessToken"), 12 | refreshToken: text("refreshToken"), 13 | idToken: text("idToken"), 14 | accessTokenExpiresAt: timestamp("accessTokenExpiresAt"), 15 | refreshTokenExpiresAt: timestamp("refreshTokenExpiresAt"), 16 | scope: text("scope"), 17 | password: text("password"), 18 | createdAt: timestamp("createdAt").defaultNow(), 19 | updatedAt: timestamp("updatedAt") 20 | .defaultNow() 21 | .$onUpdate(() => new Date()), 22 | }).enableRLS(); 23 | -------------------------------------------------------------------------------- /src/app/(routes)/(auth)/signin/page.tsx: -------------------------------------------------------------------------------- 1 | import { type Metadata } from "next"; 2 | import SignInForm from "./form"; 3 | import Link from "next/link"; 4 | 5 | export const metadata: Metadata = { 6 | title: "Sign In", 7 | }; 8 | 9 | export default function SignInPage() { 10 | return ( 11 |
12 |
13 |

Sign In

14 |

Example sign in page using Better Auth

15 | 16 |
17 | Don't have account? 18 | 19 | Sign Up 20 | 21 |
22 |
23 |
24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /src/app/(routes)/(auth)/signup/page.tsx: -------------------------------------------------------------------------------- 1 | import { type Metadata } from "next"; 2 | import Link from "next/link"; 3 | import SignUpForm from "./form"; 4 | 5 | export const metadata: Metadata = { 6 | title: "Sign Up", 7 | }; 8 | 9 | export default function SignUpPage() { 10 | return ( 11 |
12 |
13 |

Sign Up

14 |

Example sign up page using Better Auth

15 | 16 |
17 | Already have account? 18 | 19 | Sign In 20 | 21 |
22 |
23 |
24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | import localFont from "next/font/local"; 3 | import "./globals.css"; 4 | import Providers from "@/providers"; 5 | 6 | const geistSans = localFont({ 7 | src: "./fonts/GeistVF.woff", 8 | variable: "--font-geist-sans", 9 | weight: "100 900", 10 | }); 11 | const geistMono = localFont({ 12 | src: "./fonts/GeistMonoVF.woff", 13 | variable: "--font-geist-mono", 14 | weight: "100 900", 15 | }); 16 | 17 | export const metadata: Metadata = { 18 | title: "Create Next App", 19 | description: "Generated by create next app", 20 | }; 21 | 22 | export default function RootLayout({ 23 | children, 24 | }: Readonly<{ 25 | children: React.ReactNode; 26 | }>) { 27 | return ( 28 | 29 | 32 | {children} 33 | 34 | 35 | ); 36 | } 37 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2017", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "strict": true, 12 | "noEmit": true, 13 | "esModuleInterop": true, 14 | "module": "esnext", 15 | "moduleResolution": "bundler", 16 | "resolveJsonModule": true, 17 | "isolatedModules": true, 18 | "jsx": "react-jsx", 19 | "incremental": true, 20 | "plugins": [ 21 | { 22 | "name": "next" 23 | } 24 | ], 25 | "paths": { 26 | "@/*": [ 27 | "./src/*" 28 | ] 29 | } 30 | }, 31 | "include": [ 32 | "next-env.d.ts", 33 | "**/*.ts", 34 | "**/*.tsx", 35 | ".next/types/**/*.ts", 36 | ".next\\dev/types/**/*.ts", 37 | ".next\\dev/types/**/*.ts", 38 | ".next/dev/types/**/*.ts", 39 | ".next/dev/dev/types/**/*.ts" 40 | ], 41 | "exclude": [ 42 | "node_modules" 43 | ] 44 | } 45 | -------------------------------------------------------------------------------- /public/globe.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/ui/sonner.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { useTheme } from "next-themes" 4 | import { Toaster as Sonner, ToasterProps } from "sonner" 5 | 6 | const Toaster = ({ ...props }: ToasterProps) => { 7 | const { theme = "system" } = useTheme() 8 | 9 | return ( 10 | 26 | ) 27 | } 28 | 29 | export { Toaster } 30 | -------------------------------------------------------------------------------- /src/lib/auth/server.ts: -------------------------------------------------------------------------------- 1 | import { db } from "@/db"; 2 | import { betterAuth } from "better-auth"; 3 | import { username } from "better-auth/plugins"; 4 | import { drizzleAdapter } from "better-auth/adapters/drizzle"; 5 | import { restrictedUsernames } from "./usernames"; 6 | 7 | export const auth = betterAuth({ 8 | database: drizzleAdapter(db, { 9 | provider: "pg", 10 | }), 11 | plugins: [username({ 12 | minUsernameLength: 4, 13 | maxUsernameLength: 10, 14 | usernameValidator: (value) => !restrictedUsernames.includes(value), 15 | usernameNormalization: (value) => value.toLowerCase(), 16 | })], 17 | emailAndPassword: { 18 | enabled: true, 19 | }, 20 | user: { 21 | additionalFields: { 22 | role: { 23 | type: "string", 24 | defaultValue: "user", 25 | required: false, 26 | input: false, 27 | }, 28 | gender: { 29 | type: "boolean", 30 | required: true, 31 | input: true, 32 | }, 33 | }, 34 | }, 35 | }); 36 | -------------------------------------------------------------------------------- /src/components/ui/input.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | function Input({ className, type, ...props }: React.ComponentProps<"input">) { 6 | return ( 7 | 18 | ) 19 | } 20 | 21 | export { Input } 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Jabir Developer 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/(routes)/(auth)/signup/validate.ts: -------------------------------------------------------------------------------- 1 | import { passwordSchema } from "@/lib/auth/password"; 2 | import { restrictedUsernames } from "@/lib/auth/usernames"; 3 | import { z } from "zod"; 4 | 5 | export const SignUpSchema = z 6 | .object({ 7 | email: z 8 | .email({ message: "Invalid email address" }) 9 | .min(1, { message: "Email is required" }), 10 | name: z.string().min(4, { message: "Must be at least 4 characters" }), 11 | username: z 12 | .string() 13 | .min(4, { message: "Must be at least 4 characters" }) 14 | .regex(/^[a-zA-Z0-9]+$/, "Only letters and numbers allowed") 15 | .refine( 16 | (username) => { 17 | for (const pattern of restrictedUsernames) { 18 | if (username.toLowerCase().includes(pattern)) { 19 | return false; 20 | } 21 | } 22 | return true; 23 | }, 24 | { message: "Username contains disallowed words" } 25 | ), 26 | password: passwordSchema, 27 | confirmPassword: z.string().min(8, { 28 | message: "Must be at least 8 characters", 29 | }), 30 | gender: z.boolean().nonoptional(), 31 | }) 32 | .refine((data) => data.password === data.confirmPassword, { 33 | message: "Passwords don't match", 34 | path: ["confirmPassword"], 35 | }); 36 | 37 | export type SignUpValues = z.infer; 38 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nextjs-15", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev --turbopack", 7 | "build": "next build --turbopack", 8 | "start": "next start", 9 | "lint": "next lint", 10 | "db:generate": "drizzle-kit generate", 11 | "db:migrate": "drizzle-kit push", 12 | "db:studio": "drizzle-kit studio" 13 | }, 14 | "dependencies": { 15 | "@hookform/resolvers": "^5.2.2", 16 | "@radix-ui/react-label": "^2.1.7", 17 | "@radix-ui/react-radio-group": "^1.3.8", 18 | "@radix-ui/react-slot": "^1.2.3", 19 | "better-auth": "^1.3.32", 20 | "class-variance-authority": "^0.7.1", 21 | "clsx": "^2.1.1", 22 | "drizzle-orm": "^0.44.7", 23 | "drizzle-zod": "^0.8.3", 24 | "lucide-react": "^0.548.0", 25 | "next": "^16.0.0", 26 | "next-themes": "^0.4.6", 27 | "nextjs-toploader": "^3.9.17", 28 | "postgres": "^3.4.7", 29 | "react": "^19.2.0", 30 | "react-dom": "^19.2.0", 31 | "react-hook-form": "^7.65.0", 32 | "sonner": "^2.0.7", 33 | "tailwind-merge": "^3.3.1", 34 | "tailwindcss-animate": "^1.0.7", 35 | "zod": "^4.1.12" 36 | }, 37 | "devDependencies": { 38 | "@tailwindcss/postcss": "^4.1.16", 39 | "@types/node": "^24", 40 | "@types/react": "^19", 41 | "@types/react-dom": "^19", 42 | "drizzle-kit": "^0.31.5", 43 | "eslint": "^9", 44 | "eslint-config-next": "^16.0.0", 45 | "postcss": "^8", 46 | "prettier-plugin-tailwindcss": "^0.7.1", 47 | "tailwindcss": "^4.1.16", 48 | "typescript": "^5" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/proxy.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse, type NextRequest } from "next/server"; 2 | import { getSessionCookie } from "better-auth/cookies"; 3 | 4 | import { 5 | apiAuthPrefix, 6 | authRoutes, 7 | DEFAULT_LOGIN_REDIRECT, 8 | publicRoutes, 9 | } from "./routes"; 10 | 11 | export async function proxy(request: NextRequest) { 12 | const session = getSessionCookie(request); 13 | 14 | const isApiAuth = request.nextUrl.pathname.startsWith(apiAuthPrefix); 15 | 16 | const isPublicRoute = publicRoutes.includes(request.nextUrl.pathname); 17 | 18 | const isAuthRoute = () => { 19 | return authRoutes.some((path) => request.nextUrl.pathname.startsWith(path)); 20 | }; 21 | 22 | if (isApiAuth) { 23 | return NextResponse.next(); 24 | } 25 | 26 | if (isAuthRoute()) { 27 | if (session) { 28 | return NextResponse.redirect( 29 | new URL(DEFAULT_LOGIN_REDIRECT, request.url), 30 | ); 31 | } 32 | return NextResponse.next(); 33 | } 34 | 35 | if (!session && !isPublicRoute) { 36 | return NextResponse.redirect(new URL("/login", request.url)); 37 | } 38 | 39 | return NextResponse.next(); 40 | } 41 | 42 | export const config = { 43 | matcher: [ 44 | /* 45 | * Match all request paths except for the ones starting with: 46 | * - _next/static (static files) 47 | * - _next/image (image optimization files) 48 | * - favicon.ico (favicon file) 49 | * Feel free to modify this pattern to include more paths. 50 | */ 51 | "/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)", 52 | ], 53 | }; 54 | -------------------------------------------------------------------------------- /src/app/(routes)/(auth)/components/gender-radio-group.tsx: -------------------------------------------------------------------------------- 1 | import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; 2 | import { Label } from "@/components/ui/label"; 3 | import { cn } from "@/lib/utils"; 4 | 5 | const options = [ 6 | { id: "male", label: "Male", value: false }, 7 | { id: "female", label: "Female", value: true }, 8 | ]; 9 | 10 | export function GenderRadioGroup({ 11 | value, 12 | onChange, 13 | }: { 14 | value: boolean; 15 | onChange: (val: boolean) => void; 16 | }) { 17 | return ( 18 | onChange(val === "true")} 21 | className="grid grid-cols-3 gap-4" 22 | > 23 | {options.map((opt) => ( 24 |
33 | 38 | 44 |
45 | ))} 46 |
47 | ); 48 | } 49 | -------------------------------------------------------------------------------- /src/app/(routes)/(auth)/components/input-password.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Eye, EyeOff } from "lucide-react"; 4 | import { cloneElement, useState, ReactElement, isValidElement } from "react"; 5 | 6 | interface InputPasswordContainerProps { 7 | children: ReactElement<{ type?: string }>; 8 | } 9 | 10 | export default function InputPasswordContainer({ 11 | children, 12 | }: InputPasswordContainerProps) { 13 | const [isVisible, setIsVisible] = useState(false); 14 | 15 | const toggleVisibility = () => setIsVisible((prevState) => !prevState); 16 | 17 | return ( 18 |
19 |
20 | {isValidElement(children) && 21 | cloneElement(children, { 22 | type: isVisible ? "text" : "password", 23 | })} 24 | 37 |
38 |
39 | ); 40 | } 41 | -------------------------------------------------------------------------------- /src/components/ui/radio-group.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as RadioGroupPrimitive from "@radix-ui/react-radio-group" 5 | import { CircleIcon } from "lucide-react" 6 | 7 | import { cn } from "@/lib/utils" 8 | 9 | function RadioGroup({ 10 | className, 11 | ...props 12 | }: React.ComponentProps) { 13 | return ( 14 | 19 | ) 20 | } 21 | 22 | function RadioGroupItem({ 23 | className, 24 | ...props 25 | }: React.ComponentProps) { 26 | return ( 27 | 35 | 39 | 40 | 41 | 42 | ) 43 | } 44 | 45 | export { RadioGroup, RadioGroupItem } 46 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Next.js 16 Starter Template 2 | 3 | A powerful starter template for Next.js 16 projects, featuring: 4 | 5 | - **Better Auth**: Seamless and secure authentication. 6 | - **Drizzle ORM**: Elegant and type-safe database management. 7 | - **Supabase**: Robust backend services for your application. 8 | 9 | ## Features 10 | 11 | - Pre-configured authentication with Better Auth. 12 | - Integrated Drizzle ORM for easy database interactions. 13 | - Ready-to-use Supabase setup. 14 | - Scalable and modern tech stack. 15 | 16 | ## Getting Started 17 | 18 | Follow these steps to set up the project: 19 | 20 | ### 1\. Clone the Repository 21 | 22 | ``` 23 | git clone https://github.com/JabirDev/nextjs-better-auth.git 24 | cd nextjs-better-auth 25 | ``` 26 | 27 | ### 2\. Install Dependencies 28 | 29 | Make sure you have Node.js installed, then run: 30 | 31 | ``` 32 | bun install 33 | ``` 34 | 35 | ### 3\. Configure Environment Variables 36 | 37 | Copy the env.example file to create your .env file: 38 | 39 | ``` 40 | cp env.example .env 41 | ``` 42 | 43 | Edit the `.env` file with your project's specific configurations: 44 | 45 | - Add your Supabase keys and URLs. 46 | - Configure any required authentication secrets. 47 | 48 | ### 4\. Setup Drizzle ORM 49 | 50 | Generate your Drizzle schema and push into your database: 51 | 52 | ``` 53 | bun db:push 54 | ``` 55 | 56 | ### 5\. Start the Development Server 57 | 58 | Run the development server: 59 | 60 | ``` 61 | bun dev 62 | ``` 63 | 64 | Your application will be available at [http://localhost:3000](http://localhost:3000). 65 | 66 | ## Contributing 67 | 68 | Contributions are welcome! Feel free to: 69 | 70 | - Open issues for bugs or feature requests. 71 | - Submit pull requests to improve the project. 72 | 73 | ### License 74 | 75 | This project is licensed under the MIT License. 76 | -------------------------------------------------------------------------------- /src/components/ui/button.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { Slot } from "@radix-ui/react-slot" 3 | import { cva, type VariantProps } from "class-variance-authority" 4 | 5 | import { cn } from "@/lib/utils" 6 | 7 | const buttonVariants = cva( 8 | "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-[color,box-shadow] disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", 9 | { 10 | variants: { 11 | variant: { 12 | default: 13 | "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90", 14 | destructive: 15 | "bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40", 16 | outline: 17 | "border border-input bg-background shadow-xs hover:bg-accent hover:text-accent-foreground", 18 | secondary: 19 | "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80", 20 | ghost: "hover:bg-accent hover:text-accent-foreground", 21 | link: "text-primary underline-offset-4 hover:underline", 22 | }, 23 | size: { 24 | default: "h-9 px-4 py-2 has-[>svg]:px-3", 25 | sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5", 26 | lg: "h-10 rounded-md px-6 has-[>svg]:px-4", 27 | icon: "size-9", 28 | }, 29 | }, 30 | defaultVariants: { 31 | variant: "default", 32 | size: "default", 33 | }, 34 | } 35 | ) 36 | 37 | function Button({ 38 | className, 39 | variant, 40 | size, 41 | asChild = false, 42 | ...props 43 | }: React.ComponentProps<"button"> & 44 | VariantProps & { 45 | asChild?: boolean 46 | }) { 47 | const Comp = asChild ? Slot : "button" 48 | 49 | return ( 50 | 55 | ) 56 | } 57 | 58 | export { Button, buttonVariants } 59 | -------------------------------------------------------------------------------- /src/app/(routes)/(auth)/signin/form.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { 4 | Form, 5 | FormControl, 6 | FormField, 7 | FormItem, 8 | FormMessage, 9 | } from "@/components/ui/form"; 10 | import { useTransition } from "react"; 11 | import { useForm } from "react-hook-form"; 12 | import { zodResolver } from "@hookform/resolvers/zod"; 13 | import { signIn } from "@/lib/auth/client"; 14 | import { useRouter } from "next/navigation"; 15 | import { Button } from "@/components/ui/button"; 16 | import { Input } from "@/components/ui/input"; 17 | import { toast } from "sonner"; 18 | import { SignInSchema, SignInValues } from "./validate"; 19 | import InputStartIcon from "../components/input-start-icon"; 20 | import InputPasswordContainer from "../components/input-password"; 21 | import { cn } from "@/lib/utils"; 22 | import { AtSign } from "lucide-react"; 23 | 24 | export default function SignInForm() { 25 | const [isPending, startTransition] = useTransition(); 26 | const router = useRouter(); 27 | 28 | const form = useForm({ 29 | resolver: zodResolver(SignInSchema), 30 | defaultValues: { 31 | username: "", 32 | password: "", 33 | }, 34 | }); 35 | 36 | function onSubmit(data: SignInValues) { 37 | startTransition(async () => { 38 | const response = await signIn.username(data); 39 | 40 | if (response.error) { 41 | console.log("SIGN_IN:", response.error.message); 42 | toast.error(response.error.message); 43 | } else { 44 | router.push("/"); 45 | } 46 | }); 47 | } 48 | 49 | const getInputClassName = (fieldName: keyof SignInValues) => 50 | cn( 51 | form.formState.errors[fieldName] && 52 | "border-destructive/80 text-destructive focus-visible:border-destructive/80 focus-visible:ring-destructive/20", 53 | ); 54 | 55 | return ( 56 |
57 | 61 | ( 65 | 66 | 67 | 68 | 74 | 75 | 76 | 77 | 78 | )} 79 | /> 80 | 81 | ( 85 | 86 | 87 | 88 | 95 | 96 | 97 | 98 | 99 | )} 100 | /> 101 | 104 | 105 | 106 | ); 107 | } 108 | -------------------------------------------------------------------------------- /src/components/ui/form.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as LabelPrimitive from "@radix-ui/react-label" 5 | import { Slot } from "@radix-ui/react-slot" 6 | import { 7 | Controller, 8 | ControllerProps, 9 | FieldPath, 10 | FieldValues, 11 | FormProvider, 12 | useFormContext, 13 | useFormState, 14 | } from "react-hook-form" 15 | 16 | import { cn } from "@/lib/utils" 17 | import { Label } from "@/components/ui/label" 18 | 19 | const Form = FormProvider 20 | 21 | type FormFieldContextValue< 22 | TFieldValues extends FieldValues = FieldValues, 23 | TName extends FieldPath = FieldPath, 24 | > = { 25 | name: TName 26 | } 27 | 28 | const FormFieldContext = React.createContext( 29 | {} as FormFieldContextValue 30 | ) 31 | 32 | const FormField = < 33 | TFieldValues extends FieldValues = FieldValues, 34 | TName extends FieldPath = FieldPath, 35 | >({ 36 | ...props 37 | }: ControllerProps) => { 38 | return ( 39 | 40 | 41 | 42 | ) 43 | } 44 | 45 | const useFormField = () => { 46 | const fieldContext = React.useContext(FormFieldContext) 47 | const itemContext = React.useContext(FormItemContext) 48 | const { getFieldState } = useFormContext() 49 | const formState = useFormState({ name: fieldContext.name }) 50 | const fieldState = getFieldState(fieldContext.name, formState) 51 | 52 | if (!fieldContext) { 53 | throw new Error("useFormField should be used within ") 54 | } 55 | 56 | const { id } = itemContext 57 | 58 | return { 59 | id, 60 | name: fieldContext.name, 61 | formItemId: `${id}-form-item`, 62 | formDescriptionId: `${id}-form-item-description`, 63 | formMessageId: `${id}-form-item-message`, 64 | ...fieldState, 65 | } 66 | } 67 | 68 | type FormItemContextValue = { 69 | id: string 70 | } 71 | 72 | const FormItemContext = React.createContext( 73 | {} as FormItemContextValue 74 | ) 75 | 76 | function FormItem({ className, ...props }: React.ComponentProps<"div">) { 77 | const id = React.useId() 78 | 79 | return ( 80 | 81 |
86 | 87 | ) 88 | } 89 | 90 | function FormLabel({ 91 | className, 92 | ...props 93 | }: React.ComponentProps) { 94 | const { error, formItemId } = useFormField() 95 | 96 | return ( 97 |