YYayaw

Organization Invitations

Passkey-first onboarding for invited organization members.

Overview

Organization invitations use Better Auth for membership state, but Yayaw owns the signed-out onboarding experience. This is necessary because Better Auth invitation acceptance requires an authenticated session whose email matches the invitation.

The invitation email links to:

/auth/accept-invitation?invitationId=<id>&token=<secret>

invitationId is the Better Auth invitation id. token is a Yayaw onboarding secret generated when the email is sent. Only the token hash is stored in invitation_onboarding_tokens; the raw token only appears in the emailed link.

Flow

Signed-out users first see a secure invitation preview backed by the onboarding token. The preferred path is passkey:

  1. Existing users can sign in with an existing passkey.
  2. New users can create an account with a passkey from the invite link.
  3. Magic link remains available and is locked to the invited email.
  4. Password signup/sign-in remains available and preserves the invitation return URL through email verification and reset flows.

Signed-in users review the invitation and explicitly accept or reject it. Better Auth verifies that the signed-in email matches the invitation recipient before acceptance. After acceptance, Better Auth sets the active organization and Yayaw marks the onboarding token consumed.

Runtime route:

  • /auth/accept-invitation

Key services:

  • src/lib/server/services/auth/invitation-onboarding.ts
  • src/lib/server/services/auth/invitation-onboarding-utils.ts
  • src/lib/server/services/email/organization-invitation-email.ts

Passkey-First Signup

Passkey-first signup uses Better Auth passkey registration with registration.requireSession: false.

Yayaw does not pass the raw invitation token into the WebAuthn context. Instead, the client asks the server for a short-lived opaque passkey context after the email token has already been validated. The passkey registration resolver then checks that:

  • the context is still valid
  • the invitation is still pending
  • the original onboarding token has not been consumed
  • no user already exists for the invited email

Existing accounts cannot add a passkey from only an invitation link. They must authenticate first, then add a passkey after accepting the invitation.

Token Storage

The raw onboarding token is never persisted. The database stores:

  • invitation id
  • token hash
  • invited email
  • consumed timestamp
  • expiry metadata

This makes leaked database rows insufficient to accept an invitation. Email delivery remains sensitive because the raw token exists in the emailed URL.

Email Behavior

Invitation emails use the transactional email pipeline when active database templates and RESEND_API_KEY are available. Missing email configuration should surface as a send failure to the operator, not as a silent membership grant.

The invite URL must remain absolute and based on NEXT_PUBLIC_BASE_URL, so production deployments need the canonical host configured before sending real invitations.

AuthZ Synchronization

Invitation acceptance creates a Better Auth member. Yayaw also syncs accepted invitation members into the internal authorization groups through the Better Auth organization hooks, so dashboard permissions are available immediately after joining.

Better Auth roles are mapped into organization-scoped Yayaw groups during organization creation, invitation acceptance, member role updates, and member removal. Do not manually insert Better Auth members without also preserving that sync path.

Security Rules

  • Only the invited email can accept the invitation.
  • Invitation preview is allowed while signed out, but membership creation still requires a verified/authenticated identity.
  • Passkey context is short-lived and derived after token validation.
  • Consumed tokens cannot be reused.
  • Existing accounts must authenticate before adding passkeys.
  • Password and magic-link fallbacks must preserve the invitation return URL.

Validation

Useful checks after invitation or auth onboarding changes:

bun test src/lib/server/services/auth/invitation-onboarding-utils.test.ts
bun run check
bunx tsc --noEmit
bun run build