Better Auth Plugin

Official Better Auth plugin for integrating Transactional Auth as an OAuth provider.

Installation

npm install @usetransactional/better-auth better-auth
pnpm add @usetransactional/better-auth better-auth
yarn add @usetransactional/better-auth better-auth

Quick Start

Server Setup

import { betterAuth } from "better-auth";
import { transactional } from "@usetransactional/better-auth";
 
export const auth = betterAuth({
  database: {
    // your database configuration
  },
  plugins: [
    transactional({
      clientId: process.env.AUTH_CLIENT_ID!,
      clientSecret: process.env.AUTH_CLIENT_SECRET!,
      authDomain: "auth.yourdomain.com",
    }),
  ],
});

Client Setup

import { createAuthClient } from "better-auth/client";
import { transactionalClient } from "@usetransactional/better-auth/client";
 
export const authClient = createAuthClient({
  plugins: [transactionalClient()],
});

Sign In

import { authClient } from "./auth-client";
 
await authClient.signIn.oauth2({
  providerId: "transactional",
  callbackURL: "/dashboard",
});

Server Plugin

The server plugin registers Transactional Auth as an OAuth 2.0 / OpenID Connect provider in your Better Auth configuration.

import { betterAuth } from "better-auth";
import { transactional } from "@usetransactional/better-auth";
 
export const auth = betterAuth({
  database: {
    // your database configuration
  },
  plugins: [
    transactional({
      // Required
      clientId: "your-client-id",
      clientSecret: "your-client-secret",
      authDomain: "auth.yourdomain.com",
 
      // Optional
      providerId: "transactional", // default: "transactional"
      scopes: ["openid", "profile", "email"], // default
      pkce: true, // default: true
      prompt: undefined, // "login" | "consent" | "none"
      accessType: undefined, // "online" | "offline"
      disableSignUp: false, // default: false
      authorizationUrlParams: {}, // extra params for the authorization URL
      mapProfileToUser: undefined, // custom profile mapping function
 
      // Override auto-discovered endpoints (advanced)
      authorizationUrl: undefined,
      tokenUrl: undefined,
      userInfoUrl: undefined,
    }),
  ],
});

Required Options

OptionTypeDescription
clientIdstringYour Transactional Auth application client ID
clientSecretstringYour Transactional Auth application client secret
authDomainstringYour Transactional Auth domain (e.g., auth.yourdomain.com)

Optional Options

OptionTypeDefaultDescription
providerIdstring"transactional"Identifier for this provider (must match client config)
scopesstring[]["openid", "profile", "email"]OAuth scopes to request
pkcebooleantrueEnable PKCE (Proof Key for Code Exchange)
mapProfileToUser(profile) => objectCustom function to map OIDC claims to user fields
disableSignUpbooleanfalsePrevent new user creation on first login
promptstringOIDC prompt parameter ("login", "consent", "none")
accessTypestringOAuth access type ("online", "offline")
authorizationUrlParamsobject{}Additional query parameters for the authorization URL
authorizationUrlstringOverride the authorization endpoint (auto-discovered by default)
tokenUrlstringOverride the token endpoint (auto-discovered by default)
userInfoUrlstringOverride the userinfo endpoint (auto-discovered by default)

Client Plugin

The client plugin provides type-safe sign-in methods for the Transactional provider.

import { createAuthClient } from "better-auth/client";
import {
  transactionalClient,
  createTransactionalSignInOptions,
} from "@usetransactional/better-auth/client";
 
export const authClient = createAuthClient({
  plugins: [transactionalClient()],
});

createTransactionalSignInOptions

A helper that generates the correct signIn.oauth2 options with the "transactional" provider ID pre-set.

import { createTransactionalSignInOptions } from "@usetransactional/better-auth/client";
 
const options = createTransactionalSignInOptions({
  callbackURL: "/dashboard",
});
 
// Equivalent to: { providerId: "transactional", callbackURL: "/dashboard" }

Sign-In Methods

Basic Sign-In

await authClient.signIn.oauth2({
  providerId: "transactional",
  callbackURL: "/dashboard",
});

With All Options

await authClient.signIn.oauth2({
  providerId: "transactional",
  callbackURL: "/dashboard",
  errorCallbackURL: "/auth/error",
  newUserCallbackURL: "/onboarding",
  disableRedirect: false,
  scopes: ["openid", "profile", "email", "offline_access"],
  requestSignUp: false,
});
OptionTypeDescription
providerIdstringMust be "transactional" (or your custom provider ID)
callbackURLstringURL to redirect to after successful login
errorCallbackURLstringURL to redirect to on login failure
newUserCallbackURLstringURL to redirect to for first-time users
disableRedirectbooleanReturn the authorization URL instead of redirecting
scopesstring[]Override the default scopes for this sign-in
requestSignUpbooleanHint to show the sign-up form instead of sign-in

Using the Helper

import { createTransactionalSignInOptions } from "@usetransactional/better-auth/client";
 
await authClient.signIn.oauth2(
  createTransactionalSignInOptions({
    callbackURL: "/dashboard",
  })
);

Profile Mapping

Default Mapping

By default, the plugin maps OIDC claims from the ID token to Better Auth user fields:

OIDC ClaimBetter Auth FieldDescription
subidUser identifier
namenameDisplay name
emailemailEmail address
pictureimageAvatar URL
email_verifiedemailVerifiedEmail verification status
org_idorganizationIdOrganization identifier
org_nameorganizationNameOrganization name
org_roleorganizationRoleRole within the organization

Custom Profile Mapping

Override the default mapping by providing a mapProfileToUser function:

transactional({
  clientId: "your-client-id",
  clientSecret: "your-client-secret",
  authDomain: "auth.yourdomain.com",
  mapProfileToUser: (profile) => {
    return {
      id: profile.sub,
      name: profile.name,
      email: profile.email,
      image: profile.picture,
      emailVerified: profile.email_verified,
      // Custom fields
      organizationId: profile.org_id,
      organizationName: profile.org_name,
      role: profile.org_role || "member",
      department: profile["custom:department"],
    };
  },
});

Configuration Examples

Custom Scopes

Request additional scopes beyond the defaults:

transactional({
  clientId: "your-client-id",
  clientSecret: "your-client-secret",
  authDomain: "auth.yourdomain.com",
  scopes: ["openid", "profile", "email", "offline_access", "read:emails"],
});

Disable Auto Sign-Up

Prevent new accounts from being created when a user signs in for the first time. Only existing users in your Better Auth database will be allowed to log in.

transactional({
  clientId: "your-client-id",
  clientSecret: "your-client-secret",
  authDomain: "auth.yourdomain.com",
  disableSignUp: true,
});

Force Re-Authentication

Require the user to enter their credentials every time, even if they have an active session:

transactional({
  clientId: "your-client-id",
  clientSecret: "your-client-secret",
  authDomain: "auth.yourdomain.com",
  prompt: "login",
});

Custom Authorization Parameters

Pass extra query parameters to the authorization endpoint:

transactional({
  clientId: "your-client-id",
  clientSecret: "your-client-secret",
  authDomain: "auth.yourdomain.com",
  authorizationUrlParams: {
    screen_hint: "signup",
    organization: "org_abc123",
    invitation: "inv_xyz789",
  },
});

Custom Provider ID

If you need to register multiple Transactional Auth providers or prefer a different name, set a custom providerId. The ID must match on both the server and client.

Server:

transactional({
  clientId: "your-client-id",
  clientSecret: "your-client-secret",
  authDomain: "auth.yourdomain.com",
  providerId: "my-sso",
});

Client:

await authClient.signIn.oauth2({
  providerId: "my-sso",
  callbackURL: "/dashboard",
});

Override Discovery URLs

By default, the plugin auto-discovers endpoints from the OIDC well-known configuration at https://{authDomain}/.well-known/openid-configuration. You can override individual endpoints if needed:

transactional({
  clientId: "your-client-id",
  clientSecret: "your-client-secret",
  authDomain: "auth.yourdomain.com",
  authorizationUrl: "https://auth.yourdomain.com/authorize",
  tokenUrl: "https://auth.yourdomain.com/oauth/token",
  userInfoUrl: "https://auth.yourdomain.com/userinfo",
});

Redirect URI

You must register the correct redirect URI in your Transactional Auth application settings. The format is:

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

The path follows the pattern /api/auth/callback/{providerId}. If you use a custom providerId, the redirect URI must match:

https://yourapp.com/api/auth/callback/{your-custom-provider-id}

Make sure this URI is added to the "Allowed Callback URLs" list in your Transactional Auth application configuration.

How It Works

The @usetransactional/better-auth plugin integrates with Better Auth's OAuth infrastructure:

  1. Wraps Better Auth's genericOAuth plugin -- The Transactional plugin is built on top of Better Auth's generic OAuth 2.0 provider, pre-configured with Transactional Auth defaults.

  2. Auto-discovers endpoints from OIDC well-known config -- On initialization, the plugin fetches https://{authDomain}/.well-known/openid-configuration to discover the authorization, token, and userinfo endpoints automatically.

  3. Maps OIDC claims to Better Auth user model -- The ID token claims (sub, name, email, picture, etc.) are mapped to Better Auth's standard user fields. Organization-specific claims (org_id, org_name, org_role) are also extracted.

  4. Configures PKCE automatically -- PKCE (Proof Key for Code Exchange) is enabled by default for enhanced security, adding code_challenge and code_verifier to the authorization flow.

  5. Handles organization claims from ID token -- Transactional Auth includes organization membership information directly in the ID token, which the plugin extracts and makes available on the user object for multi-tenant applications.