ShipFree uses Better-Auth as its authentication foundation, providing a flexible, type-safe authentication system with support for email/password, OAuth providers, magic links, and OTP verification.
Why Better-Auth?
Better-Auth was chosen for ShipFree because it provides:
Type-safe - Full TypeScript support with excellent IntelliSense
Framework-agnostic - Works with any backend, easily integrated with Next.js
Plugin system - Extensible architecture for adding features
Database flexibility - Supports multiple databases via Drizzle ORM
Modern authentication - Built-in support for passwordless, OAuth, and more
Authentication Methods
Email & Password Traditional authentication with optional email verification
OAuth Providers Google, GitHub, Microsoft, and Facebook sign-in
Email OTP Passwordless authentication via one-time codes
Magic Links Secure sign-in links sent to email
Configuration
The authentication system is configured in src/lib/auth/auth.ts:
import { betterAuth } from 'better-auth'
import { drizzleAdapter } from 'better-auth/adapters/drizzle'
import { emailOTP , organization } from 'better-auth/plugins'
export const auth = betterAuth ({
baseURL: getBaseUrl (),
database: drizzleAdapter ( db , {
provider: 'pg' ,
}),
session: {
cookieCache: {
enabled: true ,
maxAge: 24 * 60 * 60 , // 24 hours
},
expiresIn: 30 * 24 * 60 * 60 , // 30 days
updateAge: 24 * 60 * 60 , // Refresh every 24 hours
},
emailAndPassword: {
enabled: true ,
requireEmailVerification: isEmailVerificationEnabled ,
},
plugins: [
emailOTP ({
otpLength: 6 ,
expiresIn: 15 * 60 , // 15 minutes
}),
organization (),
],
})
OAuth Providers
Configure OAuth providers via environment variables:
Google
GitHub
Microsoft
Facebook
GOOGLE_CLIENT_ID = your_client_id
GOOGLE_CLIENT_SECRET = your_client_secret
GITHUB_CLIENT_ID = your_client_id
GITHUB_CLIENT_SECRET = your_client_secret
MICROSOFT_CLIENT_ID = your_client_id
MICROSOFT_CLIENT_SECRET = your_client_secret
MICROSOFT_TENANT_ID = common # or your tenant ID
FACEBOOK_CLIENT_ID = your_app_id
FACEBOOK_CLIENT_SECRET = your_app_secret
Client-Side Usage
Authentication Client
The auth client is configured in src/lib/auth/auth-client.ts:
src/lib/auth/auth-client.ts
import { createAuthClient } from 'better-auth/react'
import { emailOTPClient , organizationClient } from 'better-auth/client/plugins'
export const client = createAuthClient ({
baseURL: getBaseUrl (),
plugins: [ emailOTPClient (), organizationClient ()],
})
export const { signIn , signUp , signOut , useSession } = client
Using in React Components
Email/Password Sign In
OAuth Sign In
Email OTP
Get Session
'use client'
import { signIn } from '@/lib/auth'
export function LoginForm () {
const handleSubmit = async ( e : React . FormEvent ) => {
e . preventDefault ()
await signIn . email ({
email: 'user@example.com' ,
password: 'password' ,
})
}
return < form onSubmit = { handleSubmit } > { /* form fields */ } </ form >
}
'use client'
import { signIn } from '@/lib/auth'
export function OAuthButtons () {
return (
<>
< button onClick = { () => signIn . social ({ provider: 'google' }) } >
Sign in with Google
</ button >
< button onClick = { () => signIn . social ({ provider: 'github' }) } >
Sign in with GitHub
</ button >
</>
)
}
'use client'
import { client } from '@/lib/auth'
export function OTPLogin () {
// Step 1: Send OTP
const sendOTP = async ( email : string ) => {
await client . signIn . emailOtp ({ email })
}
// Step 2: Verify OTP
const verifyOTP = async ( email : string , otp : string ) => {
await client . signIn . emailOtp . verify ({ email , otp })
}
return <> { /* OTP form */ } </>
}
'use client'
import { useSession } from '@/lib/auth'
export function UserProfile () {
const { data : session , isPending } = useSession ()
if ( isPending ) return < div > Loading... </ div >
if ( ! session ) return < div > Not authenticated </ div >
return (
< div >
< p > Welcome, { session . user . name } </ p >
< p > Email: { session . user . email } </ p >
</ div >
)
}
Server-Side Usage
Access authentication in Server Components and API routes:
Server Component
API Route
Server Action
src/app/(main)/dashboard/page.tsx
import { auth } from '@/lib/auth'
import { headers } from 'next/headers'
export default async function DashboardPage () {
const session = await auth . api . getSession ({
headers: await headers (),
})
if ( ! session ) {
redirect ( '/login' )
}
return (
< div >
< h1 > Dashboard </ h1 >
< p > Welcome, { session . user . name } </ p >
</ div >
)
}
src/app/api/user/route.ts
import { auth } from '@/lib/auth'
import { NextRequest } from 'next/server'
export async function GET ( req : NextRequest ) {
const session = await auth . api . getSession ({
headers: req . headers ,
})
if ( ! session ) {
return Response . json ({ error: 'Unauthorized' }, { status: 401 })
}
return Response . json ({ user: session . user })
}
'use server'
import { auth } from '@/lib/auth'
import { headers } from 'next/headers'
export async function updateProfile ( formData : FormData ) {
const session = await auth . api . getSession ({
headers: await headers (),
})
if ( ! session ) {
throw new Error ( 'Unauthorized' )
}
// Update user profile
}
Database Schema
Better-Auth uses these tables (defined in src/database/schema.ts):
export const user = pgTable ( 'user' , {
id: text ( 'id' ). primaryKey (),
name: text ( 'name' ). notNull (),
email: text ( 'email' ). notNull (). unique (),
emailVerified: boolean ( 'email_verified' ). default ( false ). notNull (),
image: text ( 'image' ),
createdAt: timestamp ( 'created_at' ). defaultNow (). notNull (),
updatedAt: timestamp ( 'updated_at' ). defaultNow (). notNull (),
})
export const session = pgTable ( 'session' , {
id: text ( 'id' ). primaryKey (),
expiresAt: timestamp ( 'expires_at' ). notNull (),
token: text ( 'token' ). notNull (). unique (),
userId: text ( 'user_id' ). notNull (). references (() => user . id ),
ipAddress: text ( 'ip_address' ),
userAgent: text ( 'user_agent' ),
})
export const account = pgTable ( 'account' , {
id: text ( 'id' ). primaryKey (),
accountId: text ( 'account_id' ). notNull (),
providerId: text ( 'provider_id' ). notNull (),
userId: text ( 'user_id' ). notNull (). references (() => user . id ),
accessToken: text ( 'access_token' ),
refreshToken: text ( 'refresh_token' ),
password: text ( 'password' ),
})
export const verification = pgTable ( 'verification' , {
id: text ( 'id' ). primaryKey (),
identifier: text ( 'identifier' ). notNull (),
value: text ( 'value' ). notNull (),
expiresAt: timestamp ( 'expires_at' ). notNull (),
})
Session Management
Session Creation
When a user signs in, Better-Auth creates a session with a secure token stored in a cookie.
Cookie Cache
Session data is cached in the cookie for 24 hours to reduce database queries.
Auto Refresh
Sessions are automatically refreshed every 24 hours if the user is active.
Expiration
Sessions expire after 30 days of inactivity and require re-authentication.
Session Configuration
session : {
cookieCache : {
enabled : true ,
maxAge : 24 * 60 * 60 , // Cache for 24 hours
},
expiresIn : 30 * 24 * 60 * 60 , // Session lasts 30 days
updateAge : 24 * 60 * 60 , // Refresh every 24 hours
freshAge : 60 * 60 , // Consider session fresh for 1 hour
}
Email Verification
Email verification can be enabled/disabled via the EMAIL_VERIFICATION_ENABLED feature flag in src/config/feature-flags.ts.
When enabled, users must verify their email before accessing the application:
emailAndPassword : {
enabled : true ,
requireEmailVerification : isEmailVerificationEnabled ,
sendResetPassword : async ({ user , url }) => {
const html = await renderPasswordResetEmail ( user . name || '' , url )
await sendEmail ({
to: user . email ,
subject: 'Reset Your Password' ,
html ,
from: getFromEmailAddress (),
emailType: 'transactional' ,
})
},
}
Organization Support
Better-Auth includes built-in organization/team support:
plugins : [
organization ({
organizationCreation: {
afterCreate : async ({ organization , user }) => {
console . info ( 'Organization created' , {
organizationId: organization . id ,
creatorId: user . id ,
})
},
},
}),
]
You can restrict organization creation based on subscription plan by implementing the allowUserToCreateOrganization callback.
Security Best Practices
Secure Cookies Sessions are stored in HTTP-only, secure cookies in production
CSRF Protection Built-in CSRF protection for all authentication requests
Rate Limiting Consider adding rate limiting to prevent brute-force attacks
Password Hashing Passwords are hashed using bcrypt before storage
Further Reading
Better-Auth Docs Official Better-Auth documentation
Plugin System Learn about available plugins
OAuth Providers Configure additional OAuth providers
Custom Fields Add custom fields to user schema