Team & Multi-Org
Team invites
Dashboard: /overview/settings/team. Internal API: /api/internal/team/*.
Invite flow
- Owner submits email + role at
POST /api/internal/team/invite. - Server:
- Rejects if target already a member (409).
- Upserts a
team_invitesrow with a 48-char nanoid token, 7-day expiry. - Emails the invitee via the app's own
sendEmail()usingSYSTEM_FROM_EMAIL(or fallback). - Writes audit log entry (
member.invited).
- Invitee lands on
/invite?token=.... POST /api/internal/team/accept { token, name?, password? }:- If existing user by email — add as
org_member. - Else create user (name + password required) with
email_verified: now()and add as member.
- If existing user by email — add as
- Invite marked
accepted_at. Auditmember.joined.
Schema
team_invites (src/lib/db/schema/team-invites.ts):
| Column | Notes |
|---|---|
token |
PK — the link value |
org_id |
scope |
email |
invited address |
role |
member / owner |
invited_by |
user id |
expires_at |
7d default |
accepted_at |
nullable |
Unique (org_id, email) prevents duplicate invites; re-inviting rotates the token.
Multi-org membership
A user can belong to multiple orgs. The active org is tracked via a cookie (sendoka_active_org) rather than JWT — no re-login required to switch.
Resolution
getActiveOrgId(userId, fallbackOrgId) in src/lib/auth/active-org.ts:
- Read
sendoka_active_orgcookie. - Verify the user has a matching
org_membersrow. - Use it; else fall back to the session's default org (first membership at sign-in time).
Endpoints
GET /api/internal/org— list the user's orgs + current active.POST /api/internal/org/switch { org_id }— verify membership, set cookie, return success. Client should refresh.
UI
src/components/overview/org-switcher.tsx — dropdown in sidebar; only shows when user has 2+ memberships.
Caveats
- Dashboard pages and most internal APIs still use
session.user.orgIddirectly — they see the sign-in-time default, not the cookie-overridden value. Migrate them togetActiveOrgId()as a follow-up (see open gaps). - Members can't be removed yet — only added via invite.
- No role-based permissions beyond
owner/member— both roles currently have the same privileges.