OAuth
Configure OAuth providers per tenant. Credentials are encrypted at rest with your Better Auth secret. When no tenant-specific config exists, the plugin falls back to globally configured social providers.
Global fallback
Configure social providers in your Better Auth config as usual:
betterAuth({
socialProviders: {
google: {
clientId: process.env.GOOGLE_CLIENT_ID!,
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
},
},
plugins: [tenantAuth()],
});Tenants without their own OAuth config use these global credentials. The callback URL is:
{baseURL}/tenant/callback/{providerId}For example: http://localhost:3000/api/auth/tenant/callback/google when Better Auth is mounted at /api/auth.
Register per-tenant OAuth config
Requires management access (see Configuration).
await authClient.$fetch("/tenant/oauth-config/register", {
method: "POST",
body: {
tenantId: "<tenant-id>",
providerId: "google",
clientId: "your-client-id",
clientSecret: "your-client-secret",
scopes: ["email", "profile"], // optional
redirectURI: "https://app.example.com/api/auth/tenant/callback/google", // optional
enabled: true, // optional, default true
},
});Registering again for the same tenant + provider upserts the existing config.
List OAuth configs
Returns configs without client secrets. Client ids are decrypted for display.
await authClient.$fetch("/tenant/oauth-config/list?tenantId=<tenant-id>");Delete OAuth config
await authClient.$fetch("/tenant/oauth-config/delete", {
method: "POST",
body: {
tenantId: "<tenant-id>",
providerId: "google",
},
});Social sign-in
Start the OAuth flow for a tenant:
await authClient.signInSocialTenant({
tenantId: "<tenant-id>",
provider: "google",
callbackURL: "/welcome", // where to redirect after success
errorCallbackURL: "/error", // optional
inviteToken: "<invite-token>", // required for first-time registration when invite-only is enabled
});Endpoint: POST /tenant/sign-in/social
The plugin resolves the provider from the tenant's database config first, then from global socialProviders.
When requireInviteForTenantSignUp or allowedEmailDomains is configured, first-time social registration is gated the same way as email sign-up (existing OAuth users can still sign in). Pass inviteToken on social sign-in so it can be validated at the callback after the provider returns the email. Failures redirect to errorCallbackURL with codes such as invite_required, invite_invalid, or email_domain_not_allowed.
callbackURL, newUserCallbackURL, and errorCallbackURL are all validated against your Better Auth trustedOrigins before the OAuth flow starts, and the final redirect target is re-checked at the callback. Relative paths (e.g. /welcome) are always allowed; untrusted absolute URLs are rejected with INVALID_CALLBACK_URL.
Callback
The OAuth callback is handled at:
GET /tenant/callback/{providerId}On success, a session is created with tenantId set and the user is redirected to callbackURL. On failure, the user is redirected to the error URL with error and optional error_description query parameters.
Provider resolution order
- Tenant's
tenantOauthConfigrow (if enabled) - Global social provider from Better Auth config
PROVIDER_NOT_FOUNDerror if neither exists
Only built-in Better Auth social providers (e.g. google, github) are supported for per-tenant configuration.
Security notes
- Client secrets are never returned by list/get endpoints
- Credentials are encrypted with
symmetricEncryptusing your auth secret - Register redirect URIs in your OAuth provider's console for each tenant if using custom redirect URIs
- Callback URLs must be in your Better Auth
trustedOrigins(relative paths are OK) — see Callback URL validation
See Endpoints and the Next.js demo for a complete OAuth walkthrough.