Skip to main content
Buildstory’s team system lets project owners invite collaborators through direct invites (by username) or shareable link invites. Team members can work together on projects during hackathons or standalone builds.

Team Structure

Every project has:
  • Owner - The profile that created the project (stored in projects.profileId)
  • Members - Additional collaborators (stored in projectMembers table)
  • Pending invites - Outstanding invitations (stored in teamInvites table)

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:
1

Search for user

Type username or display name in the invite search box
2

Select from results

Real-time search shows up to 5 matching users (filters out existing members, banned/hidden profiles)
3

Send invite

Click to send — creates teamInvites row with type: "direct" and recipientId
4

Recipient responds

User sees invite in notification bell popover, can accept or decline

Direct Invite Workflow

// Server action: sendDirectInvite
1. Verify project ownership
2. Lookup recipient by username
3. Check recipient.allowInvites (privacy setting)
4. Check not already a member or pending invite
5. Rate limit: max 5 pending invites per sender
6. Insert teamInvites row with status "pending"
7. Revalidate project page
Direct invites check allowInvites on the recipient’s profile. If disabled, the invite fails with “This user is not accepting invites.”
Project owners can generate shareable invite links for broader recruitment:
1

Generate link

Click “Generate invite link” button — creates invite with random UUID token
2

Copy and share

Link format: buildstory.com/invite/{token}
3

Anyone with link can join

First user to click the link and accept claims the invite
4

Link consumed or revoked

Invite status changes to “accepted” or “revoked”, link becomes invalid
// Server action: generateInviteLink
1. Verify project ownership
2. Rate limit: max 5 pending invites per sender
3. Generate crypto.randomUUID() token
4. Insert teamInvites with type "link", recipientId null
5. Return token to client
6. Client builds full URL and displays copy button
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)

// Server action: respondToInvite
1. Atomically update invite status to "accepted" or "declined"
2. If accepted, insert projectMembers row with inviteId reference
3. Handle race condition with unique constraint on (projectId, profileId)
// Server action: acceptInviteLink
1. Read invite by token (type "link", status "pending")
2. Check not project owner
3. Check not already a member
4. Atomically claim: set status "accepted", set recipientId to current user
5. Insert projectMembers row
6. Return project slug for redirect
Auth-gated invite links redirect unauthenticated users to /sign-in with a redirect back to the invite URL after login.

Privacy Controls

Allow Invites Toggle

Users control invite visibility via profiles.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
1

Navigate to Settings

Open the Settings page from the app sidebar
2

Find Privacy section

Scroll to the “Privacy” section
3

Toggle Allow team invites

Switch on/off to control discoverability
4

Save changes

Click “Save changes” to update profile

Search Filtering

The searchUsersForInvite 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 NULL and hiddenAt IS NULL (visible profiles)
  • Not already a project member or owner
Returns up to 5 results for autocomplete dropdown.

Rate Limiting

Project owners are limited to 5 pending invites at any time:
  • Counts all invites with senderId = profileId and status = 'pending'
  • Applies to both direct and link invites
  • Enforced in sendDirectInvite and generateInviteLink actions
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:
// Server action: removeTeamMember
1. Verify project ownership
2. Delete projectMembers row where (projectId, profileId)
3. Revalidate project page

Leaving a Team

Members (non-owners) can leave voluntarily:
// Server action: leaveProject
1. Check user is not the owner
2. Delete projectMembers row where (projectId, profileId)
3. Revalidate project page
Owners cannot leave their own projects — they must delete the project instead.

Revoking Invites

Project owners can revoke pending invites before they’re accepted:
// Server action: revokeInvite
1. Verify sender is current user
2. Check invite status is "pending"
3. Update status to "revoked"
4. Revalidate project page
Revoked invites:
  • 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

The TeamSection 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 InviteUserSearch component
  • Generates shareable links with copy-to-clipboard
  • Lists pending invites with revoke actions
  • Remove member / Leave team buttons
All mutations use server actions with optimistic UI updates via useTransition and router.refresh().

Database Schema

teamInvites Table

{
  id: uuid,
  projectId: uuid (FK to projects),
  senderId: uuid (FK to profiles),
  recipientId: uuid | null (FK to profiles, null for link invites),
  type: "direct" | "link",
  status: "pending" | "accepted" | "declined" | "revoked",
  token: string | null (unique, used for link invites),
  createdAt: timestamp
}
Indexes:
  • (recipientId, type, status) - Fast lookup of user’s pending invites
  • (senderId, status) - Count pending invites for rate limiting
  • (projectId, status) - Project detail page queries

projectMembers Table

{
  id: uuid,
  projectId: uuid (FK to projects),
  profileId: uuid (FK to profiles),
  inviteId: uuid | null (FK to teamInvites),
  joinedAt: timestamp,
  UNIQUE (projectId, profileId)
}
The unique constraint prevents duplicate memberships and is used for race condition handling in invite acceptance.

Build docs developers (and LLMs) love