Better Auth (Advanced)

Complete guide to Better Auth integration with Transactional Auth.

Overview

Better Auth is a popular TypeScript authentication library for Next.js and React applications. This guide shows how to integrate Better Auth with Transactional Auth as your OAuth/OIDC provider.

This combination gives you:

  • Transactional Auth - Identity provider with user management, MFA, enterprise SSO, and hosted login
  • Better Auth - Local session management, React hooks, and middleware for your app

How It Works

┌─────────────────┐      ┌──────────────────────────────────┐
│   Your App      │      │     Transactional Auth           │
│                 │      │  https://auth.usetransactional.com│
│  ┌───────────┐  │      │                                  │
│  │Better Auth│◄─┼──────┼─► OAuth/OIDC Login              │
│  └───────────┘  │      │   User Management                │
│       │         │      │   MFA, SSO, Branding             │
│       ▼         │      └──────────────────────────────────┘
│  ┌───────────┐  │
│  │ Your DB   │  │  ◄── Better Auth stores sessions locally
│  │ (sessions)│  │
│  └───────────┘  │
└─────────────────┘

Transactional Auth manages:

  • User accounts and passwords
  • Email verification
  • Multi-factor authentication
  • Enterprise SSO (SAML/OIDC)
  • Hosted login pages
  • Security policies

Better Auth stores locally (in your database):

  • User sessions
  • OAuth account links
  • Cached user profile data

Prerequisites

  • A Transactional account with an Auth application created
  • A Next.js application
  • Node.js 18+

Installation

Install Better Auth and its dependencies:

npm install better-auth

Create a Transactional Auth Application

  1. Go to your Transactional Dashboard
  2. Navigate to Auth > Applications
  3. Click Create Application
  4. Configure your application:
    • Name: Your app name
    • Type: SPA or Server (depending on your setup)
    • Redirect URIs: https://yourapp.com/api/auth/callback/transactional
    • Allowed Origins: https://yourapp.com
  5. Save and copy your credentials from the Credentials card

Your application settings page will show:

  • Auth Domain: your-app-name-abc123.auth.usetransactional.com (use this for TRANSACTIONAL_AUTH_URL)
  • Client ID: app_xxxxxxxxxxxx (use this for TRANSACTIONAL_CLIENT_ID)
  • Client Secret: secret_xxxxxxxxxxxx (use this for TRANSACTIONAL_CLIENT_SECRET)

Important: Each application gets its own unique Auth Domain (subdomain). Use your application's specific domain, not the generic auth.usetransactional.com.

Note: For local development, add http://localhost:3000/api/auth/callback/transactional to Redirect URIs and http://localhost:3000 to Allowed Origins.

Configure Better Auth

Create your Better Auth configuration file:

// lib/auth.ts
import { betterAuth } from "better-auth";
import { genericOAuth } from "better-auth/plugins";
 
export const auth = betterAuth({
  baseURL: process.env.BETTER_AUTH_URL!, // Your app URL (e.g., https://yourapp.com)
  database: {
    provider: "postgresql",
    url: process.env.DATABASE_URL!,
  },
  plugins: [
    genericOAuth({
      config: [
        {
          providerId: "transactional",
          clientId: process.env.TRANSACTIONAL_CLIENT_ID!,
          clientSecret: process.env.TRANSACTIONAL_CLIENT_SECRET!,
          // Explicit endpoints (recommended for reliability)
          authorizationUrl: `${process.env.TRANSACTIONAL_AUTH_URL}/auth`,
          tokenUrl: `${process.env.TRANSACTIONAL_AUTH_URL}/token`,
          userInfoUrl: `${process.env.TRANSACTIONAL_AUTH_URL}/me`,
          scopes: ["openid", "profile", "email"],
          pkce: true, // Required by Transactional Auth
        },
      ],
    }),
  ],
});

Environment Variables

Add these to your .env or .env.local:

# Your application URL
BETTER_AUTH_URL=https://yourapp.com
 
# Transactional Auth (use your app's Auth Domain from the dashboard)
TRANSACTIONAL_AUTH_URL=https://your-app-name-abc123.auth.usetransactional.com
TRANSACTIONAL_CLIENT_ID=your_client_id
TRANSACTIONAL_CLIENT_SECRET=your_client_secret
 
# Database (for Better Auth sessions)
DATABASE_URL=postgresql://user:password@host:5432/dbname
 
# Public URL for client-side
NEXT_PUBLIC_APP_URL=https://yourapp.com

Warning: Never commit secrets to version control. Use environment variables in production.

Database Schema

Better Auth creates these tables in your database to store sessions and user data:

-- Users table (cached from Transactional Auth)
CREATE TABLE "user" (
  "id" TEXT PRIMARY KEY,
  "name" TEXT,
  "email" TEXT UNIQUE NOT NULL,
  "emailVerified" BOOLEAN DEFAULT FALSE,
  "image" TEXT,
  "createdAt" TIMESTAMP DEFAULT NOW(),
  "updatedAt" TIMESTAMP DEFAULT NOW()
);
 
-- Sessions table (local session management)
CREATE TABLE "session" (
  "id" TEXT PRIMARY KEY,
  "expiresAt" TIMESTAMP NOT NULL,
  "ipAddress" TEXT,
  "userAgent" TEXT,
  "userId" TEXT NOT NULL REFERENCES "user"("id") ON DELETE CASCADE,
  "createdAt" TIMESTAMP DEFAULT NOW(),
  "updatedAt" TIMESTAMP DEFAULT NOW()
);
 
-- OAuth accounts (links to Transactional Auth)
CREATE TABLE "account" (
  "id" TEXT PRIMARY KEY,
  "accountId" TEXT NOT NULL,           -- User ID from Transactional
  "providerId" TEXT NOT NULL,          -- "transactional"
  "userId" TEXT NOT NULL REFERENCES "user"("id") ON DELETE CASCADE,
  "accessToken" TEXT,
  "refreshToken" TEXT,
  "idToken" TEXT,
  "expiresAt" TIMESTAMP,
  "scope" TEXT,
  "createdAt" TIMESTAMP DEFAULT NOW(),
  "updatedAt" TIMESTAMP DEFAULT NOW()
);

Run the migration to create these tables:

npx better-auth migrate

Note: User data (email, name, etc.) is synced from Transactional Auth on each login. Transactional Auth remains the source of truth for user identity.

Create the Auth API Route

Create the API route handler for Better Auth:

// app/api/auth/[...all]/route.ts
import { auth } from "@/lib/auth";
import { toNextJsHandler } from "better-auth/next-js";
 
export const { GET, POST } = toNextJsHandler(auth.handler);

Create the Auth Client

Create a client-side auth helper with the genericOAuthClient plugin:

// lib/auth-client.ts
import { createAuthClient } from "better-auth/react";
import { genericOAuthClient } from "better-auth/client/plugins";
 
export const authClient = createAuthClient({
  baseURL: process.env.NEXT_PUBLIC_APP_URL,
  plugins: [genericOAuthClient()],
});
 
export const {
  signIn,
  signOut,
  useSession
} = authClient;

Important: The genericOAuthClient plugin is required when using the genericOAuth plugin on the server. Without it, signIn.oauth2() won't be available.

Add Login Button

Create a login button component using signIn.oauth2() for generic OAuth providers:

// components/login-button.tsx
"use client";
 
import { signIn } from "@/lib/auth-client";
 
export function LoginButton() {
  const handleLogin = () => {
    // Use signIn.oauth2() for genericOAuth providers (not signIn.social())
    signIn.oauth2({
      providerId: "transactional",
      callbackURL: "/dashboard",
    });
  };
 
  return (
    <button onClick={handleLogin}>
      Sign in with Transactional
    </button>
  );
}

Note: Use signIn.oauth2() with providerId for providers configured via the genericOAuth plugin. The signIn.social() method is only for built-in providers like Google and GitHub.

Protect Routes

Use the session hook to protect pages:

// app/dashboard/page.tsx
"use client";
 
import { useSession } from "@/lib/auth-client";
import { redirect } from "next/navigation";
 
export default function DashboardPage() {
  const { data: session, isPending } = useSession();
 
  if (isPending) {
    return <div>Loading...</div>;
  }
 
  if (!session) {
    redirect("/login");
  }
 
  return (
    <div>
      <h1>Welcome, {session.user.name}</h1>
      <p>Email: {session.user.email}</p>
    </div>
  );
}

Server-Side Session Access

Access the session in Server Components:

// app/dashboard/page.tsx
import { auth } from "@/lib/auth";
import { headers } from "next/headers";
import { redirect } from "next/navigation";
 
export default async function DashboardPage() {
  const session = await auth.api.getSession({
    headers: await headers(),
  });
 
  if (!session) {
    redirect("/login");
  }
 
  return (
    <div>
      <h1>Welcome, {session.user.name}</h1>
    </div>
  );
}

Add Logout

// components/logout-button.tsx
"use client";
 
import { signOut } from "@/lib/auth-client";
 
export function LogoutButton() {
  const handleLogout = () => {
    signOut({
      callbackURL: "/",
    });
  };
 
  return (
    <button onClick={handleLogout}>
      Sign out
    </button>
  );
}

User Profile Mapping

Transactional Auth returns standard OIDC claims. Better Auth automatically maps these:

Transactional ClaimBetter Auth Field
subid
emailemail
namename
pictureimage
email_verifiedemailVerified

Advanced Configuration

Custom User Fields

Map additional claims from Transactional:

genericOAuth({
  config: [
    {
      providerId: "transactional",
      clientId: process.env.TRANSACTIONAL_CLIENT_ID!,
      clientSecret: process.env.TRANSACTIONAL_CLIENT_SECRET!,
      discoveryUrl: `${process.env.TRANSACTIONAL_AUTH_URL}/.well-known/openid-configuration`,
      scopes: ["openid", "profile", "email"],
      mapProfileToUser: (profile) => ({
        id: profile.sub,
        email: profile.email,
        name: profile.name,
        image: profile.picture,
        emailVerified: profile.email_verified,
        // Add custom fields
        organizationId: profile.org_id,
      }),
    },
  ],
}),

Request Additional Scopes

Request more data from Transactional:

scopes: ["openid", "profile", "email", "offline_access"],

The offline_access scope provides refresh tokens for long-lived sessions.

Production Checklist

Before deploying to production:

  1. Update Redirect URIs - Add https://yourapp.com/api/auth/callback/transactional to your Transactional Auth application
  2. Update Allowed Origins - Add https://yourapp.com to your application settings
  3. Set Environment Variables - Configure all production URLs and secrets
  4. Enable HTTPS - Required for OAuth in production (OAuth will fail over HTTP)
  5. Configure Session Storage - Use a production PostgreSQL database for Better Auth
  6. Run Migrations - Ensure Better Auth tables are created with npx better-auth migrate

Troubleshooting

"Invalid redirect_uri" Error

Ensure your callback URL exactly matches what's configured in Transactional:

https://yourapp.com/api/auth/callback/transactional

Session Not Persisting

Check your database connection and ensure Better Auth tables are created:

npx better-auth migrate

CORS Errors

Add your app's origin to the Transactional Auth application's allowed origins.

Next Steps