Skip to content
Logo

Better Auth

Better Auth is a TypeScript authentication framework. @stripe/link-integrations-better-auth is a plugin that adds Link as a provider, so your application can let users connect their Link wallet to an existing account in your application.

Connecting a wallet gives your application an access token for that customer. Use it to call the Link API, for example to create spend requests for an agent or to read financial data. Better Auth handles the OAuth flow, token storage, and token refresh.

Install

The plugin requires Node.js 22 or newer and Better Auth 1.7.5 or newer. This is higher than the Node.js version that the TypeScript SDK requires.

pnpm add @stripe/link-integrations-better-auth better-auth

Configure the server

Before you begin, complete OAuth setup to get a Link OAuth client. Register https://yourapp.com/api/auth/callback/link as its callback URL. Adjust the path if you use a custom base path, and register your local development callback too.

Add the link plugin to your Better Auth configuration:

import {betterAuth} from 'better-auth'
import {link} from '@stripe/link-integrations-better-auth'
 
export const auth = betterAuth({
  account: {
    encryptOAuthTokens: true,
    accountLinking: {
      trustedProviders: ['link'],
      allowDifferentEmails: true,
    },
  },
  plugins: [
    link({
      clientId: process.env.LINK_CLIENT_ID!,
      clientSecret: process.env.LINK_CLIENT_SECRET!,
      publishableKey: process.env.STRIPE_PUBLISHABLE_KEY!,
    }),
  ],
})

allowDifferentEmails lets a user’s wallet email differ from their app email.

By default, the plugin requests the payment_methods.agentic and userinfo:read scopes. For sign-in only, set scopes: ['userinfo:read']. Always include userinfo:read.

Request financial data

The default scopes don’t grant access to financial data. To read transactions, balances, or account details, also pass authorizationDetails with only the source actions you need:

link({
  clientId: process.env.LINK_CLIENT_ID!,
  clientSecret: process.env.LINK_CLIENT_SECRET!,
  publishableKey: process.env.STRIPE_PUBLISHABLE_KEY!,
  authorizationDetails: [
    {
      type: 'source',
      actions: [
        'read_link_transactions',
        'read_external_transactions',
        'read_balances',
        'read_source_details',
      ],
    },
  ],
})

This keeps the default scopes, so the customer can authorize payments and financial data together. For financial data only, also set scopes: ['userinfo:read'].

If you request read_external_transactions, read_balances, or read_source_details, your Stripe account must be registered for Financial Connections. A customer can approve only some accounts or actions, so check what was granted before you rely on it. See Request financial data scopes.

Configure the client

Add linkClient to your Better Auth client:

import {createAuthClient} from 'better-auth/react'
import {linkClient} from '@stripe/link-integrations-better-auth/client'
 
export const authClient = createAuthClient({
  plugins: [linkClient()],
})

Use the plugin

After you configure the server and client, users can connect a wallet or sign in with Link.

Connect a wallet to the signed-in user:

await authClient.link.connect({callbackURL: '/settings'})

Sign in with Link:

await authClient.signIn.social({provider: 'link', callbackURL: '/dashboard'})

Better Auth’s account-linking and email-verification policies still apply to sign-in.

To disconnect a wallet, find the account with providerId === 'link' in authClient.listAccounts(), then call authClient.link.disconnect({accountId: account.id}). Disconnecting requires a fresh session, protects the user’s last sign-in method by default, and revokes Link access before it deletes the local record.

Get an access token

To call the Link API on the server, get the customer’s access token from Better Auth. In the following example, linkAccountId is the id of the customer’s Link account record, which is the account in authClient.listAccounts() with providerId === 'link'. It’s the same account.id you use to disconnect. Better Auth refreshes an expired token before it returns it.

import {headers} from 'next/headers'
 
const {accessToken} = await auth.api.getAccessToken({
  body: {accountId: linkAccountId},
  headers: await headers(),
})

Better Auth requires you to select the account in the request. If you enable account.storeAccountCookie, you can pass useAccountCookie: true instead of accountId. Pass the token to the TypeScript SDK or another Link API client.

Example

Better Auth integration source includes a runnable Next.js example.