OAuth providers
Magic links are always on. Google and Apple only register when their credentials are both set.
Auth in Orbit is handled by better-auth. Magic-link sign-in is the default and is always on. OAuth is additive: Google and Apple get registered as providers only when both their client id and client secret are present in env, so a half-configured provider never shows up on the login page.
Conditional registration
apps/api/src/composition.ts
const googleClientId = process.env.GOOGLE_CLIENT_ID;const googleClientSecret = process.env.GOOGLE_CLIENT_SECRET;const appleClientId = process.env.APPLE_CLIENT_ID;const appleClientSecret = process.env.APPLE_CLIENT_SECRET;
const social: NonNullable<AppConfig["social"]> = {};if (googleClientId && googleClientSecret) { social.google = { clientId: googleClientId, clientSecret: googleClientSecret };}if (appleClientId && appleClientSecret) { social.apple = { clientId: appleClientId, clientSecret: appleClientSecret };}The social object is handed to better-auth only if it has at least one key:
apps/api/src/interfaces/http/better-auth.ts
socialProviders: Object.keys(socialProviders).length > 0 ? socialProviders : undefined,The web shell renders Google and Apple buttons unconditionally — if the server never registered the provider, clicking the button just surfaces an error in the form. The buttons in the scaffolded login page (apps/web-tanstack/src/pages/login.tsx, apps/web-next/src/views/login.tsx) ship with disabled={true} so an unconfigured project can't dead-end users on a half-broken flow. Drop that flag once you've wired the credentials below.
Callback URLs
better-auth mounts OAuth under /v1/auth, so the redirect URIs you register with each provider are:
${API_ORIGIN}/v1/auth/callback/google${API_ORIGIN}/v1/auth/callback/appleAPI_ORIGIN has to match exactly — including protocol and port. In local dev that's http://localhost:4002; in prod it's whatever public hostname the API answers on.
- Create a project in the Google Cloud Console, then enable the OAuth consent screen.
- Create an OAuth 2.0 Client ID of type "Web application".
- Authorized redirect URI:
${API_ORIGIN}/v1/auth/callback/google. Add both dev and prod if you need them. - Copy the client id and secret into
GOOGLE_CLIENT_IDandGOOGLE_CLIENT_SECRETin your API env.
Apple
Sign in with Apple has more steps but the same shape. You'll need a Services ID, a Key, and the resulting JWT-signed client secret better-auth expects.
- In the Apple Developer portal, create a Services ID. Enable "Sign in with Apple".
- Add the return URL:
${API_ORIGIN}/v1/auth/callback/apple. - Generate a Key (type: Sign in with Apple) and download the
.p8. - Produce the short-lived client secret JWT (Apple rotates every 6 months; scripts exist in the ecosystem). Store it as
APPLE_CLIENT_SECRET; the Services ID goes inAPPLE_CLIENT_ID.
Apple's client secret expires. Plan a rotation: script the regeneration, feed it to your secret manager, and restart the API. Nothing in Orbit manages this for you.
Linked vs. separate accounts
better-auth's default behaviour: if someone signs in with Google using an email that already has an account (via magic link or another provider), the accounts are linked on the Account table. Orbit doesn't override this. A user can therefore have multiple Account rows (one per provider) but a single User.
Origins and CORS
better-auth's cookie policy and CORS guard are driven by the same origin config we use elsewhere. The trustedOrigins list includes WEB_ORIGIN, WWW_ORIGIN, API_ORIGIN, and ADDITIONAL_WEB_ORIGINS. If you see "CSRF check failed" after an OAuth round-trip in a new environment, check that the initiating origin is in that list.
Adding another provider
better-auth supports more than Google and Apple. To add one:
- Add the secret pair to
.env.exampleand env validation incomposition.ts. - Extend the conditional block that builds
socialwith the new provider key. - Register the callback URI with the provider:
${API_ORIGIN}/v1/auth/callback/<provider>. - The web sign-in page reads the registered provider list from the API — no client code change needed.