Documentation

Authentication

Learn how to set up and use Better Auth for authentication in your Sushify Next.js application

Sushify Next.js uses Better Auth for authentication. Better Auth is a modern, type-safe authentication library that provides a comprehensive authentication solution with support for multiple authentication methods, session management, and organization features.

Features

  • Email & Password Authentication: Traditional email/password login with verification
  • Magic Link: Passwordless authentication via email
  • Social Providers: OAuth login with Google and GitHub
  • Passkeys: WebAuthn support for passwordless authentication
  • Two-Factor Authentication: Optional 2FA for enhanced security
  • Organization Support: Multi-tenant authentication with organization management
  • Admin Plugin: Administrative controls and user management
  • Username Support: Optional username-based authentication

Setup

1. Environment Variables

First, configure the required environment variables in your .env.local file:

.env.local
# Required: Authentication Secret
BETTER_AUTH_SECRET="your-random-secret-string"

# Optional: OAuth Providers
GOOGLE_CLIENT_ID="your-google-client-id"
GOOGLE_CLIENT_SECRET="your-google-client-secret"

GITHUB_CLIENT_ID="your-github-client-id"
GITHUB_CLIENT_SECRET="your-github-client-secret"

Generate a secure BETTER_AUTH_SECRET:

openssl rand -base64 32

2. OAuth Provider Setup (Optional)

If you want to enable social login, you'll need to configure OAuth providers:

Google OAuth

  1. Go to Google Cloud Console
  2. Create a new project or select an existing one
  3. Navigate to "APIs & Services" → "Credentials"
  4. Click "Create Credentials" → "OAuth client ID"
  5. Choose "Web application"
  6. Add authorized redirect URIs:
    • Development: http://localhost:3000/api/auth/callback/google
    • Production: https://yourdomain.com/api/auth/callback/google
  7. Copy the Client ID and Client Secret to your .env.local

GitHub OAuth

  1. Go to GitHub Developer Settings
  2. Click "OAuth Apps" and then "New OAuth App"
  3. Fill in the application details:
    • Application name: Your application name
    • Homepage URL: Your application URL
    • Authorization callback URL:
      • Development: http://localhost:3000/api/auth/callback/github
      • Production: https://yourdomain.com/api/auth/callback/github
  4. Copy the Client ID and generate a Client Secret
  5. Add them to your .env.local

3. Database Setup

Better Auth requires database tables for storing users, sessions, and accounts. The schema is already included in the Prisma schema.

If you have already set up your database and the schema hasn't been modified, you can skip this step. Otherwise, run migrations to create the necessary tables:

pnpm db:migrate

For detailed database setup instructions, see the Database documentation.

4. Configuration

The authentication configuration is located in packages/auth/auth.ts Here's an overview of the main configuration:

packages/auth/auth.ts
export const auth = betterAuth({
  baseURL: appUrl,
  trustedOrigins: [appUrl],
  appName: config.appName,

  // Database adapter
  database: prismaAdapter(db, {
    provider: "postgresql",
  }),

  // Session configuration
  session: {
    expiresIn: config.auth.sessionCookieMaxAge,
    freshAge: 0,
  },

  // Account linking
  account: {
    accountLinking: {
      enabled: true,
      trustedProviders: ["google", "github"],
    },
  },

  // Email & Password
  emailAndPassword: {
    enabled: true,
    autoSignIn: !config.auth.enableSignup,
    requireEmailVerification: config.auth.enableSignup,
  },

  // Social providers
  socialProviders: {
    google: {
      clientId: process.env.GOOGLE_CLIENT_ID,
      clientSecret: process.env.GOOGLE_CLIENT_SECRET,
    },
    github: {
      clientId: process.env.GITHUB_CLIENT_ID,
      clientSecret: process.env.GITHUB_CLIENT_SECRET,
    },
  },

  // Plugins
  plugins: [
    username(),
    admin(),
    passkey(),
    magicLink(),
    organization(),
    twoFactor(),
  ],
});

You can customize authentication behavior in config/index.ts:

config/index.ts
auth: {
  // Whether users can create accounts
  enableSignup: true,
  // Whether magic link authentication is enabled
  enableMagicLink: true,
  // Whether social login is enabled
  enableSocialLogin: true,
  // Whether passkey authentication is enabled
  enablePasskeys: true,
  // Whether password login is enabled
  enablePasswordLogin: true,
  // Whether two-factor authentication is enabled
  enableTwoFactor: true,
  // Redirect path after successful sign in
  redirectAfterSignIn: "/app",
  // Redirect path after logout
  redirectAfterLogout: "/",
  // Session duration (in seconds)
  sessionCookieMaxAge: 60 * 60 * 24 * 30, // 30 days
}

Usage

Client-Side

The auth client is located at packages/auth/client.ts and provides React hooks and functions for authentication.

Using Session Hook

import { useSession } from "@saas/auth/hooks/use-session";

function MyComponent() {
  const { user, session, loaded, reloadSession } = useSession();

  if (!loaded) {
    return <div>Loading...</div>;
  }

  if (!user) {
    return <div>Not authenticated</div>;
  }

  return (
    <div>
      <p>Welcome, {user.name}!</p>
      <p>Email: {user.email}</p>
    </div>
  );
}

Server-Side

For server-side authentication, use the auth API from apps/web/modules/saas/auth/lib/server.ts.

Get Session in Server Components

import { getSession } from "@saas/auth/lib/server";

export default async function ServerComponent() {
  const session = await getSession();

  if (!session?.user) {
    redirect("/auth/login");
  }

  return (
    <div>
      <h1>Welcome, {session.user.name}!</h1>
    </div>
  );
}

Get Active Organization

import { getActiveOrganization } from "@saas/auth/lib/server";

export default async function OrganizationPage({
  params
}: {
  params: { slug: string }
}) {
  const organization = await getActiveOrganization(params.slug);

  if (!organization) {
    notFound();
  }

  return (
    <div>
      <h1>{organization.name}</h1>
    </div>
  );
}

Get User's Organizations

import { getOrganizationList } from "@saas/auth/lib/server";

export default async function OrganizationsPage() {
  const organizations = await getOrganizationList();

  return (
    <div>
      <h1>Your Organizations</h1>
      <ul>
        {organizations.map((org) => (
          <li key={org.id}>{org.name}</li>
        ))}
      </ul>
    </div>
  );
}

Protect API Endpoints with oRPC

Sushify Next.js uses oRPC for API management. The protectedProcedure automatically validates authentication and provides user context.

The protectedProcedure is defined in packages/api/orpc/procedures.ts:

export const protectedProcedure = publicProcedure.use(
  async ({ context, next }) => {
    const session = await auth.api.getSession({
      headers: context.headers,
    });

    if (!session) {
      throw new ORPCError("UNAUTHORIZED");
    }

    return await next({
      context: {
        session: session.session,
        user: session.user,
      },
    });
  },
);

Usage example:

import { protectedProcedure } from "../../../orpc/procedures";
import { z } from "zod";

export const listPurchases = protectedProcedure
  .route({
    method: "GET",
    path: "/payments/purchases",
    tags: ["Payments"],
    summary: "Get purchases",
  })
  .input(
    z.object({
      organizationId: z.string().optional(),
    }),
  )
  .handler(async ({ input, context: { user, session } }) => {
    // user and session are automatically available in context
    const purchases = await getPurchasesByUserId(user.id);
    return { purchases };
  });

There's also an adminProcedure that extends protectedProcedure to require admin role:

export const adminProcedure = protectedProcedure.use(
  async ({ context, next }) => {
    if (context.user.role !== "admin") {
      throw new ORPCError("FORBIDDEN");
    }
    return await next();
  },
);

Additional Fields

Better Auth allows you to extend the user model with custom fields. In Sushify Next.js, we've added:

user: {
  additionalFields: {
    onboardingComplete: {
      type: "boolean",
      required: false,
    },
    locale: {
      type: "string",
      required: false,
    },
  },
}

These fields are automatically synced with the database and available in the session.

Hooks

Better Auth supports lifecycle hooks that run before and after authentication events. Sushify Next.js uses hooks to:

  • Update organization seats when members are added/removed
  • Cancel subscriptions when users/organizations are deleted

See packages/auth/auth.ts:62-119 for implementation details.

Email Templates

Authentication emails (verification, password reset, magic links) are sent using the mail provider configured in your environment. Templates are managed in the @repo/mail package.

To customize email templates, see the Mail documentation.

Type Safety

Better Auth provides full TypeScript support. All session and user types are automatically inferred:

import type { Session } from "@repo/auth";

// Inferred types
type User = Session["user"];
type SessionData = Session["session"];