Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
9284108
Migrate Convex adapter to Better Auth 1.7
sshkeda Aug 21, 2026
7640768
fix: preserve declared Better Auth index order
sshkeda Aug 21, 2026
57007e5
fix: guard optional cross-domain auth methods
sshkeda Aug 21, 2026
1d0d46c
test: isolate Better Auth adapter options
sshkeda Aug 21, 2026
0598e92
docs: fix migration guide list formatting
sshkeda Aug 21, 2026
dd5a8cd
fix: document multi-tenant Microsoft migration
sshkeda Aug 21, 2026
9257a1c
fix: harden account identity enforcement
sshkeda Aug 21, 2026
ef66f09
fix: align credential account indexes
sshkeda Aug 21, 2026
ca807a1
fix: detect cross-domain client capability
sshkeda Aug 21, 2026
791953f
fix: harden account migration validation
sshkeda Aug 21, 2026
8fe58f0
fix: guard unique updates across pagination
sshkeda Aug 21, 2026
6dff06e
fix: validate Convex JWKS URL configuration
sshkeda Aug 21, 2026
21410bf
docs: clarify Local Install migration indexes
sshkeda Aug 21, 2026
6c31eb6
fix: validate unique constraints after triggers
sshkeda Aug 21, 2026
1c60eba
fix: validate Convex site URL format
sshkeda Aug 21, 2026
64196d7
docs: list required Local Install account indexes
sshkeda Aug 21, 2026
4e8f35a
fix: reject URL components in Convex site URL
sshkeda Aug 21, 2026
7919b6c
Return ComponentApi from the test register function (#447)
ianmacartney Oct 10, 2026
e3d4815
Update Better Auth compatibility and migration to 1.7.7
sshkeda Oct 13, 2026
df61d21
Fix filtered pagination cursor advancement
sshkeda Oct 13, 2026
4c2a5aa
Fix refresh-token guards for unset optional fields
sshkeda Oct 13, 2026
a2eed8a
Fix OAuth provider discovery and document signing keys
sshkeda Oct 13, 2026
d3fc65c
Clarify Local Install provider migration steps
sshkeda Oct 13, 2026
7c25603
Test OAuth refresh and discovery on a real Convex backend
sshkeda Oct 13, 2026
1bd223e
Clarify Better Auth 1.7 migration and plugin requirements
sshkeda Oct 13, 2026
1dc17b4
Clarify removal of legacy OAuth client fields
sshkeda Oct 13, 2026
c693545
Run OAuth tests through the existing E2E script
sshkeda Oct 13, 2026
5a7c5bf
Remove redundant comments
sshkeda Oct 13, 2026
6c9fe49
Keep Vitest aligned with upstream
sshkeda Oct 13, 2026
fe7e043
Merge latest upstream changes
sshkeda Oct 16, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/content/docs/features/local-install.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ installed component.
findOne,
findMany,
updateOne,
incrementOne,
updateMany,
deleteOne,
deleteMany,
Expand Down
2 changes: 1 addition & 1 deletion docs/content/docs/framework-guides/expo.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ description: Install and configure Convex + Better Auth for Expo.

```npm
npm install convex@latest @convex-dev/better-auth
npm install better-auth@~1.6.15 @better-auth/expo@~1.6.15
npm install better-auth@~1.7.7 @better-auth/expo@~1.7.7
```

</div>
Expand Down
2 changes: 1 addition & 1 deletion docs/content/docs/framework-guides/next.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ description: Install and configure Convex + Better Auth for Next.js.

```npm
npm install convex@latest @convex-dev/better-auth
npm install better-auth@~1.6.15
npm install better-auth@~1.7.7
```

</div>
Expand Down
2 changes: 1 addition & 1 deletion docs/content/docs/framework-guides/react.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ description: Install and configure Convex + Better Auth for React.

```npm
npm install convex@latest @convex-dev/better-auth
npm install better-auth@~1.6.15
npm install better-auth@~1.7.7
```

</div>
Expand Down
2 changes: 1 addition & 1 deletion docs/content/docs/framework-guides/sveltekit.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ npm install convex @mmailaender/convex-svelte

```npm
npm install @convex-dev/better-auth @mmailaender/convex-better-auth-svelte
npm install better-auth@~1.6.15
npm install better-auth@~1.7.7
```

</div>
Expand Down
2 changes: 1 addition & 1 deletion docs/content/docs/framework-guides/tanstack-start.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ description: Install and configure Convex + Better Auth for TanStack Start.

```npm
npm install convex@latest @convex-dev/better-auth
npm install better-auth@~1.6.15
npm install better-auth@~1.7.7
npm install @types/node --save-dev
```

Expand Down
2 changes: 1 addition & 1 deletion docs/content/docs/migrations/meta.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"title": "Migrations",
"pages": ["migrate-to-0-12", "migrate-to-0-11", "migrate-to-0-10", "migrate-to-0-9", "migrate-to-0-8"],
"pages": ["migrate-to-0-13", "migrate-to-0-12", "migrate-to-0-11", "migrate-to-0-10", "migrate-to-0-9", "migrate-to-0-8"],
"defaultOpen": true
}
308 changes: 308 additions & 0 deletions docs/content/docs/migrations/migrate-to-0-13.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,308 @@
---
title: Migrate to 0.13
description: Migrate to @convex-dev/better-auth@0.13
---

## Upgrade

<div className="fd-steps">

<div className="fd-step">

### Install dependencies

Update Better Auth and the component. Better Auth must be on `1.7.7` or later in
the `1.7.x` line. Update any installed `@better-auth/*` packages together.

```npm
npm install @convex-dev/better-auth@^0.13.0
npm install better-auth@~1.7.7
```

<Callout>
Review the [Better Auth 1.7 upgrade
guide](https://www.better-auth.com/docs/guides/1-7-upgrade-guide) and [1.7.7
release notes](https://github.com/better-auth/better-auth/releases/tag/v1.7.7)
for the features your app uses.
</Callout>

</div>

<div className="fd-step">

### Check existing accounts

Accounts still use `(providerId, accountId)`, as in 1.6. **No issuer backfill is
needed.** Check for duplicate pairs before upgrading; Better Auth now rejects
ambiguous account lookups. Resolve duplicates using trusted provider data and
keep different users separate. Credential accounts should have `accountId` equal
to `userId`.

If you tested 1.7.0–1.7.2, see [Early 1.7 deployments](#early-17-deployments)
below. Microsoft accounts have a separate migration described below.

</div>

<div className="fd-step">

### Deploy and test

Back up your data and test the upgrade on a development deployment first.
Regenerate Convex bindings, then deploy the component and application changes
together. Verify sign-in, existing sessions, account linking, token refresh, and
`/api/auth/convex/jwks` for the features you use.

Upgrade servers sharing verification storage and OAuth Proxy participants
together. Request fresh Magic Links and restart pending OAuth/SAML sign-ins
after deploying, as required by the 1.7.7 security fixes.

</div>

</div>

## Breaking changes

### Account selectors

Read the local account `id` from `listAccounts` and pass it as `accountId` to
`unlinkAccount`. For `getAccessToken`, `refreshToken`, and `accountInfo`, pass
exactly one of `{ accountId: account.id }` or `{ useAccountCookie: true }`.
Remove `providerId` from these selectors. Cookie selection requires
`account.storeAccountCookie` in your Better Auth configuration.

### Generic OAuth

Use the standard social client APIs:

```ts
await authClient.signIn.social({ provider: "my-provider" });
await authClient.linkSocial({ provider: "my-provider" });
```

These replace `signIn.oauth2()` and `oauth2.link()`. Remove
`genericOAuthClient()` and change provider callback URLs from
`/api/auth/oauth2/callback/:id` to `/api/auth/callback/:id`.

PKCE is enabled by default. Set `pkce: false` only for providers that reject it.
Remove `issuer` and `requireIssuerValidation`; issuer validation is automatic.

Review custom provider identity mapping. Discovery providers use verified `sub`,
and plain OAuth providers use `id`. Set `accountSubject` if that default is not
the provider's immutable identifier. Keep the provider identifier in
`getUserInfo().data`; `getUserInfo().user` contains mutable user fields, and
`mapProfileToUser` can no longer return `id`. See the upstream
[account identity guidance](https://www.better-auth.com/docs/guides/1-7-upgrade-guide#account-identity-keeps-the-provider-key).

### Microsoft accounts

Microsoft accounts now use `oid` instead of `sub`, including the Generic OAuth
Microsoft Entra helper. Pause Microsoft sign-in and linking while updating
stored account IDs from verified ID tokens or a trusted Entra export. The old
account row alone cannot supply this mapping.

The Generic OAuth `microsoftEntraId` helper requires a concrete tenant GUID and
an `id_token` from the token exchange. Use the built-in Microsoft provider for
`common`, `organizations`, or `consumers`. Follow the upstream guide's
[Microsoft migration steps](https://www.better-auth.com/docs/guides/1-7-upgrade-guide#migrate-microsoft-account-identifiers).

### Database joins

Move `experimental.joins` to `advanced.database.joins`. The Convex adapter
continues to disable native joins and use its existing relation queries.

### Expo

The `@better-auth/expo` client's `getCookie()` now returns a promise. Await it
and make callbacks that read cookies asynchronous. Passing `expo-secure-store`
directly still works; custom storage must implement `getItem`, `getItemAsync`,
`setItem`, and `setItemAsync`. See the upstream
[Expo migration guidance](https://www.better-auth.com/docs/guides/1-7-upgrade-guide#expo-and-react-native).

### Two-factor authentication

Check the returned `method` before reading `totpURI` or backup codes from
`enableTwoFactor`. Those fields are present only when `method === "totp"`. The
Next.js and TanStack examples include this check.

## Early 1.7 deployments

Better Auth 1.7.3 removed the required `issuer` field introduced in 1.7.0–1.7.2.
The bundled component keeps it optional to accept existing data, but no longer
includes issuer indexes or backfill functions.

For Local Install, remove issuer-based constraints and indexes. Keep `issuer`
optional until stored values are removed. If different issuers shared one
provider ID and overlapping account IDs, assign distinct provider IDs and update
the matching account rows before resuming sign-in.

## OAuth or OIDC provider

The conversion below is for apps that used `oidcProvider` to act as an identity
provider. Apps that only sign in through an external provider can skip it.

If you already use `@better-auth/oauth-provider` with Local Install, follow the
upstream
[OAuth client migration](https://www.better-auth.com/docs/guides/1-7-upgrade-guide#migrate-oauth-client-records)
before applying the generated schema. Existing clients need field backfills and
configuration changes; schema generation alone does not migrate their data. Skip
the legacy export and cleanup below.

Use a temporary Convex schema that accepts both the existing and migrated client
records. Backfill the new fields and remove the legacy `type` and `public`
fields from existing `oauthClient` documents before deploying the final
generated schema. Remove those fields in a component mutation with
`ctx.db.patch(id, { type: undefined, public: undefined })`; setting them to
`null` still leaves fields that the final schema rejects.

### Export and clear legacy records

Back up your data and pause authentication writes and public HTTP routes during
the conversion. Keep the bundled component mounted until export and cleanup are
complete. Add these internal wrappers:

```ts title="convex/migrations.ts"
import { v } from "convex/values";
import { internalMutation, internalQuery } from "./_generated/server";
import { components } from "./_generated/api";

export const listLegacyOAuthApplications = internalQuery({
args: { cursor: v.union(v.string(), v.null()) },
handler: async (ctx, { cursor }) => {
return await ctx.runQuery(
components.betterAuth.migrations.listLegacyOAuthApplications,
{ paginationOpts: { cursor, numItems: 100 } }
);
},
});

export const clearLegacyOAuthProviderRecords = internalMutation({
args: {
table: v.union(
v.literal("oauthApplication"),
v.literal("oauthAccessToken"),
v.literal("oauthConsent")
),
cursor: v.union(v.string(), v.null()),
},
handler: async (ctx, { table, cursor }) => {
return await ctx.runMutation(
components.betterAuth.migrations.clearLegacyOAuthProviderRecords,
{ table, paginationOpts: { cursor, numItems: 100 } }
);
},
});
```

Export every page of client registrations and save the results securely. The
export omits `clientSecret`; review custom `metadata` before reusing it.

```bash
npx convex run migrations:listLegacyOAuthApplications '{"cursor":null}'
```

After verifying the export, clear the old tokens, consents, and applications in
that order. This invalidates existing provider tokens and removes client
secrets.

```bash
npx convex run migrations:clearLegacyOAuthProviderRecords \
'{"table":"oauthAccessToken","cursor":null}'
npx convex run migrations:clearLegacyOAuthProviderRecords \
'{"table":"oauthConsent","cursor":null}'
npx convex run migrations:clearLegacyOAuthProviderRecords \
'{"table":"oauthApplication","cursor":null}'
```

For each command, pass the returned `continueCursor` to the next call until
`isDone` is true. A completed cursor is terminal; use `null` only to start a new
pass. Add `--prod` when running against production.

### Switch to Local Install

1. Follow the [Local Install guide](/local-install). Keep the component mounted
as `betterAuth` so existing data stays under `components.betterAuth`.
2. Install `@better-auth/oauth-provider@~1.7.7` and replace `oidcProvider()`
with `jwt()` and `oauthProvider()` in your static Better Auth options. Follow
the upstream
[provider migration guide](https://www.better-auth.com/docs/guides/1-7-upgrade-guide#as-an-identity-provider).
3. Generate the new schema after adding the plugin and clearing all three legacy
tables. Retain optional `account.issuer` if existing records still contain
it.
4. Deploy while writes remain paused. Confirm that existing users, accounts, and
sessions remain under the same component mount before proceeding.
5. Re-register exported clients. Map `redirectUrls` to `redirectUris`, parse
legacy `metadata` as JSON, and review grant types and token authentication.
Distribute new client secrets and issue new provider tokens.
6. Test authorization, consent, token issuance, refresh, user info, and logout
before restoring traffic. Restore the backup with the old application version
if the conversion fails.

### Configure provider signing keys

Use database-backed signing keys for this provider setup. Static keys passed to
`convex({ jwks: ... })` are not supported: the JWT plugin signs provider tokens
with database keys, while Convex publishes the static keys. Clients would then
be unable to verify those provider tokens.

In the runtime plugin array, place `jwt()` before `convex()` and
`oauthProvider()` after `convex()`. These plugins share endpoint keys, so order
matters. `registerRoutes` and `registerRoutesLazy` direct root OpenID discovery
to the OAuth provider when it is configured, so root discovery advertises the
provider issuer. Convex JWT verifiers should continue to use the Convex issuer
and `/api/auth/convex/jwks` directly.

Configure the JWT plugin to use Convex's public signing-key URL:

```ts
jwt({
disableSettingJwtHeader: true,
jwks: {
remoteUrl: `${process.env.CONVEX_SITE_URL}/api/auth/convex/jwks`,
keyPairConfig: { alg: "RS256" },
},
});
```

Replace `/api/auth` if you use a custom `basePath`. Use the same signing
algorithm as your Convex auth configuration (`RS256` by default). Disabling the
JWT header avoids generating an extra token when reading a session; Convex
clients use `/convex/token`.

### Custom consent pages

Before displaying consent details, verify the signed query on the server with
`verifyOAuthQueryParams` from `@better-auth/oauth-provider`. Use the provider's
resolved secret, `(await auth.$context).secret`, from your server-side auth
instance and keep it on the server. See the upstream
[consent screen example](https://www.better-auth.com/docs/plugins/oauth-provider#consent-screen).

## Local Install additional steps

Generate the 1.7.7 schema to a side file and compare it with your deployed
schema:

```npm
cd convex/betterAuth
npx auth generate --output generatedSchema.next.ts
```

Preserve existing plugin tables, custom fields, and indexes. Include the
generated credential lookup index `userId_providerId_accountId` on
`["userId", "providerId", "accountId"]` and export `incrementOne` with your
other adapter functions. The adapter implements `consumeOne` using the
component's atomic `deleteOne`.

If you already use Local Install with `oidcProvider`, copy
`src/component/migrations.ts` from the installed `@convex-dev/better-auth`
package to `convex/betterAuth/migrations.ts`. Install `convex-helpers` as a
direct dependency, choosing a version compatible with your installed Convex
version. The helpers use its `paginator`, since `ctx.db.query(...).paginate()`
is not supported inside components. Use the internal wrappers above to call
them.

Before exporting, deploy a temporary schema that retains `oauthApplication` and
accepts both legacy and new `oauthAccessToken` and `oauthConsent` records. Keep
both sets of fields and indexes, make required new fields such as `clientId` and
`scopes` optional, and allow `scopes` to be either a string or an array of
strings. Clear legacy records before applying the final provider schema, and
remove the temporary migration helpers and wrappers in that same deployment.
Loading