Files
teatea-pension/.trellis/tasks/07-03-invite-link-limits/design.md

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 1
  • used_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?: string
  • roleId: string
  • validityDays?: number
  • maxUses?: number

Defaults:

  • validityDays = 7
  • maxUses = 1

Validation:

  • roleId is still required.
  • validityDays must be a positive integer in an operationally reasonable range.
  • maxUses must 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 >= now
  • usedCount < maxUses

On successful invitation registration:

  • Increment usedCount.
  • If the increment reaches maxUses, set status = "accepted" and fill acceptedByAccountId / acceptedAt with 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.