Skip to main content

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:

ProviderWhere to create itCallback URL
GitHubGitHub settings, Developer settings, OAuth Appshttps://app.example.com/api/auth/callback/github
GoogleGoogle Cloud console, APIs and services, Credentialshttps://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:

bash
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_SECRET

Do 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:

convex/auth.ts
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:

app/pages/auth/signin.vue
<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 errorCallbackURL with error=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():

ts
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:

convex/auth.ts
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

  1. Sign in with each provider on the deployed origin.
  2. Reload the page and check that the session stays.
  3. Sign out and sign in again.
  4. 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.
  5. Sign in with email, link the provider, sign out, and sign in with the provider.