Migrated from Linear: AK-585
GitHub is the canonical source of truth. The Linear issue is archived.
Why
Adding organization members currently requires knowing their BetterAuth user ID — exposed in the UI, impractical for real invitations. We need email-based invites that send a link and auto-join on accept.
Description
Add organizationInvitations table and replace the current "add by user ID" flow with email invitations.
Data model
organizationInvitations:
organizationId: Id<organizations>
email: string
role: 'owner' | 'admin' | 'member'
invitedBy: UserId
expiresAt: number // epoch ms, e.g., 7 days
// NO token field — email verification is the auth mechanism
Index: by_email, by_organization_id, by_organization_id_email.
Accept flow
- User invited → row in
organizationInvitations + email sent with a link to CNAP
- User signs up or logs in at CNAP
- On successful auth, check
organizationInvitations where email === user.verifiedEmail
- For each pending invite: auto-add user to org with the specified role, delete the invitation row
- Expired invitations are purged by a cron
Email template
New Svelte component OrganizationInvitation.svelte matching the existing MagicLinkEmail.svelte style. Register in email.controller.ts.
API changes
Replace POST /v1/organizations/:id/members body from { user_id, role } to { email, role }. Same for workspace members when that lands.
UI changes
- Replace "User ID" input in Add Member sheet with "Email" input (form() + SvelteKit's native form validation with Zod)
- Show pending invitations in the members list with a "Pending" badge
- Resend / revoke actions on pending invitations
Acceptance Criteria
Out of Scope
- Batch invite UI → CNAP-584
- Workspace-scope invitations (same system, follow-up)
- Seat limits → CNAP-585
Why
Adding organization members currently requires knowing their BetterAuth user ID — exposed in the UI, impractical for real invitations. We need email-based invites that send a link and auto-join on accept.
Description
Add
organizationInvitationstable and replace the current "add by user ID" flow with email invitations.Data model
Index:
by_email,by_organization_id,by_organization_id_email.Accept flow
organizationInvitations+ email sent with a link to CNAPorganizationInvitationswhereemail === user.verifiedEmailEmail template
New Svelte component
OrganizationInvitation.sveltematching the existingMagicLinkEmail.sveltestyle. Register inemail.controller.ts.API changes
Replace
POST /v1/organizations/:id/membersbody from{ user_id, role }to{ email, role }. Same for workspace members when that lands.UI changes
Acceptance Criteria
organizationInvitationstable with proper indexesemail.controller.tspatternOut of Scope