Skip to content

[AK-585] Email-based invitations for organizations and workspaces #76

Description

@robinbraemer

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

  1. User invited → row in organizationInvitations + email sent with a link to CNAP
  2. User signs up or logs in at CNAP
  3. On successful auth, check organizationInvitations where email === user.verifiedEmail
  4. For each pending invite: auto-add user to org with the specified role, delete the invitation row
  5. 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

  • organizationInvitations table with proper indexes
  • Invitation email sent via existing email.controller.ts pattern
  • On login, verified email matches pending invite → auto-join
  • UI shows email input, not user ID
  • Pending invitations shown in member list with resend/revoke
  • Expired invitations purged by cron

Out of Scope

  • Batch invite UI → CNAP-584
  • Workspace-scope invitations (same system, follow-up)
  • Seat limits → CNAP-585

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions