Part 5: Add GitHub sign-in
Continue from Part 4. GitHub credentials come from a GitHub OAuth application; Alchemy’s automatic signing secret is a separate value.
Register the local OAuth application
Section titled “Register the local OAuth application”Use the DEV_URL captured from Alchemy in Part 1. Print the two URLs to register:
printf 'Homepage URL: %s\n' "$DEV_URL"printf 'Authorization callback URL: %s/api/auth/callback/github\n' "$DEV_URL"Open GitHub’s new OAuth application form, name the app, and paste those values into its homepage and callback fields.
Use a separate OAuth application for production so its credentials remain independent of local development.
Keep credentials out of Git
Section titled “Keep credentials out of Git”.env.env.*!.env.examplenode_modules/.alchemy/public/ui.jsPreserve the local .alchemy directory even though it is ignored by Git; it contains this tutorial’s deployment state.
Supply the GitHub credentials
Section titled “Supply the GitHub credentials”Generate a client secret in the GitHub application settings, then save the values locally:
GITHUB_CLIENT_ID=your-local-oauth-client-idGITHUB_CLIENT_SECRET=your-local-oauth-client-secretThese names are application configuration keys, not built-in Better Auth environment variables. The next step reads them explicitly.
Read the client ID
Section titled “Read the client ID”import * as Config from "effect/Config";
const makeAuth = BetterAuth({ basePath: "/api/auth", emailAndPassword: { enabled: true },const makeAuth = Effect.gen(function* () { const clientId = yield* Config.String("GITHUB_CLIENT_ID"); return yield* BetterAuth({ basePath: "/api/auth", emailAndPassword: { enabled: true }, });});Resolve configuration while constructing the Auth layer so Alchemy discovers and binds it. Reading it only inside a request handler would be too late for deployment-time binding discovery.
Read the client secret
Section titled “Read the client secret”import * as Redacted from "effect/Redacted";
const makeAuth = Effect.gen(function* () { const clientId = yield* Config.String("GITHUB_CLIENT_ID"); const clientSecret = yield* Config.Redacted("GITHUB_CLIENT_SECRET"); return yield* BetterAuth({ basePath: "/api/auth", emailAndPassword: { enabled: true }, });});Keep the secret redacted until the library needs the string. Neither credential belongs in the browser bundle.
Enable GitHub
Section titled “Enable GitHub” return yield* BetterAuth({ basePath: "/api/auth", emailAndPassword: { enabled: true }, socialProviders: { github: { clientId, clientSecret: Redacted.value(clientSecret), }, }, });The existing Auth service and middleware infer the updated options. The database layer stays unchanged.
Add a sign-in button
Section titled “Add a sign-in button”<button id="sign-out" type="button">Sign out</button><button id="github" type="button">Sign in with GitHub</button>Start the authorization flow
Section titled “Start the authorization flow”// Append to src/ui.tsconst signInWithGitHub = async () => { const { error } = await authClient.signIn.social({ provider: "github", callbackURL: "/", }); if (error) show(error.message ?? "GitHub sign-in failed");};
document.querySelector("#github")!.addEventListener("click", () => { void signInWithGitHub().catch(() => show("Unable to reach the server."));});The client redirects to GitHub and returns through the registered callback. The resulting session works with the same /api/me endpoint.
Better Auth handles failed callbacks on its built-in error page. No custom error query parameter or callback handler is needed.
Restart with the new configuration
Section titled “Restart with the new configuration”Stop the running development command, rebuild the client, and start it again:
bun build ./src/ui.ts --target browser --outdir ./publicbunx alchemy dev --stage tutorialTry GitHub sign-in and call the protected API. Then sign out, retry, and cancel at GitHub to check the error path.
Use the GitHub provider guide for account linking and provider-specific settings. Continue to Part 6: Deploy and verify.