get-convex / get-convex/better-auth
Honor BA core's `advanced.generateId({ model })` for branded IDs on component tables
- Dominant language
- TypeScript
- Stars
- 764
- Forks
- 126
- PR merge metrics
- No merged PRs in 30d
Description
Better Auth core exposes `options.advanced.generateId({ model, size })` as the hook for application-supplied primary IDs. On the Convex adapter that hook is effectively a no-op, which makes it difficult to use Stripe/Linear style prefixed identifiers (`ws_*` on organization, `mbr_*` on member, `inv_*` on invitation, `sub_*` on subscription) without standing up parallel app-side tables per model.
The Convex adapter sets `disableIdGeneration: true`(`src/client/adapter.ts:186`) because Convex's `_id` is platform-generated and immutable, so BA's id pipeline has to stay off on this adapter. The downstream side effect is that the `customIdGenerator` default-value branch in `@better-auth/core/db/adapter/get-id-field.mjs` (lines 13–22) is also skipped: when `disableIdGeneration` is true, `shouldGenerateId` is false, the `defaultValue` block that would route through `options.advanced.generateId` is never attached and the hook doesn't fire.
The current workaround is to keep a parallel app-side table **per BA model** with a `publicId` column and a `by_publicId` index, plus a resolver at every REST/CLI input boundary.
It works but duplicates a contract BA core already defines, and it has to be repeated per model the application wants to brand.
### Proposal
An opt-in, per-model column. When enabled, the adapter generates the BA component table with an extra `publicId: v.string()` field and a `by_publicId` index. On `create`, the adapter calls `options.advanced.generateId({ model })` and writes the result to `publicId`. Convex `_id` remains platform owned; the BA `id` ↔ Convex `_id` mapping is unchanged.
```ts
export const createAuth = createClient({
components: {
organization: { publicId: true },
member: { publicId: true },
invitation: { publicId: true },
subscription: { publicId: true },
},
advanced: {
generateId: ({ model }) => {
const prefix = {
organization: "ws",
member: "mbr",
invitation: "inv",
subscription: "sub",
}[model];
return prefix ? `${prefix}_${nanoid(14)}` : undefined;
},
},
});
```
The `publicId` is a secondary, externally-facing identifier. It is never used as a foreign key — those continue to reference `_id` / BA `id`. The shape mirrors the pattern apps already use on their own tables, applied to the BA-owned ones.
### Scope
This proposal does not change:
- Convex `_id` — still auto-generated, immutable, the platform primary key.
- The BA `id` ↔ Convex `_id` mapping (`mapKeysTransformInput/Output`).
- `forceAllowId` / `customIdGenerator` plumbing in BA core.
- Internal foreign keys such as `member.organizationId` and `subscription.referenceId`,...those continue to reference BA `id` /Convex `_id`.
### Current workaround
Parallel `workspaceMetadata` table keyed on `organization._id`, with `publicId` + `by_publicId` index, a resolver at the REST entry, a brand-typed projection helper, and an ESLint allowlist to enforce the boundary. Roughly 150 lines of glue that would no longer be necessary if the adapter populated `publicId` directly.
Happy to open a PR if the direction is welcome but won't if it's a non-starter or I am missing something critical. Thanks for reading. 🙏
Contributor guide
Research direction
Start by reading src/client/adapter.ts around line 186, then inspect the Better Auth ID-generation path at @better-auth/core/db/adapter/get-id-field.mjs lines 13–22 and the adapter’s component-table creation and mapping paths. Confirm how an opt-in per-model publicId field and by_publicId index would interact with creation and existing foreign-key mappings. Done means the proposal is implemented without changing Convex _id, BA id mappings, or internal foreign keys, with coverage for enabled and disabled models.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- typescript
- Domain
- authentication, databases
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 48/100