Better Enrollment

Database

The tables the plugin creates, every column, and how they are indexed.

Better Enrollment adds two tables, invite and inviteUse, and extends the organization plugin's organization table when org features are enabled. Run npx auth generate (or npx @better-auth/cli generate on Better Auth 1.6 and earlier) after installing to add them to your schema.

invite

One row per invitation. Status is only ever pending, accepted, or cancelled; expired is derived from expiresAt at read time, so there is no cron and nothing to sweep.

Tableinvite
Field
Type
Key
Description
id
string
PK
Unique identifier for each invite
type
string
-
Delivery type: "private" (email-bound, single use) or "public" (shareable link)
kind
string
-
What redeeming grants: "app", "org-join", or "org-create". Defaults to "app"
email ?
string
-
Address a private invite is bound to; null for public invites. Indexed
name ?
string
-
Optional display name for the invitee
role
string
-
Role merged into the user on redemption
tokenHash
string
UQ
Hash of the invite token; the raw token is never stored
status
string
-
"pending", "accepted", or "cancelled". "expired" is derived from expiresAt and never stored. Indexed
mode
string
-
Mode the invite was created for: "invite-only" or "open"
organizationId ?
string
-
Organization an org-join invite adds the invitee to. Plain column, no FK: accepted invites survive org deletion as audit records. Indexed
organizationRole ?
string
-
Role granted inside the organization on join
teamId ?
string
-
Team the invitee is added to, when the org plugin uses teams
presetSeatLimit ?
number
-
Seat limit applied to the organization founded through an org-create invite
presetOrganizationName ?
string
-
Name of the organization an org-create invite founds, fixed by the inviter. Null lets the founder choose
presetOrganizationSlug ?
string
-
Slug for that organization; derived from the name when null, and suffixed when taken at redemption
preCreatedUserId ?
string
-
Shell user pre-created for a private invite; claimed by the invitee on accept
createdByUserId ?
string
-
User who created the invite. Plain column, no FK: the record outlives the inviter. Indexed
inviterName
string
-
Denormalized inviter name, kept for display and audit
inviterEmail
string
-
Denormalized inviter email, kept for display and audit
expiresAt ?
date
-
Expiry timestamp; null means the invite never expires. Indexed
maxUses ?
number
-
Redemption cap for public invites; null means unlimited
useCount
number
-
Successful redemptions so far. Defaults to 0
revokedAt ?
date
-
When the invite was revoked
revokedByUserId ?
string
-
User who revoked the invite
createdAt
date
-
When the invite was created
updatedAt
date
-
When the invite was last updated

inviteUse

Append-only audit table: one row per redemption. Rows are never updated, and they cascade only when their invite is deleted.

TableinviteUse
Field
Type
Key
Description
id
string
PK
Unique identifier for each redemption
inviteId
string
FK
The invite that was redeemed. Indexed References invite.id.
usedByUserId
string
-
User who redeemed the invite
inviteeEmail
string
-
Email of the invitee at redemption time
usedAt
date
-
When the invite was redeemed

organization (extension)

Columns added to the organization plugin's table, and only when org features are configured, so apps without the org plugin never get stray columns.

Tableorganization
Field
Type
Key
Description
seatLimit ?
number
-
Maximum member count for the organization; enforced on joins
disabledAt ?
date
-
When the organization was disabled by a platform control

user (extension)

Each configured additional field becomes a column on the user table, named after the field and typed by its type. Columns are always nullable: pre-created and open-mode users exist before redemption collects a value, so requiredness is enforced by the redeem flow rather than the database.

Indexes

The schema indexes email, organizationId, tokenHash (unique), status, expiresAt, and createdByUserId. If your invite table grows into the millions, consider adding composite indexes (email, status) and (organizationId, status) in your own migrations. Better Auth 1.7 can declare multi-column indexes in a plugin schema, but the plugin keeps to single-column ones so the same schema still works on 1.4 to 1.6.

Schema validation on Better Auth 1.7

From Better Auth 1.7, the auth instance checks your database schema against what Better Auth and its plugins expect when it starts. Auth requests wait for that check and fail if it finds a mismatch, so a missing invite column now fails every auth request instead of only the invite flows. A mismatch is remembered and returned on every later request until Better Auth's own migration runs or the process restarts.

  • Kysely (the built-in adapter): the live database is inspected. Run npx auth migrate after upgrading the plugin.
  • Prisma and Drizzle: your local schema metadata is compared, without a database connection. For Prisma that means the generated client, so after npx auth generate, also run prisma generate and apply the migration before you deploy.

The check is on by default. advanced.database.validateSchema: false turns it off, but treat that as a temporary escape hatch: the mismatch it hides is still there.

Prisma example

A complete Prisma schema example lives in examples/prisma.

Last updated on

On this page