Team Structure
Every project has:- Owner - The profile that created the project (stored in
projects.profileId) - Members - Additional collaborators (stored in
projectMemberstable) - Pending invites - Outstanding invitations (stored in
teamInvitestable)
Owner privileges
Edit project, invite/remove members, delete project
Member privileges
View project details, leave team voluntarily
Direct Invites
Project owners can send invites directly to specific users by username:Select from results
Real-time search shows up to 5 matching users (filters out existing members, banned/hidden profiles)
Direct Invite Workflow
Direct invites check
allowInvites on the recipient’s profile. If disabled, the invite fails with “This user is not accepting invites.”Link Invites
Project owners can generate shareable invite links for broader recruitment:Link Invite Workflow
Link invites use atomic updates to prevent race conditions — only one user can claim each link via
where: status = 'pending' check.Invite Acceptance
Direct Invite (via notification bell)
Link Invite (via /invite/ page)
Privacy Controls
Allow Invites Toggle
Users control invite visibility viaprofiles.allowInvites (default: true):
- Enabled - Appear in project owner search, receive direct invites
- Disabled - Hidden from search, direct invites fail, can still accept link invites if shared directly
Search Filtering
ThesearchUsersForInvite action filters candidates by:
- Username or display name matches search query (case-insensitive ILIKE)
username IS NOT NULL(completed onboarding)allowInvites = true(accepting invites)bannedAt IS NULLandhiddenAt IS NULL(visible profiles)- Not already a project member or owner
Rate Limiting
Project owners are limited to 5 pending invites at any time:- Counts all invites with
senderId = profileIdandstatus = 'pending' - Applies to both direct and link invites
- Enforced in
sendDirectInviteandgenerateInviteLinkactions
Once an invite is accepted, declined, or revoked, it no longer counts toward the limit, allowing the owner to send more invites.
Managing Team Members
Viewing Team
Project detail pages (/projects/{slug}) display:
- Owner - Profile card with “Owner” badge
- Members - Profile cards for all accepted team members
- Pending invites (owner only) - List of outgoing invites with revoke buttons
- Invite UI (owner only) - Search box and link generator
Removing Members
Project owners can remove any team member:Leaving a Team
Members (non-owners) can leave voluntarily:Revoking Invites
Project owners can revoke pending invites before they’re accepted:- No longer appear in notification bell
- Link invites return “invalid or expired” error
- No longer count toward rate limit
Notification Bell
Authenticated users see a notification bell in the app topbar:- Badge - Shows count of pending direct invites
- Popover - Lists all pending invites with project name and sender
- Actions - Accept or Decline buttons inline
Real-time count
Query
getPendingInvitesForUser fetches invites with recipientId = profileId and status = 'pending'Optimistic updates
Accept/decline triggers router.refresh() to update UI immediately
Team Section Component
TheTeamSection client component (components/projects/team-section.tsx) handles all team UI:
- Displays owner and members with avatars and usernames
- Shows invite search (owner only) via
InviteUserSearchcomponent - Generates shareable links with copy-to-clipboard
- Lists pending invites with revoke actions
- Remove member / Leave team buttons
useTransition and router.refresh().
Database Schema
teamInvites Table
(recipientId, type, status)- Fast lookup of user’s pending invites(senderId, status)- Count pending invites for rate limiting(projectId, status)- Project detail page queries