| 1 | --- |
| 2 | title: "Part 5: Add GitHub sign-in" |
| 3 | description: Register a GitHub OAuth app, bind its credentials through Effect Config, and sign in from the browser. |
| 4 | sidebar: |
| 5 | order: 5 |
| 6 | --- |
| 7 | |
| 8 | Continue from [Part 4](/better-auth/tutorial/part-4). GitHub credentials come from a GitHub OAuth application; Alchemy's automatic signing secret is a separate value. |
| 9 | |
| 10 | ## Register the local OAuth application |
| 11 | |
| 12 | Use the `DEV_URL` captured from Alchemy in [Part 1](/better-auth/tutorial/part-1). Print the two URLs to register: |
| 13 | |
| 14 | ```sh |
| 15 | printf 'Homepage URL: %s\n' "$DEV_URL" |
| 16 | printf 'Authorization callback URL: %s/api/auth/callback/github\n' "$DEV_URL" |
| 17 | ``` |
| 18 | |
| 19 | Open [GitHub's new OAuth application form](https://github.com/settings/applications/new), name the app, and paste those values into its homepage and callback fields. |
| 20 | |
| 21 | Use a separate OAuth application for production so its credentials remain independent of local development. |
| 22 | |
| 23 | ## Keep credentials out of Git |
| 24 | |
| 25 | ```gitignore |
| 26 | # .gitignore |
| 27 | .env |
| 28 | .env.* |
| 29 | !.env.example |
| 30 | node_modules/ |
| 31 | .alchemy/ |
| 32 | public/ui.js |
| 33 | ``` |
| 34 | |
| 35 | Preserve the local `.alchemy` directory even though it is ignored by Git; it contains this tutorial's deployment state. |
| 36 | |
| 37 | ## Supply the GitHub credentials |
| 38 | |
| 39 | Generate a client secret in the GitHub application settings, then save the values locally: |
| 40 | |
| 41 | ```dotenv |
| 42 | # .env |
| 43 | GITHUB_CLIENT_ID=your-local-oauth-client-id |
| 44 | GITHUB_CLIENT_SECRET=your-local-oauth-client-secret |
| 45 | ``` |
| 46 | |
| 47 | These names are application configuration keys, not built-in Better Auth environment variables. The next step reads them explicitly. |
| 48 | |
| 49 | ## Read the client ID |
| 50 | |
| 51 | ```diff lang="typescript" |
| 52 | // src/auth.ts |
| 53 | +import * as Config from "effect/Config"; |
| 54 | |
| 55 | -const makeAuth = BetterAuth({ |
| 56 | - basePath: "/api/auth", |
| 57 | - emailAndPassword: { enabled: true }, |
| 58 | +const makeAuth = Effect.gen(function* () { |
| 59 | + const clientId = yield* Config.String("GITHUB_CLIENT_ID"); |
| 60 | + return yield* BetterAuth({ |
| 61 | + basePath: "/api/auth", |
| 62 | + emailAndPassword: { enabled: true }, |
| 63 | + }); |
| 64 | }); |
| 65 | ``` |
| 66 | |
| 67 | 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. |
| 68 | |
| 69 | ## Read the client secret |
| 70 | |
| 71 | ```diff lang="typescript" |
| 72 | // src/auth.ts |
| 73 | +import * as Redacted from "effect/Redacted"; |
| 74 | |
| 75 | const makeAuth = Effect.gen(function* () { |
| 76 | const clientId = yield* Config.String("GITHUB_CLIENT_ID"); |
| 77 | + const clientSecret = yield* Config.Redacted("GITHUB_CLIENT_SECRET"); |
| 78 | return yield* BetterAuth({ |
| 79 | basePath: "/api/auth", |
| 80 | emailAndPassword: { enabled: true }, |
| 81 | }); |
| 82 | }); |
| 83 | ``` |
| 84 | |
| 85 | Keep the secret redacted until the library needs the string. Neither credential belongs in the browser bundle. |
| 86 | |
| 87 | ## Enable GitHub |
| 88 | |
| 89 | ```diff lang="typescript" |
| 90 | // src/auth.ts |
| 91 | return yield* BetterAuth({ |
| 92 | basePath: "/api/auth", |
| 93 | emailAndPassword: { enabled: true }, |
| 94 | + socialProviders: { |
| 95 | + github: { |
| 96 | + clientId, |
| 97 | + clientSecret: Redacted.value(clientSecret), |
| 98 | + }, |
| 99 | + }, |
| 100 | }); |
| 101 | ``` |
| 102 | |
| 103 | The existing Auth service and middleware infer the updated options. The database layer stays unchanged. |
| 104 | |
| 105 | ## Add a sign-in button |
| 106 | |
| 107 | ```diff lang="html" |
| 108 | <!-- public/index.html --> |
| 109 | <button id="sign-out" type="button">Sign out</button> |
| 110 | +<button id="github" type="button">Sign in with GitHub</button> |
| 111 | ``` |
| 112 | |
| 113 | ## Start the authorization flow |
| 114 | |
| 115 | ```typescript |
| 116 | // Append to src/ui.ts |
| 117 | const signInWithGitHub = async () => { |
| 118 | const { error } = await authClient.signIn.social({ |
| 119 | provider: "github", |
| 120 | callbackURL: "/", |
| 121 | }); |
| 122 | if (error) show(error.message ?? "GitHub sign-in failed"); |
| 123 | }; |
| 124 | |
| 125 | document.querySelector("#github")!.addEventListener("click", () => { |
| 126 | void signInWithGitHub().catch(() => show("Unable to reach the server.")); |
| 127 | }); |
| 128 | ``` |
| 129 | |
| 130 | The client redirects to GitHub and returns through the registered callback. The resulting session works with the same `/api/me` endpoint. |
| 131 | |
| 132 | Better Auth handles failed callbacks on its built-in error page. No custom error query parameter or callback handler is needed. |
| 133 | |
| 134 | ## Restart with the new configuration |
| 135 | |
| 136 | Stop the running development command, rebuild the client, and start it again: |
| 137 | |
| 138 | ```sh |
| 139 | bun build ./src/ui.ts --target browser --outdir ./public |
| 140 | bunx alchemy dev --stage tutorial |
| 141 | ``` |
| 142 | |
| 143 | Try GitHub sign-in and call the protected API. Then sign out, retry, and cancel at GitHub to check the error path. |
| 144 | |
| 145 | Use the [GitHub provider guide](/better-auth/sign-in-providers/github) for account linking and provider-specific settings. Continue to [Part 6: Deploy and verify](/better-auth/tutorial/part-6). |