Social OAuth
Render and gate one-click social sign-in buttons via config.auth.socialProviders and per-provider OAuth credentials.
config.auth.socialProviders (packages/config/src/index.ts) is the single source of truth for which social sign-in buttons render, in array order. @repo/auth registers a provider server-side only when its OAuth env vars are set, so the array and the credentials must stay in sync.
How it works
| Piece | Location | Role |
|---|---|---|
config.auth.socialProviders | packages/config/src/index.ts | Provider IDs to render. Starts empty ([]); an empty array hides the whole social block |
SOCIAL_PROVIDERS_META | packages/config/src/social-providers.ts | Maps each ID to its label, i18n labelKey, and required env vars |
AuthSocialButtons | apps/web/components/auth/auth-social-buttons.tsx | Renders one button per ID, signing in via authClient.signIn.social |
| Server registration | packages/auth/src/config.ts | Adds the provider to Better Auth only when all its env vars are set |
An ID with no credentials renders a dead button - it exists client-side with no working backend. Removing an ID hides its button instantly, no env change needed.
Providers
Each provider needs an OAuth app from its developer console. Set all its env vars or it stays unregistered.
| Provider | ID | Env vars |
|---|---|---|
google | GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET | |
| GitHub | github | GITHUB_CLIENT_ID, GITHUB_CLIENT_SECRET |
facebook | FACEBOOK_CLIENT_ID, FACEBOOK_CLIENT_SECRET | |
| Discord | discord | DISCORD_CLIENT_ID, DISCORD_CLIENT_SECRET |
| X | x | TWITTER_CLIENT_ID, TWITTER_CLIENT_SECRET |
| Apple | apple | APPLE_CLIENT_ID, APPLE_TEAM_ID, APPLE_KEY_ID, APPLE_PRIVATE_KEY - the native flow adds APPLE_APP_BUNDLE_IDENTIFIER; without APPLE_CLIENT_ID it runs native-only (no web button), see mobile auth |
.env.example seeds commented GOOGLE_*, GITHUB_* and APPLE_* placeholders; copy the other names from this table.
Setup
Create the OAuth app in the provider's developer console and copy its client ID and secret.
Set the callback URL to <API_URL>/auth/callback/<provider> - Better Auth's baseURL is ${API_URL}/auth. With the default API_URL=http://localhost:3000/api that is http://localhost:3000/api/auth/callback/google.
Set every env var for that provider (see the table).
Add the ID to config.auth.socialProviders so the button renders, then restart so @repo/auth re-reads the env.
For X the callback segment is twitter (<API_URL>/auth/callback/twitter), and the credentials keep the TWITTER_* prefix - Better Auth still names the underlying provider twitter.
Add a new provider
Wiring a provider end to end (linkedin as the example) touches five files, plus the labelKey in packages/i18n/translations/en/web.json under auth.social.
| # | File | Edit |
|---|---|---|
| 1 | packages/config/src/types/app.ts | Add "linkedin" to the SocialProvider union |
| 2 | packages/config/src/social-providers.ts | Add a SOCIAL_PROVIDERS_META entry (label, labelKey, envVars) |
| 3 | packages/runtime/src/env.ts | Add LINKEDIN_CLIENT_ID / LINKEDIN_CLIENT_SECRET as z.string().optional() |
| 4 | packages/auth/src/config.ts | Add the conditional block in socialProviders |
| 5 | apps/web/components/auth/auth-social-buttons.tsx | Add the brand icon to socialProviderIcons |
// packages/auth/src/config.ts - inside socialProviders
...(env.LINKEDIN_CLIENT_ID &&
env.LINKEDIN_CLIENT_SECRET && {
linkedin: {
clientId: env.LINKEDIN_CLIENT_ID,
clientSecret: env.LINKEDIN_CLIENT_SECRET
}
})socialProviderIcons and SOCIAL_PROVIDERS_META are both typed Record<SocialProvider, …>, so extending the union raises a TypeScript error in each until you add the entry - your safety net against a half-wired provider.
Tokens at rest
Better Auth stores each provider's access and refresh token in the account table encrypted with the auth secret (account.encryptOAuthTokens in packages/auth/src/config.ts), so a database leak does not hand out live provider grants.
| Task | How |
|---|---|
| Read or write a token column yourself | Use readAccountToken / sealAccountToken from @repo/auth/account-tokens - never the raw column |
Encrypted tokens are bound to BETTER_AUTH_SECRET. Rotate it through BETTER_AUTH_SECRETS (versioned) rather than replacing it, or every stored token becomes unreadable and users must sign in with the provider again.
Frequently asked questions
The button shows but sign-in fails - why?
The ID is in config.auth.socialProviders but its env vars are missing or wrong, so @repo/auth never registered it. Set all of them and restart.
How do I hide a provider?
Remove its ID from config.auth.socialProviders. The button stops rendering immediately.
Why does X use a TWITTER_* prefix?
The ID is x for the label, but Better Auth's underlying provider key is twitter, so credentials and the callback path keep that name.