Skip to content
Docs

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.

This guide uses slack/acme-slack as the connector UID and slack as the Better Auth provider ID. Replace both values with your connector and a stable provider ID for your application.

  1. Link your application to the Vercel project that can use the connector. Then pull a development OIDC token:

    terminal
    vercel link
    vercel env pull

    vercel env pull adds VERCEL_OIDC_TOKEN to .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:

    terminal
    vercel connect attach slack/acme-slack

    By default, this enables Production, Preview, and Development. Configure Custom Environments separately. See Project links for environment controls.

  2. Install Better Auth, Vercel Connect, and the PostgreSQL client used in this example:

    Terminal
    pnpm i @vercel/connect better-auth pg
    Terminal
    yarn add @vercel/connect better-auth pg
    Terminal
    npm i @vercel/connect better-auth pg
    Terminal
    bun add @vercel/connect better-auth pg

    If your Better Auth application already has a database adapter, keep it and omit pg.

  3. Add the Better Auth settings, your database connection, and the connector UID to .env.local:

    .env.local
    BETTER_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-slack

    Generate a high-entropy Better Auth secret with openssl rand -base64 32. Set BETTER_AUTH_URL to your production origin in Production.

  4. Create the Better Auth server configuration:

    lib/auth.ts
    import { 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 connector value can be a human-readable UID such as slack/acme-slack or an opaque connector key that starts with scl_. providerId belongs 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:

    terminal
    pnpm dlx auth@latest migrate
  5. Expose Better Auth's catch-all handler:

    app/api/auth/[...all]/route.ts
    import { 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 slack provider ID, the callback is:

    http://localhost:3000/api/auth/oauth2/callback/slack

    In Production, Better Auth uses your BETTER_AUTH_URL origin. You do not need to create another callback route.

  6. Add the generic OAuth client plugin:

    lib/auth-client.ts
    import { 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.tsx
    import { SignInButton } from './sign-in-button';
     
    export default function SignInPage() {
      return (
        <main>
          <h1>Sign in</h1>
          <SignInButton />
        </main>
      );
    }

    providerId must match the value passed to connect(). callbackURL is the page Better Auth opens after a successful sign-in. It is not the OAuth callback route.

  7. Start your application with .env.local loaded, then open the sign-in page:

    terminal
    pnpm dev

    Select 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_access

The 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 code
  • status and statusText: the gateway HTTP response
  • vendor: 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/connect when 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.
Last updated September 18, 2026

Was this helpful?

supported.