Social sign-in is an Optional Provider built on Better Auth's socialProviders. GitHub and Google buttons appear on the sign-in and sign-up pages only when their environment variables are set; with none configured the auth screens render exactly the email/password Local Auth Path that ships by default. Nothing exists-but-disabled: an unset provider is absent from the Better Auth config entirely.
What exists where
- Provider wiring:
packages/auth/src/index.tsbuilds Better Auth'ssocialProvidersfromAuthConfig.socialProviders, which the web app resolves from worker env throughactiveSocialProviders(packages/env/src/server.ts). A provider is included only when both its*_CLIENT_IDand*_CLIENT_SECREThave values; one without the other means the provider is unconfigured, never half-configured. - Auth screens:
/sign-inand/sign-upload the active provider ids server-side (apps/web/src/lib/server/social-providers.ts) and render one secondary button per provider above the email form, separated by a divider. No providers, no buttons, no divider. - Last sign-in method: the core
lastLoginMethodplugin remembers how each visitor last signed in (a client-readable cookie, no database column). The sign-in page answers with a quiet "Last signed in with GitHub." hint when a method is remembered. - Account linking: Better Auth's default implicit linking: a social sign-in whose email matches an existing account links the provider to it when the provider confirms the email as verified and the local mailbox is verified. An unverified local mailbox is refused (
account_not_linked); that refusal is the guard against pre-registering a victim's address and taking the account over with a matching provider email. - Account page:
/accountlists every sign-in method (email and password plus linked providers) with an Unlink action. Unlinking stays available only while another sign-in method remains; Better Auth refuses removing the last account and the UI presents that rule as a reason instead of a control. - Audit events: linking and unlinking record
auth.account_linked/auth.account_unlinkedthrough the governanceAuditEventLog(adapter:apps/web/src/lib/server/social-account-audit.ts), attributed to the user and naming the provider. Social sign-ins and failures also recordauth.sign_in/auth.sign_in_failedfrom the OAuth callback, like credential sign-ins.
Environment variables
| Variable | Value |
|---|---|
GITHUB_CLIENT_ID | GitHub OAuth app client ID |
GITHUB_CLIENT_SECRET | GitHub OAuth app client secret (server-side only) |
GOOGLE_CLIENT_ID | Google OAuth client ID |
GOOGLE_CLIENT_SECRET | Google OAuth client secret (server-side only) |
Set a pair in the root .env for local development, or in the deploy shell environment; alchemy.run.ts forwards them to the web worker (secrets wrapped in Redacted). The web worker is the only consumer: Better Auth runs there, and the API worker authenticates bearer tokens, not browsers.
Provider setup
GitHub
- Open GitHub → Settings → Developer settings → OAuth Apps → New OAuth App.
- Set the Homepage URL to your app's origin (for example
http://localhost:3071in development). - Set the Authorization callback URL to the exact callback below.
- Copy the client ID and generate a client secret into the two variables.
- Open the Google Cloud console → APIs & Services → Credentials → Create OAuth client ID (Web application).
- Add your origin under Authorized JavaScript origins.
- Add the exact callback URL below under Authorized redirect URIs.
- Copy the client ID and secret into the two variables.
OAuth callback URLs
The Better Auth catchall (/api/auth/*) serves both providers' callbacks. Use these exact URLs when registering each app: {BETTER_AUTH_URL} is the same base URL the auth instance runs on (http://localhost:3071 in local development):
| Provider | Callback URL |
|---|---|
| GitHub | {BETTER_AUTH_URL}/api/auth/callback/github |
{BETTER_AUTH_URL}/api/auth/callback/google |
The callback origin must also be trusted by Better Auth: same-origin callbacks are trusted by default, and BETTER_AUTH_TRUSTED_ORIGINS covers any split-domain deployment.
Verification
With the variables unset, /sign-in shows no provider buttons; that is the provider-light default working as designed. After setting a pair and restarting the dev server:
/sign-inand/sign-uprender the provider's button above the email form.- Completing the provider's consent screen lands you signed-in, and the sign-in page later shows "Last signed in with …".
/accountlists the provider under Sign-in methods, with an Unlink action while another method remains./admin(as a system admin) or the workspace audit trail shows the matchingauth.sign_inandauth.account_linkedevents.
Notes and limits
- The Local Auth Path is untouched: email/password sign-in, username sign-in, two-factor, and verification all behave identically with providers on or off.
- Manual linking (a signed-in user adding a provider from
/accountvialinkSocial) is available through Better Auth's API but has no dedicated UI in the starter yet; implicit linking on sign-in covers the common path. - The linked GitHub account on the Seed Workspace's demo user is fixture state for
/account, like the seed webhook endpoints: it demonstrates the surface and names no real GitHub identity.