Invitation Decline Feature - Future Enhancement¶
Date: November 6, 2025 Priority: LOW (Post-Beta) Status: Deferred until Week 2-3 of Beta
Current State¶
Invitation States: - ✅ Pending - Created, not yet accepted - ✅ Accepted - User joined team - ✅ Expired - 7 days passed - ✅ Cancelled - Owner/admin deleted invitation - ❌ Declined - User explicitly rejected invitation (NOT IMPLEMENTED)
UI: Decline button already exists in invitation acceptance page
Feature Request: Add Decline Functionality¶
User Story¶
As an invited user, I want to explicitly decline an invitation so that: 1. The inviter knows I saw the invitation 2. I stop receiving reminder emails 3. The invitation is closed properly
Implementation Plan¶
1. Database Changes¶
Add declinedAt column to team_invitations table:
Update schema.ts:
export const teamInvitations = pgTable('team_invitations', {
// ... existing columns
declinedAt: timestamp('declined_at', { withTimezone: true }),
});
2. API Endpoint¶
Create PATCH /api/invite/[token]/decline:
// src/app/api/invite/[token]/decline/route.ts
export async function PATCH(
request: NextRequest,
{ params }: { params: Promise<{ token: string }> }
) {
const { token } = await params;
// Find invitation
const [invitation] = await db
.select()
.from(teamInvitations)
.where(eq(teamInvitations.token, token));
if (!invitation) {
return Response.json({ error: 'Invitation not found' }, { status: 404 });
}
// Check if already accepted, declined, or expired
if (invitation.acceptedAt) {
return Response.json({ error: 'Invitation already accepted' }, { status: 400 });
}
if (invitation.declinedAt) {
return Response.json({ error: 'Invitation already declined' }, { status: 400 });
}
if (invitation.expiresAt < new Date()) {
return Response.json({ error: 'Invitation expired' }, { status: 410 });
}
// Mark as declined
await db
.update(teamInvitations)
.set({ declinedAt: new Date() })
.where(eq(teamInvitations.id, invitation.id));
// Get team and inviter info
const [team] = await db.select().from(teams).where(eq(teams.id, invitation.teamId));
const [inviter] = await db.select().from(users).where(eq(users.id, invitation.invitedBy));
// Send notification email to inviter
if (process.env.SPACEMAIL_SECRET && inviter) {
await sendInvitationDeclinedEmail({
to: inviter.email,
inviterName: inviter.name || inviter.email,
teamName: team.name,
declinedEmail: invitation.email,
}).catch(console.error);
}
return Response.json({
success: true,
message: 'Invitation declined'
});
}
3. Email Template¶
Create sendInvitationDeclinedEmail() in spacemail-smtp-client.ts:
export async function sendInvitationDeclinedEmail({
to,
inviterName,
teamName,
declinedEmail,
}: {
to: string;
inviterName: string;
teamName: string;
declinedEmail: string;
}) {
// Subject: "Invitation to TeamName was declined"
// Body: "John Doe (john@example.com) declined your invitation to join TeamName."
}
4. UI Updates¶
Update /invite/[token]/page.tsx:
// Add decline handler
const handleDecline = async () => {
const response = await fetch(`/api/invite/${token}/decline`, {
method: 'PATCH',
});
if (response.ok) {
// Show success message
toast.success('Invitation declined');
router.push('/');
}
};
// Update UI
<div className="flex gap-4">
<button onClick={handleAccept}>Accept Invitation</button>
<button onClick={handleDecline} variant="outline">Decline</button>
</div>
5. Analytics¶
Track decline events in PostHog:
Testing Checklist¶
- User can decline invitation via button
- Declined invitation cannot be accepted later
- Inviter receives decline notification email
- Declined invitation shows in team settings (grayed out)
- Cannot decline already accepted invitation
- Cannot decline expired invitation
- Analytics event captured correctly
Design Considerations¶
Q: Should declined users be able to re-invite?¶
A: Yes, owner/admin should be able to send a new invitation to the same email after decline.
Q: Should we show decline reason?¶
A: Optional, but could be valuable feedback:
Q: Should declined invitations be deletable?¶
A: Yes, allow owner to delete declined invitations to clean up the list.
Priority¶
Post-Beta Enhancement - Implement after: 1. Beta users provide feedback 2. We observe invitation acceptance/ignore rates 3. We validate that users actually want this feature
Estimated Effort: 2-3 hours - 30 min: Database migration + API endpoint - 1 hour: Email template + UI updates - 30 min: Testing - 30 min: Analytics integration
Alternative: Soft Decline (Lower Priority)¶
Instead of explicit decline, track "invitation viewed" events:
- When user opens /invite/[token], log view timestamp
- Send reminder after 3 days if viewed but not accepted
- Auto-cancel after 7 days
This is less intrusive and requires no user action.
Document Created: November 6, 2025 Status: Deferred to Post-Beta Review Date: Beta Week 2-3 (Nov 18-25)