Space members & invitation system
How space owners can manage their space members.
Overview
The space members and invitation system allows space owners to manage their space members and invite new users to join their space. This document outlines the technical implementation and workflows of this system.
System workflow
The following diagram illustrates the complete invitation flow from creation to space membership:
The diagram shows:
- Green: Initial invite creation process
- Orange: Core invite processing
- Blue: Final state updates
- Dotted lines: File references for key operations
Core components
Space members management
The space members management interface is implemented in two main components:
app/[locale]/space/settings/space-members/page.tsx: Server component that handles data prefetchingapp/[locale]/space/settings/space-members/space-members-client.tsx: Client component that renders the UI
The management interface allows space owners to:
- View all current space members
- Add new members via email invitation
- Remove existing members
- View member roles and permissions
// Example: Space members client component structure
export function SpaceMembersClient() {
// State for managing the add member dialog
const [isAddMemberDialogOpen, setIsAddMemberDialogOpen] = useState(false);
// Hooks for fetching space members data
const { spaceMembers, isLoadingSpaceMembers } = useSpaceMembers();
const currentUserMembership = useCurrentUserMembership();
const isOwner = currentUserMembership?.role === SpaceMemberRole.Owner;
return (
<div>
{/* Add member button (only visible to owners) */}
{isOwner && <Button>Add Member</Button>}
{/* Members table */}
<SpaceMembersTable spaceMembers={spaceMembers} isOwner={isOwner} />
{/* Add member dialog */}
<AddMemberDialog />
</div>
);
}Invitation workflow
The invitation system follows a multi-step workflow:
1. Invitation creation
Files involved:
-
app/[locale]/space/settings/space-members/components/add-member-dialog.tsx// Handles the UI for sending invites export function AddMemberDialog({ onInviteSent }) { const form = useForm<InviteFormData>(); // Form for collecting invitee email and role } -
app/lib/queries/invite.query.ts// Creates the invite in the database export async function createInvite({ email, spaceId, role, }: CreateInviteParams): Promise<ApiResponse<Invite>> { // Generates invite code // Creates database record // Triggers email sending } -
app/lib/actions/emails/send-invite-email.ts// Sends the invitation email export async function sendInviteEmail( inviteeEmail: string, inviteCode: string, inviterName: string ) { // Formats and sends email with invite link }
2. Invitation acceptance
Files involved:
-
app/api/accept-invite/route.tsexport async function GET(request: NextRequest) { const inviteId = url.searchParams.get("invite_id"); // Validates invite via Supabase RPC const { error } = await supabase.rpc("accept_invite", { invite_id: inviteId, }); // Redirects to signup/login with invite context return NextResponse.redirect( new URL(`/signup?invite_id=${inviteId}`, request.url) ); } -
database/functions/accept_invite.sql-- Supabase stored procedure create or replace function accept_invite(invite_id uuid) returns void as $$ begin -- Validates invite status -- Updates invite status to accepted -- Records acceptance timestamp end; $$ language plpgsql;
3. User authentication
Files involved:
-
app/[locale]/(auth)/signup/page.tsxexport default async function SignUp({ searchParams }) { const inviteId = searchParams.invite_id; // Fetches invite details if present const invite = inviteId ? await getInvite(inviteId) : null; return <SignUpForm invite={invite} />; } -
app/[locale]/(auth)/signup/components/sign-up-form.tsxexport function SignUpForm({ invite, inviter }) { // Pre-fills form with invite data const form = useForm({ defaultValues: { email: invite?.email || "", inviteCode: invite?.code || "", }, }); // Handles form submission with invite context const onSubmit = async (data) => { // Creates user account // Associates with space if invite present }; } -
app/[locale]/(auth)/login/components/sign-in-form.tsxexport function SignInForm() { const { invite_id } = useSearchParams(); // Handles login with invite context const handleSubmit = async (data) => { // Authenticates user // Processes pending invite if present if (invite_id) { await processInviteAfterAuth(invite_id); } }; }
4. Space association (final Step)
Files involved:
app/lib/queries/space.query.ts// Associates user with space after successful auth export async function addSpaceMember({ userId, spaceId, role, }: AddSpaceMemberParams): Promise<ApiResponse<null>> { // Creates space_member record // Sets up initial permissions // Sends welcome notification }
Each step includes validation, error handling, and appropriate user feedback. The system maintains data consistency through database transactions and handles edge cases like:
- Expired invites
- Already accepted invites
- Invalid invite codes
- Concurrent invite processing
Database operations
The system uses several key database queries (implemented in space.query.ts):
- Get space members
export async function getSpaceMembers(
spaceId: string
): Promise<ApiResponse<SpaceMember[]>> {
// Fetches all members of a space with their roles
}- Remove space member
export async function removeSpaceMember(
spaceGroupId: string,
userId: string,
spaceOwnerUserId?: string
): Promise<ApiResponse<null>> {
// Removes member and handles cleanup
}Authentication flow with invites
The authentication system (sign-in-form.tsx and sign-up-form.tsx) handles invite-specific logic:
-
Sign up with invite
- Form pre-fills email if provided in invite
- Validates invite code
- Creates user account
- Associates user with space upon successful signup
-
Sign in with invite
- Checks for invite context
- After successful login, processes pending invite
- Adds user to space
// Example: Sign up form with invite handling
const form = useForm<z.infer<typeof signUpFormSchema>>({
defaultValues: {
email: invitee?.email || "",
inviteCode: invite?.inviteCode || searchParams.get("inviteCode") || "",
},
});Security considerations
-
Invite validation
- Invites are single-use only
- Invites expire after a configured time period
- Invite codes are cryptographically secure
-
Permission checks
- Only space owners can send invites
- Users can't accept invites for spaces they're already members of
- Role-based access control for member management
Error handling
The system implements comprehensive error handling:
-
Invalid invites
- Shows clear error messages for expired/used invites
- Redirects to appropriate error pages
-
Rate limiting
- Prevents spam by limiting invite creation
- Implements cooldown periods for failed attempts
-
Edge cases
- Handles concurrent invite acceptances
- Manages race conditions in member addition/removal
Related components
AddMemberDialog: UI for sending invitesSpaceMembersTable: Displays and manages membersInviteAcceptance: Handles invite processing- Email templates for invitations