|
| 1 | +--- |
| 2 | +title: Auth plugin timestamp writes must respect table schema |
| 3 | +date: 2026-04-22 |
| 4 | +category: integration-issues |
| 5 | +module: auth-adapter |
| 6 | +problem_type: integration_issue |
| 7 | +component: authentication |
| 8 | +symptoms: |
| 9 | + - Better Auth Stripe subscription upgrade fails with Convex schema validation |
| 10 | + - Convex reports extra `createdAt` or `updatedAt` on the `subscription` table |
| 11 | + - `@better-auth/stripe` writes `updatedAt` even though its subscription schema omits it |
| 12 | +root_cause: wrong_api |
| 13 | +resolution_type: code_fix |
| 14 | +severity: high |
| 15 | +tags: [auth, better-auth, stripe, subscriptions, timestamps, schema] |
| 16 | +--- |
| 17 | + |
| 18 | +# Auth plugin timestamp writes must respect table schema |
| 19 | + |
| 20 | +## Problem |
| 21 | + |
| 22 | +Stripe subscription flows can write timestamp fields that the Stripe plugin's |
| 23 | +own Better Auth schema does not define. Convex rejects those writes when the |
| 24 | +generated `subscription` table omits `createdAt` and `updatedAt`. |
| 25 | + |
| 26 | +## Symptoms |
| 27 | + |
| 28 | +- `/api/auth/subscription/upgrade` fails during Better Auth Stripe upgrade. |
| 29 | +- Convex throws an extra-field validator error for `createdAt` or `updatedAt`. |
| 30 | +- The failure happens before subscription state can be patched. |
| 31 | + |
| 32 | +## What Didn't Work |
| 33 | + |
| 34 | +- Treating `createdAt` and `updatedAt` as universal auth fields. Core Better |
| 35 | + Auth tables use them, but plugin tables do not always opt in. |
| 36 | +- Looking only at core Better Auth tables. `@better-auth/stripe` lives in a |
| 37 | + separate package and its `subscription` schema has no timestamp fields. |
| 38 | + |
| 39 | +## Solution |
| 40 | + |
| 41 | +Resolve the concrete write field set for the target model before auth writes. |
| 42 | +Use the Convex table validator when available, then fall back to the Better Auth |
| 43 | +schema. Only synthesize or preserve auth timestamp fields when that target table |
| 44 | +defines them. |
| 45 | + |
| 46 | +```ts |
| 47 | +const stripUnsupportedAuthTimestamps = ( |
| 48 | + data: Record<string, unknown>, |
| 49 | + schema: Schema, |
| 50 | + betterAuthSchema: any, |
| 51 | + model: string |
| 52 | +) => { |
| 53 | + const writeFields = resolveWriteFields(schema, betterAuthSchema, model); |
| 54 | + if (!writeFields) { |
| 55 | + return data; |
| 56 | + } |
| 57 | + |
| 58 | + let result: Record<string, unknown> | undefined; |
| 59 | + for (const field of ["createdAt", "updatedAt"] as const) { |
| 60 | + if (field in data && !writeFields.has(field)) { |
| 61 | + result ??= { ...data }; |
| 62 | + delete result[field]; |
| 63 | + } |
| 64 | + } |
| 65 | + |
| 66 | + return result ?? data; |
| 67 | +}; |
| 68 | +``` |
| 69 | + |
| 70 | +Cover both paths: |
| 71 | + |
| 72 | +- creates should not inject timestamps into plugin tables without those fields |
| 73 | +- updates should strip unsupported `updatedAt` before `ctx.db.patch()` or ORM |
| 74 | + `set()` |
| 75 | + |
| 76 | +## Why This Works |
| 77 | + |
| 78 | +The generated auth runtime is the last boundary before Convex validates the |
| 79 | +document. Filtering there fixes db and ORM writes without hiding arbitrary schema |
| 80 | +mistakes: only the known auth timestamp fields get special treatment. |
| 81 | + |
| 82 | +## Prevention |
| 83 | + |
| 84 | +- When adding auth plugin support, inspect the plugin package's schema, not only |
| 85 | + Better Auth core tables. |
| 86 | +- Add regression tests for plugin tables that intentionally omit core auth |
| 87 | + fields. |
| 88 | +- Do not assume `createdAt` and `updatedAt` are universal across auth plugin |
| 89 | + tables. |
| 90 | + |
| 91 | +## Related Issues |
| 92 | + |
| 93 | +- [Better Auth 1.6 support needs structural Convex auth wrappers](./better-auth-1-6-support-needs-structural-convex-auth-wrappers-20260416.md) |
| 94 | +- [Convex Better Auth upstream sync must filter runtime fixes from repo churn](./convex-better-auth-upstream-sync-runtime-fixes-20260416.md) |
| 95 | +- [Root auth schema sync should merge missing fragments into local tables](./root-auth-schema-sync-should-merge-missing-fragments-into-local-tables-20260328.md) |
0 commit comments