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-authCreate a Transactional Auth Application
- Go to your Transactional Dashboard
- Navigate to Auth > Applications
- Click Create Application
- 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
- 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 forTRANSACTIONAL_AUTH_URL) - Client ID:
app_xxxxxxxxxxxx(use this forTRANSACTIONAL_CLIENT_ID) - Client Secret:
secret_xxxxxxxxxxxx(use this forTRANSACTIONAL_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/transactionalto Redirect URIs andhttp://localhost:3000to 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.comWarning: 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 migrateNote: 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
genericOAuthClientplugin is required when using thegenericOAuthplugin 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()withproviderIdfor providers configured via thegenericOAuthplugin. ThesignIn.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 Claim | Better Auth Field |
|---|---|
sub | id |
email | email |
name | name |
picture | image |
email_verified | emailVerified |
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:
- Update Redirect URIs - Add
https://yourapp.com/api/auth/callback/transactionalto your Transactional Auth application - Update Allowed Origins - Add
https://yourapp.comto your application settings - Set Environment Variables - Configure all production URLs and secrets
- Enable HTTPS - Required for OAuth in production (OAuth will fail over HTTP)
- Configure Session Storage - Use a production PostgreSQL database for Better Auth
- 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 migrateCORS Errors
Add your app's origin to the Transactional Auth application's allowed origins.
Next Steps
- OAuth Flows - Learn more about OAuth implementation
- MFA - Add multi-factor authentication
- Organizations - Set up B2B multi-tenancy
On This Page
- Overview
- How It Works
- Prerequisites
- Installation
- Create a Transactional Auth Application
- Configure Better Auth
- Environment Variables
- Database Schema
- Create the Auth API Route
- Create the Auth Client
- Add Login Button
- Protect Routes
- Server-Side Session Access
- Add Logout
- User Profile Mapping
- Advanced Configuration
- Custom User Fields
- Request Additional Scopes
- Production Checklist
- Troubleshooting
- "Invalid redirect_uri" Error
- Session Not Persisting
- CORS Errors
- Next Steps