OAuth login
Register GitHub and Google OAuth apps, set client credentials, and test logins against a local OAuth emulator.
Your dashboard’s sign-in page shows “Continue with GitHub” and “Continue with Google” buttons. Each one works only after you register an OAuth app with the provider and put its credentials in the server’s environment. The providers themselves are optional: leave the variables empty and email and password login keeps working.
The auth server mounts better-auth under /api/auth on the API host, so every auth
route, including the OAuth callbacks, lives on API_URL.
Callback URLs
Register one callback URL per provider. Both point at the API host:
| Provider | Callback URL |
|---|---|
| GitHub | <API_URL>/api/auth/callback/github |
<API_URL>/api/auth/callback/google |
With API_URL=https://api.example.com, the GitHub callback is:
https://api.example.com/api/auth/callback/github
Providers compare the callback against their registered values exactly. Scheme, host,
port, and path must match byte for byte, including the absence of a trailing slash.
A redirect that ends at “redirect_uri mismatch” almost always means the registered URL
and API_URL disagree on one of those pieces.
Client credentials
Set all four variables in the server environment:
| Variable | Notes |
|---|---|
GITHUB_CLIENT_ID |
OAuth app client ID |
GITHUB_CLIENT_SECRET |
OAuth app client secret |
GOOGLE_CLIENT_ID |
OAuth client ID |
GOOGLE_CLIENT_SECRET |
OAuth client secret |
The server registers a provider only when both its ID and secret are set. Set one
without the other and the button for that provider still renders, but clicks fail with
an error from the auth server, so keep the pair together. AUTH_SIGN_UP_ENABLED=false
keeps social sign-in working while switching off social sign-up: the provider config
disables new-user registration, not logins.
Trusted origins and the sign-in flow
Sign-in starts from two places: the dashboard on APP_URL and public board subdomains
of APP_ROOT_DOMAIN. Both must count as trusted origins, or better-auth rejects the
request before the provider ever sees it.
Clicking a social button sends the browser to
<API_URL>/api/auth/sign-in/social?provider=github&callbackURL=.... Better-auth checks
the request’s Origin header and the callbackURL against trustedOrigins and aborts
with INVALID_ORIGIN or INVALID_CALLBACK_URL when neither matches. From there it
redirects to the provider’s authorize page, and the provider redirects back to the
callback above. A successful exchange creates a 7-day session and redirects the browser
back to the callbackURL.
The server builds its default trusted origin list from the same three variables:
APP_URLAPI_URL*.<APP_ROOT_DOMAIN>in production,*.localhost:3001in development
The wildcard is what lets users sign in from any board subdomain. The docker-compose file derives the same default for you:
AUTH_TRUSTED_ORIGINS=$APP_URL,$API_URL,*.$APP_ROOT_DOMAIN
Set AUTH_TRUSTED_ORIGINS yourself and it replaces the defaults entirely, so include
every origin your browsers call the API from. The list is comma-separated. Scheme-less
patterns such as *.example.com match both http and https hosts; a pattern like
https://*.example.com pins the scheme.
In development the web server proxies /api requests same-origin and sets host-only
cookies per subdomain of *.localhost, which is why the dev trusted-origin default is
*.localhost:3001 rather than a production wildcard.
Local OAuth emulator
For local development, GITHUB_EMULATOR_URL and GOOGLE_EMULATOR_URL point the
providers at vercel-labs/emulate instead of
the real services:
GITHUB_EMULATOR_URL=http://localhost:4000
GOOGLE_EMULATOR_URL=http://localhost:4001
The repo root’s emulate.config.yaml defines the emulator’s users and OAuth apps. The
registered client credentials and redirect URIs live in that file:
- GitHub app
Iv1.example_client_idwith redirecthttp://localhost:3000/api/auth/callback/github - Google client
example-client-id.apps.googleusercontent.comwith redirecthttp://localhost:3000/api/auth/callback/google
The emulator provider activates only when the emulator URL, the client ID, and the
client secret are all set, and its credential values must match emulate.config.yaml.
With the URL set, the server skips the built-in provider for that service. The
portless URLs https://github.emulate.localhost and https://google.emulate.localhost
work when your resolver maps .emulate.localhost.