Social sign-in
Let users sign in with GitHub or Google through the Nuxt auth proxy.
Add GitHub or Google sign-in to an application that already has working email sign-in. Complete Add authentication first.
Better Auth runs in Convex, but the browser only talks to your Nuxt origin.
The provider therefore sends the user back to
https://app.example.com/api/auth/callback/<provider>. The Nuxt auth proxy
forwards that request to Convex.
1. Create the provider app
Register an OAuth app with each provider. Use your exact SITE_URL in the
callback URL:
| Provider | Where to create it | Callback URL |
|---|---|---|
| GitHub | GitHub settings, Developer settings, OAuth Apps | https://app.example.com/api/auth/callback/github |
| Google Cloud console, APIs and services, Credentials | https://app.example.com/api/auth/callback/google |
For local development, register a separate app with
http://localhost:3000/api/auth/callback/github. Each environment needs its
own callback URL, because SITE_URL differs. Do not point a provider at the
.convex.site origin.
2. Store the client secrets in Convex
The provider options run inside Convex, so the secrets belong in the Convex
environment, not in Nuxt. Set them for the deployment that .env.local
selects:
pnpm exec better-convex convex env set GITHUB_CLIENT_ID your-github-client-id
printf '%s' "$GITHUB_CLIENT_SECRET" | pnpm exec better-convex convex env set GITHUB_CLIENT_SECRET
pnpm exec better-convex convex env set GOOGLE_CLIENT_ID your-google-client-id
printf '%s' "$GOOGLE_CLIENT_SECRET" | pnpm exec better-convex convex env set GOOGLE_CLIENT_SECRETDo not print or commit the secrets.
3. Add the providers to the factory
Pass the providers as a function. The function runs for each auth request, so a missing variable fails that request with a clear error instead of failing the deployment:
import { createBetterConvexAuth } from '@lupinum/better-convex-nuxt/better-auth/server'
import { components } from './_generated/api'
import type { DataModel } from './_generated/dataModel'
function requireEnv(name: string): string {
const value = process.env[name]
if (!value) throw new Error(`${name} is required`)
return value
}
export const auth = createBetterConvexAuth<DataModel>(components.betterAuth, {
socialProviders: () => ({
github: {
clientId: requireEnv('GITHUB_CLIENT_ID'),
clientSecret: requireEnv('GITHUB_CLIENT_SECRET'),
},
google: {
clientId: requireEnv('GOOGLE_CLIENT_ID'),
clientSecret: requireEnv('GOOGLE_CLIENT_SECRET'),
},
}),
})Keep your other options, such as emailAndPassword or email, in the same
call. Nothing changes in nuxt.config.ts: the auth proxy already forwards
/api/auth/callback/*.
4. Add the sign-in buttons
Call client.signIn.social() from a click handler. Better Auth redirects the
browser to the provider:
<script setup lang="ts">
definePageMeta({ convexAuth: 'guest' })
const route = useRoute()
const { client, pending } = useConvexAuth()
const returnTo = useConvexAuthReturnTo()
const signInError = ref(
route.query.error === 'account_not_linked'
? 'This email already has an account. Sign in with your password, then link the provider in your settings.'
: route.query.error
? 'Sign in could not be completed.'
: '',
)
async function signInWith(provider: 'github' | 'google') {
signInError.value = ''
const result = await client.signIn.social({
provider,
callbackURL: returnTo.value ?? '/dashboard',
errorCallbackURL: '/auth/signin',
})
if (result.error) signInError.value = 'Sign in could not be completed.'
}
</script>
<template>
<button :disabled="pending" @click="signInWith('github')">Continue with GitHub</button>
<button :disabled="pending" @click="signInWith('google')">Continue with Google</button>
<p v-if="signInError" role="alert">{{ signInError }}</p>
</template>callbackURL and errorCallbackURL are local paths in your application.
When the provider step fails, Better Auth sends the browser to
errorCallbackURL with an error query parameter. useConvexAuthReturnTo()
returns the page that sent the user to sign in, or null.
Account linking rules
createBetterConvexAuth fixes these rules. You cannot turn them off:
- A social sign-in never joins an existing account automatically. If a user
with the same email address already exists, the sign-in fails and Better
Auth redirects to
errorCallbackURLwitherror=account_not_linked. - A provider account can join only an account with the same email address.
- A user cannot remove the last sign-in method from an account.
To join a provider to an existing account, the user signs in first with the
existing method. Then the application calls client.linkSocial():
const { client } = useConvexAuth()
await client.linkSocial({ provider: 'github', callbackURL: '/settings/accounts' })Better Auth accepts the link only when the provider reports the email address
as verified. To accept a provider without that report, list it in
account.accountLinking.trustedProviders:
export const auth = createBetterConvexAuth<DataModel>(components.betterAuth, {
socialProviders: () => ({
github: {
clientId: requireEnv('GITHUB_CLIENT_ID'),
clientSecret: requireEnv('GITHUB_CLIENT_SECRET'),
},
}),
account: { accountLinking: { trustedProviders: ['github'] } },
})Each name must be unique and must be a configured provider. Otherwise the
factory fails with AUTH_CONFIG_INVALID. Trust a provider only when you know
that it checks email ownership.
Provider tokens stay in Convex
Better Auth stores the provider's access, refresh, and ID tokens encrypted in
the auth component. The /get-access-token and /refresh-token routes are
turned off. Your application cannot read these tokens, so it cannot call the
GitHub or Google API as the user. Use a separate integration for that.
Test the flow
- Sign in with each provider on the deployed origin.
- Reload the page and check that the session stays.
- Sign out and sign in again.
- Create an email account, then try a social sign-in with the same email. Check that the sign-in fails and your error page explains the next step.
- Sign in with email, link the provider, sign out, and sign in with the provider.