Better Auth with Vercel Connect
Vercel Connect is available on all plans and is subject to the Vercel Connect terms
The @vercel/connect/betterauth adapter lets your application sign users in
through a Vercel Connect connector. It configures Better Auth's
genericOAuth
plugin to use the Vercel Connect OAuth gateway.
This tutorial uses a Next.js App Router application. After you finish, a user can select a sign-in button, authorize the connector, and return with a Better Auth session.
Before you start, make sure you have:
- A Vercel project with OIDC authentication
- A Better Auth database, or a PostgreSQL database for the example in this guide
- An OAuth connector whose identity response includes a stable subject, email, and name for the signing-in user
- A project link between the connector and every Vercel environment that runs the application
- Node.js 18 or later
If you supply your own OAuth application credentials when creating the
connector, add https://connect.vercel.com/callback as an allowed redirect URI
in the provider's application settings. This URI returns the provider's
response to Vercel Connect. It is different from the Better Auth callback in
your application.
Link your application to the Vercel project that can use the connector. Then pull a development OIDC token:
terminalvercel link vercel env pullvercel env pulladdsVERCEL_OIDC_TOKENto.env.local. The adapter uses this token to authenticate the authorization-code exchange. Vercel provides and rotates the token automatically after you deploy.If the connector is not linked to the project yet, attach it:
terminalvercel connect attach slack/acme-slackBy default, this enables Production, Preview, and Development. Configure Custom Environments separately. See Project links for environment controls.
Install Better Auth, Vercel Connect, and the PostgreSQL client used in this example:
Terminalpnpm i @vercel/connect better-auth pgTerminalyarn add @vercel/connect better-auth pgTerminalnpm i @vercel/connect better-auth pgTerminalbun add @vercel/connect better-auth pgIf your Better Auth application already has a database adapter, keep it and omit
pg.Add the Better Auth settings, your database connection, and the connector UID to
.env.local:.env.localBETTER_AUTH_SECRET=replace-with-at-least-32-random-characters BETTER_AUTH_URL=http://localhost:3000 DATABASE_URL=postgresql://user:password@host:5432/database CONNECTOR_SLACK=slack/acme-slackGenerate a high-entropy Better Auth secret with
openssl rand -base64 32. SetBETTER_AUTH_URLto your production origin in Production.Create the Better Auth server configuration:
lib/auth.tsimport { Pool } from 'pg'; import { betterAuth } from 'better-auth'; import { genericOAuth } from 'better-auth/plugins/generic-oauth'; import { connect, ConnectError, } from '@vercel/connect/betterauth'; export const auth = betterAuth({ secret: process.env.BETTER_AUTH_SECRET!, baseURL: process.env.BETTER_AUTH_URL!, database: new Pool({ connectionString: process.env.DATABASE_URL!, }), plugins: [ genericOAuth({ config: [ connect({ providerId: 'slack', connector: process.env.CONNECTOR_SLACK!, }), ], }), ], onAPIError: { errorURL: '/sign-in', onError(error) { if (error instanceof ConnectError) { console.error('Vercel Connect sign-in failed', { code: error.code, status: error.status, statusText: error.statusText, }); return; } console.error('Better Auth request failed', error); }, }, });The
connectorvalue can be a human-readable UID such asslack/acme-slackor an opaque connector key that starts withscl_.providerIdbelongs to Better Auth. It appears in Better Auth routes and account records, so keep it stable.If this is a new Better Auth database, create its tables:
terminalpnpm dlx auth@latest migrateExpose Better Auth's catch-all handler:
app/api/auth/[...all]/route.tsimport { toNextJsHandler } from 'better-auth/next-js'; import { auth } from '@/lib/auth'; export const { GET, POST } = toNextJsHandler(auth);The generic OAuth plugin mounts its callback under this handler. For the
slackprovider ID, the callback is:http://localhost:3000/api/auth/oauth2/callback/slackIn Production, Better Auth uses your
BETTER_AUTH_URLorigin. You do not need to create another callback route.Add the generic OAuth client plugin:
lib/auth-client.tsimport { createAuthClient } from 'better-auth/react'; import { genericOAuthClient } from 'better-auth/client/plugins'; export const authClient = createAuthClient({ plugins: [genericOAuthClient()], });Create a sign-in button:
app/sign-in/sign-in-button.tsx'use client'; import { authClient } from '@/lib/auth-client'; export function SignInButton() { async function signIn() { const { error } = await authClient.signIn.oauth2({ providerId: 'slack', callbackURL: '/', errorCallbackURL: '/sign-in', }); if (error) { console.error('Could not start sign-in', error); } } return <button onClick={signIn}>Sign in with Slack</button>; }Render the button from a page:
app/sign-in/page.tsximport { SignInButton } from './sign-in-button'; export default function SignInPage() { return ( <main> <h1>Sign in</h1> <SignInButton /> </main> ); }providerIdmust match the value passed toconnect().callbackURLis the page Better Auth opens after a successful sign-in. It is not the OAuth callback route.Start your application with
.env.localloaded, then open the sign-in page:terminalpnpm devSelect Sign in with Slack. Better Auth redirects the browser to the Vercel Connect OAuth gateway. After authorization, the gateway returns to
/api/auth/oauth2/callback/slack. Better Auth creates or updates the user and redirects to/.
By default, connect() requests these scopes:
openid profile email offline_accessThe adapter adds offline_access automatically, including when you provide a
custom scope list. It also removes duplicate scopes.
Pass scopes when your connector needs other scopes. Include the identity
scopes that your sign-in flow needs:
connect({
providerId: 'slack',
connector: process.env.CONNECTOR_SLACK!,
scopes: ['openid', 'profile', 'email', 'channels:read'],
})The resulting request includes offline_access without requiring you to add
it. Only request scopes that your application needs and that the connector's
provider supports.
Authorization runs in your project's context. The token exchange requires a
Vercel OIDC token, and the connector must belong to your team, with a
project link that includes the calling
project and environment. The getVercelOidcToken option on connect() is
available for tests or runtimes that mint this token another way.
The adapter throws ConnectError when the gateway cannot exchange the
authorization code or fetch user information. The class exposes:
code: a machine-readable Connect or provider error codestatusandstatusText: the gateway HTTP responsevendor: structured details returned by the upstream provider, when present
The onAPIError handler in the server configuration logs safe fields when
Better Auth passes through the original adapter error. Do not send raw
token-exchange responses or the vendor payload to the browser.
If the gateway returns no access token for its user information request, the
adapter throws a ConnectError with the code missing_access_token. Use the
error code and status for diagnostics, then verify the connector, project
link, requested scopes, and local OIDC token.
Better Auth catches errors from its custom authorization-code exchange, logs
them on the server, and redirects with oauth_code_verification_failed.
errorCallbackURL controls the redirect after Better Auth has read the OAuth
state. The global onAPIError.errorURL is the fallback for errors that happen
before that state is available. Keep both URLs on a trusted application origin
and show a generic message to the user.
These three features solve different problems:
- Connect OAuth gateway sign-in: The integration in this guide signs a person into your application. Better Auth receives identity data from Vercel Connect and creates an application session.
- Provider API token access: Use
getToken()from@vercel/connectwhen your server needs a scoped token for a provider API. Do not send the access token stored by Better Auth's generic OAuth account to the provider API. Provider token requests have their own subject, installation, and scope rules. See Tokens. - Vercel Passport: Passport protects a deployment before a request reaches your application. It does not create a Better Auth session and does not issue provider API tokens. See Passport.
Was this helpful?