Skip to content
v3.0.1 — MCP OAuth, organizations, passkeys, SSR adaptersChangelog
Convex Auth
Esc
↑↓navigate↵open⌘Jpreview
On this page

Migrating to v3

What changed in v3 and whether you need to act.

@vortex-api/convex-auth v3 removes deprecated API surfaces, drops legacy session-compatibility fallbacks, and tightens the SSR landing flow. Most consumers upgrade with no code changes — use this page to find the entries that apply to you.

Do I need to do anything?

Change You are affected if… Effort
Landing verifier binding Users open magic-link/OAuth emails in a different browser or webview Set requireLandingVerifier: false, or handle ?error=landing_verifier_mismatch
getSessionByToken requires expiresAt You wrote a custom OidcProviderStorageAdapter Return expiresAt (ms) — now required
Session fallback removals Upgrading from < 2.5.x with live sessions Deploy latest 2.x first so sessions backfill
Provider args removed Passing sendVerificationEmailOnSignUp/OnSignIn at top level Move them under email
resolvePermissionOverride removed You implemented the B2B override adapter Model the variance as roles
Stricter redirect validation Passing callbackURL/errorURL/newUserURL to untrusted origins Add them to trustedOrigins
path-to-regexp v8 createRouteMatcher patterns use unnamed groups like /(a|b)/ Rewrite as /:key or RegExp

If none of the “you are affected” rows apply, v3 is a drop-in upgrade.


Session landings are bound to the initiating browser (#355)

What changed. Session-triple landings (?token=&refreshToken=&sessionId= in the URL) now require the __convexAuthLandingVerifier cookie that was minted in the browser that started the flow. A landing opened in a different browser — e.g. a magic link the user reads on their phone, or inside a mail client’s in-app webview — no longer writes auth cookies. The redirect still completes, but with the session params stripped and ?error=landing_verifier_mismatch appended.

Why. Anyone holding the landing URL could land the session — a login CSRF where an attacker finishes their own OAuth flow and feeds the victim the resulting landing URL.

What to do. Nothing if your flows start and finish in the same browser — the verifier is threaded automatically (OAuth via signed state, magic links via the verifier record). If your product expects cross-browser completion (common for magic links):

// convex/auth boundary or middleware config
requireLandingVerifier: false;

…or keep the check on and render a real error page when the landing URL carries ?error=landing_verifier_mismatch — the recommended option. React Native / non-DOM clients never bind; token-mode web clients bind client-side (requireLandingVerifier={false} opts out there too).

OidcProviderStorageAdapter.getSessionByToken must return expiresAt (#334)

What changed. getSessionByToken previously accepted { userId, revokedAt?, expiresAt? }. In v3 expiresAt is required: an adapter returning only { userId } no longer type-checks instead of silently skipping session expiry at runtime.

What to do. Custom storage adapters return the session’s expiry timestamp in milliseconds:

getSessionByToken: async (token) => {
  const session = await lookupSession(token);
  if (!session) return null;
  return {
    userId: session.userId,
    expiresAt: session.expiresAtMs, // now required
    revokedAt: session.revokedAtMs, // still optional — omit when not revoked
  };
};

The built-in component-backed adapter always populates expiresAt; consumers on the default storage need no changes.

Upgrade path: deploy the latest 2.x first

v3 removes three legacy-compatibility fallbacks in the session layer:

  • Legacy passkey sessions are no longer auto-revoked by revokePasskeySessions (#337 — sessions minted before v2.4.0 lacked credentialId).
  • identityId is no longer read from JWT claims during session refresh — only the session row’s column is honored (#373 — column added in v2.5.1).
  • Sessions without a familyId are no longer matched by revokeSessionFamily (#374 — threading added in v2.4.1).

These removals are safe because the 2.x line backfills the fields: migrateSession stamps familyId/identityId on any session that rotates through refresh, and the 30-day refresh TTL means unmigrated rows expire on their own.

If you are running a version older than 2.5.x, deploy the latest 2.x release first and let it run long enough for active sessions to refresh once (a few hours covers any active user). Sessions that never migrate lose family-revocation coverage — e.g. “sign out other sessions” — after upgrading to v3. Fresh sign-ins are unaffected.

Deprecated provider args removed

What changed. nativeEmailAndPassword no longer accepts the top-level sendVerificationEmailOnSignUp / sendVerificationEmailOnSignIn args (deprecated since 2.x).

What to do. Move them under email:

nativeEmailAndPassword(component, {
  email: {
    sendEmail,
    sendOnSignUp: true, // was sendVerificationEmailOnSignUp
    sendOnSignIn: false, // was sendVerificationEmailOnSignIn
  },
});

resolvePermissionOverride adapter removed

What changed. The B2B glue’s resolvePermissionOverride slot is deleted. Per-member overrides stored authorization state outside the component’s source of truth — the canonical model is roles carry permissions, members carry roles.

What to do. If you implemented it: express each override variance as a role (e.g. a member+export role) and assign it, or move the field into the component. expandPermissions is unchanged and remains the hook for wildcard/inheritance expansion.

Stricter redirect & callback validation

What changed. The OAuth signIn action now validates callbackURL/errorURL/newUserURL against the same merged allowlist the HTTP routes always used (SITE_URL, CONVEX_SITE_URL, trustedOrigins, OIDC login origin). Previously only the routes validated; the public action accepted arbitrary origins — an OAuth URL could exfiltrate ?token= to an attacker origin.

Also tightened: protocol-relative URLs (//evil.com) resolve before comparison; Sec-Fetch-Site: same-site no longer skips origin validation; relative redirect URLs resolve against SITE_URL (not http://localhost); magic-link errorCallbackURL/newUserCallbackURL are validated; custom schemes (myapp://, exp://) work only via explicit trustedOrigins patterns.

What to do. If any flow passes callback/redirect URLs to origins outside your site — custom-scheme deep links, a marketing domain, a staging origin — add them to emailAndPassword.trustedOrigins / oauth.trustedOrigins. Everything else is unchanged.

path-to-regexp v8 (Next.js adapter)

The adapter’s runtime dependency moved v6 → v8. Your existing patterns still work — the adapter translates the legacy wildcard forms internally:

  • /api/(.*) → /api/{*splat} (slash required, zero-or-more segments — identical to v6, verified pattern-by-pattern)
  • Glued /dashboard(.*) → /dashboard{*splat} (matches /dashboardfoo, as v6 did)
  • Native v8 syntax (/api/{*splat}, /api/*rest) is also accepted
  • RegExp instances and predicate functions are untouched

Patterns outside the advertised contract now throw a descriptive error at createRouteMatcher construction — unnamed groups like /(a|b)/, and bare * wildcards (which threw in v6 as well). Rewrite those as v8 named parameters (/:key) or pass RegExp instances.

Everything else in 3.0.0

No action required — listed for completeness:

  • Session security fixes — revokeOtherSessions now spares the caller’s whole session family, and family revocation only fires on rotated-token replay (administrative revocation replays get a quiet rejection instead of nuking the family).
  • Index cleanup — 8 covered prefix indexes dropped; applies on your next convex dev/push. Creation-ordered list endpoints keep their dedicated indexes.
  • API keys — allowedIpRanges now accepted on issueApiKey / issueServiceOwnedApiKey in the feature-gated component (parity with the monolith).
  • New entries — @vortex-api/convex-auth/tanstack-start adapter and @vortex-api/convex-auth/test (register, convexAuthTest, modules, schema for convex-test harnesses).
  • ConvexAuthClient type — the alias remains but is deprecated; use ConvexBetterAuthClient.

Was this page helpful?