Valcraven Docs
Getting Started

OAuth setup

Configure Google, GitHub, and Apple sign-in via Better Auth — by hand or with the /configure-sso skill.

Valcraven handles authentication with Better Auth. Email/password sign-in works out of the box; social sign-in is opt-in per provider and activates as soon as you set that provider's credentials. The wired providers live in socialProviders in apps/web/lib/auth.ts: Google, GitHub, Apple, and Facebook. This page covers the common three (Google, GitHub, Apple) and the /configure-sso skill that automates them. For the auth system as a whole, see Authentication.

The fast path — /configure-sso

Rather than clicking through five developer portals, run the skill from Claude Code:

/configure-sso

It detects which providers are already configured, then for each one you select it either drives the provider's portal with browser automation (creating the OAuth app, filling redirect URIs, extracting the client ID and secret) or walks you through it manually — writing the resulting credentials to .env.local automatically. Microsoft is fully automated via the az CLI.

Everything below is what the skill automates, so you understand what it's doing (and can do it by hand if you prefer).

How the redirect URIs work

Every provider redirects back to the same path shape:

{BETTER_AUTH_URL}/api/auth/callback/{provider}

So for a production domain https://app.example.com, Google's callback is https://app.example.com/api/auth/callback/google, GitHub's is .../github, and so on.

BETTER_AUTH_URL is explicit and host-scoped. Better Auth does not reliably infer its base URL behind a reverse proxy or the Tailscale sidecar, so BETTER_AUTH_URL must be set. The OAuth state/PKCE cookie is host-scoped, meaning the callback host must match the host you started the login on — one BETTER_AUTH_URL value serves exactly one host. You can't drive OAuth from both localhost and a tailnet URL at once; pin it to whichever host you'll test on, and register that host's callback with each provider.

Dev and prod are separate registrations. .env.local (or apps/web/.env) is for dev; production credentials live separately — for Valcraven on Cloudflare Workers, as Worker secrets (wrangler secret put). Google, Facebook, and Apple let one app hold multiple redirect URIs, so a single registration can serve dev + prod. GitHub allows exactly one callback URL per OAuth App, so you need a separate GitHub app for dev and for prod.

Google

  1. Go to the Google Cloud Console → Credentials.
  2. Create an OAuth client ID of type Web application (configure the OAuth consent screen first if prompted).
  3. Under Authorized redirect URIs, add your callback(s): https://yourdomain.com/api/auth/callback/google (and a localhost/tailnet one for dev).
  4. Copy the Client ID and Client secret into your env file:
GOOGLE_CLIENT_ID=your-client-id.apps.googleusercontent.com
GOOGLE_CLIENT_SECRET=your-client-secret

If the consent screen is in Testing mode, only test users you've explicitly added can sign in until you publish it.

GitHub

  1. Go to GitHub → Settings → Developer settings → OAuth Apps.
  2. Register a New OAuth App.
  3. Homepage URL: https://yourdomain.com. Authorization callback URL: https://yourdomain.com/api/auth/callback/github.
  4. Copy the Client ID, generate a Client secret, and add both:
GITHUB_CLIENT_ID=your-client-id
GITHUB_CLIENT_SECRET=your-client-secret

GitHub validates the callback only after login. A logged-out redirect test can look fine while the callback is actually wrong — only a completed sign-in proves it. Better Auth requests the user:email scope automatically, so you get the user's email even if it's private on GitHub.

Apple

Apple is the most involved and, in practice, production-only: Apple rejects localhost and cannot verify a private Tailscale (*.ts.net) domain, so set it up against your public production domain and store its credentials as Worker secrets, never in .env.local.

Two env vars drive it:

  • APPLE_CLIENT_ID — your Services ID (the web identifier, e.g. com.example.app.service), not your app bundle ID.
  • APPLE_CLIENT_SECRET — a short-lived JWT you generate from a "Sign in with Apple" private key (.p8).

The high-level flow (all automated by /configure-sso, including the JWT generation):

  1. In the Apple Developer portal, create a primary App ID with the "Sign in with Apple" capability, then a separate Services ID (this becomes APPLE_CLIENT_ID).
  2. On the Services ID's Web Authentication Configuration, add your bare production domain (no https://) under Domains and the full https://yourdomain.com/api/auth/callback/apple under Return URLs.
  3. Create a "Sign in with Apple" key, download the .p8, and note the Key ID and your Team ID.
  4. Generate the client-secret JWT from the .p8 (it expires in 6 months max — set a reminder to rotate it, or logins silently break).
# Production Worker secrets (not .env.local):
APPLE_CLIENT_ID=com.example.app.service
APPLE_CLIENT_SECRET=eyJ...   # the generated JWT

The /configure-sso skill's Apple section documents each portal step; see also the Apple links in apps/web/.env.example.

Facebook

Facebook is also wired in lib/auth.ts (FACEBOOK_CLIENT_ID / FACEBOOK_CLIENT_SECRET), but the email permission returns nothing until your app clears Facebook's Business Verification + App Review. Until then, sign-in completes but Better Auth receives no email and the login fails — there's no dev workaround. Wire it only when you're ready to go through Facebook's review.

Account linking

Account linking is enabled: signing in with a provider whose email matches an existing account links them rather than creating a duplicate user. The trusted providers are google, github, apple, and facebook (see account.accountLinking in lib/auth.ts).

Adding another provider

Better Auth supports many providers beyond the four wired by default (Microsoft, for example, is supported by the /configure-sso skill but is not in the template's socialProviders). To add one:

  1. Add it to socialProviders in apps/web/lib/auth.ts:

    socialProviders: {
      // ...existing providers
      microsoft: {
        clientId: process.env.MICROSOFT_CLIENT_ID || "",
        clientSecret: process.env.MICROSOFT_CLIENT_SECRET || "",
      },
    }
  2. Add a sign-in button that calls authClient.signIn.social({ provider: "microsoft", callbackURL: "/app" }).

  3. Register the OAuth app with the provider and set its credentials in your env file.

Troubleshooting

redirect_uri_mismatch — the redirect URI in the provider's settings must exactly match {BETTER_AUTH_URL}/api/auth/callback/{provider}, with no trailing slash. Note HTTPS, not HTTP, and no port for tailnet/prod hosts (the sidecar terminates TLS on :443).

Sign-in fails with "Invalid email or password" from an OAuth flow — usually a trustedOrigins miss: the origin you started on isn't trusted. In dev the template trusts localhost/127.0.0.1/0.0.0.0 on the dev port and any *.ts.net host; in production only BETTER_AUTH_URL is trusted, so make sure it's set to your real origin.

Session not persisting — confirm BETTER_AUTH_URL and APP_URL are set correctly for the host you're on.

Security notes

  • Never commit OAuth credentials. Use env vars (and Worker secrets in production).
  • BETTER_AUTH_SECRET must be a random 32+ character string (openssl rand -base64 32).
  • In production, session cookies are automatically secure. Security headers are configured in apps/web/next.config.ts.

On this page