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

83 lines
2.7 KiB
Markdown

# 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.