|
| 1 | +--- |
| 2 | +title: Better Auth external plugin calls need Convex actions |
| 3 | +date: 2026-04-29 |
| 4 | +category: integration-issues |
| 5 | +module: auth-runtime |
| 6 | +problem_type: runtime_error |
| 7 | +component: authentication |
| 8 | +symptoms: |
| 9 | + - `ctx.auth.api.updateOrganization()` fails inside a Convex mutation when the Stripe plugin is installed. |
| 10 | + - Convex throws `Can't use setTimeout in queries and mutations`. |
| 11 | +root_cause: wrong_api |
| 12 | +resolution_type: documentation_update |
| 13 | +severity: high |
| 14 | +tags: [auth, better-auth, stripe, convex, actions] |
| 15 | +--- |
| 16 | + |
| 17 | +# Better Auth external plugin calls need Convex actions |
| 18 | + |
| 19 | +## Problem |
| 20 | + |
| 21 | +Better Auth API endpoints can run plugin hooks in addition to local database |
| 22 | +writes. When an endpoint reaches Stripe, Polar, or direct email delivery from a |
| 23 | +Convex mutation, the external SDK can call APIs such as `setTimeout` or `fetch` |
| 24 | +that Convex mutations do not allow. |
| 25 | + |
| 26 | +## Symptoms |
| 27 | + |
| 28 | +- `auth.api.updateOrganization()` works until `@better-auth/stripe` is present. |
| 29 | +- Convex throws `Can't use setTimeout in queries and mutations`. |
| 30 | +- The stack points into `stripe/esm/RequestSender.js` or another external SDK. |
| 31 | + |
| 32 | +## What Didn't Work |
| 33 | + |
| 34 | +- Treating `ctx.auth.api.*` as a safe mutation helper for every organization |
| 35 | + write. Some endpoints are DB-only in one plugin set and side-effectful in |
| 36 | + another. |
| 37 | +- Creating placeholder provider files or changing adapter write behavior. The |
| 38 | + failure happens before Convex can allow external SDK work in a mutation. |
| 39 | + |
| 40 | +## Solution |
| 41 | + |
| 42 | +Use mutations only for local Convex writes. Use actions for Better Auth |
| 43 | +endpoints that may run external plugin work. |
| 44 | + |
| 45 | +For simple organization profile updates, stay local: |
| 46 | + |
| 47 | +```ts |
| 48 | +await ctx.orm |
| 49 | + .update(organization) |
| 50 | + .set(data) |
| 51 | + .where(eq(organization.id, input.organizationId)); |
| 52 | +``` |
| 53 | + |
| 54 | +For operations that call Stripe, Polar, or direct email delivery, expose an |
| 55 | +`authAction` and bridge back into queries or mutations through generated |
| 56 | +callers when local reads or writes are needed. |
| 57 | + |
| 58 | +## Why This Works |
| 59 | + |
| 60 | +Convex mutations are deterministic database transactions. External SDKs are not |
| 61 | +allowed there. Actions are the Convex function type designed for external I/O, |
| 62 | +and generated callers keep the app code typed when action code needs local data. |
| 63 | + |
| 64 | +## Prevention |
| 65 | + |
| 66 | +- Do not document `ctx.auth.api.*` as universally mutation-safe. |
| 67 | +- Prefer `ctx.orm` for simple reads and updates. |
| 68 | +- Use `authAction` for user-facing billing, payment, portal, email, and other |
| 69 | + SDK-backed flows. |
| 70 | +- When adding auth plugin docs, explicitly classify each example as DB-only |
| 71 | + mutation work or external-I/O action work. |
| 72 | + |
| 73 | +## Related Issues |
| 74 | + |
| 75 | +- `docs/solutions/integration-issues/auth-plugin-timestamp-writes-must-respect-table-schema-20260422.md` |
| 76 | +- `docs/solutions/integration-issues/raw-convex-start-auth-adoption-must-patch-start-provider-and-react-client-20260410.md` |
0 commit comments