Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/revokslab/shipfree/llms.txt

Use this file to discover all available pages before exploring further.

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:
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:
.env
GOOGLE_CLIENT_ID=your_client_id
GOOGLE_CLIENT_SECRET=your_client_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

'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>
}

Server-Side Usage

Access authentication in Server Components and API routes:
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>
  )
}

Database Schema

Better-Auth uses these tables (defined in src/database/schema.ts):
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

1

Session Creation

When a user signs in, Better-Auth creates a session with a secure token stored in a cookie.
2

Cookie Cache

Session data is cached in the cookie for 24 hours to reduce database queries.
3

Auto Refresh

Sessions are automatically refreshed every 24 hours if the user is active.
4

Expiration

Sessions expire after 30 days of inactivity and require re-authentication.

Session Configuration

src/lib/auth/auth.ts
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:
src/lib/auth/auth.ts
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:
src/lib/auth/auth.ts
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

Build docs developers (and LLMs) love