2.7 KiB
Invite Link Limits Design
Architecture
This change spans the persisted invitation model, the invitation create API, the registration consumption transaction, and the organization settings UI.
The invitation row remains the source of truth. The existing expires_at column continues to represent validity. New persisted counters will represent usage limits:
max_uses: maximum successful invitation registrations allowed.used_count: number of successful registrations that have consumed the invitation.
Data Model
Add non-null integer columns to organization_invitations:
max_uses integer not null default 1used_count integer not null default 0
Existing invitation rows remain compatible: previous one-time invites become max_uses = 1, used_count = 0 unless already accepted. The existing status = accepted already prevents use of accepted legacy rows.
Invitation Creation Contract
POST /api/organizations/[id]/invitations accepts:
email?: stringroleId: stringvalidityDays?: numbermaxUses?: number
Defaults:
validityDays = 7maxUses = 1
Validation:
roleIdis still required.validityDaysmust be a positive integer in an operationally reasonable range.maxUsesmust be a positive integer in an operationally reasonable range.- Invalid input returns
jsonFailure(...)with the existing structured API shape.
Invitation Consumption
Registration only counts a use after account creation and membership insertion succeed inside the existing transaction.
An invitation is valid only if:
status === "active"expiresAt >= nowusedCount < maxUses
On successful invitation registration:
- Increment
usedCount. - If the increment reaches
maxUses, setstatus = "accepted"and fillacceptedByAccountId/acceptedAtwith the consuming account details. - If remaining uses exist, keep
status = "active"and leave accepted metadata empty.
The update should guard against concurrent over-consumption by updating with a used_count < max_uses predicate and checking that a row was returned.
UI
OrganizationInviteDialog adds controls for:
- Validity days, default 7.
- Maximum uses, default 1.
OrganizationDetailClient displays:
- Expiry time.
- Usage as
usedCount / maxUses.
Copy behavior remains unchanged.
Compatibility
The existing /register?invite=... URL format remains unchanged.
Existing rows without the new columns are migrated through default values. Existing one-time semantics are preserved by default.
Rollback
If needed, UI inputs can be hidden and backend defaults can keep the old behavior. Dropping the columns would require a reverse migration only if deployed migrations must be rolled back.