Supabase returns 'invite_not_found: Invite has expired or already been used' when a user attempts to accept an email invitation that is no longer valid. This occurs when the invitation token has expired (default 24-48 hours), has already been redeemed, or was manually revoked. The fix involves resending a fresh invitation, extending the token expiry, or checking for duplicate acceptances.
Supabase's email invitation system creates a time-limited, single-use token that allows users to join your organization or project. When someone clicks the invitation link, Supabase validates the token against the `auth.invitations` table (or similar internal tracking). The "invite_not_found" error means: 1. **Token expired**: Invitations typically expire after 24-48 hours for security reasons 2. **Already redeemed**: The same invitation link was used previously 3. **Revoked manually**: An admin removed the invitation before acceptance 4. **Database mismatch**: The invitation record was deleted or belongs to a different Supabase project This is a security feature preventing stale or reused invitations from granting access. The error appears during the final acceptance step, not when sending the invitation.
The simplest fix is to send a new invitation from the Supabase dashboard or via the Management API:
// Using the Supabase Management API
import { createClient } from '@supabase/supabase-js'
const supabaseAdmin = createClient(
process.env.SUPABASE_URL!,
process.env.SUPABASE_SERVICE_ROLE_KEY!
)
// Resend invitation to user email
const { data, error } = await supabaseAdmin.auth.admin.inviteUserByEmail(
'[email protected]',
{
redirectTo: 'https://yourapp.com/invite-accepted',
data: { role: 'member' } // Optional metadata
}
)
if (error) {
console.error('Failed to resend invitation:', error)
} else {
console.log('New invitation sent:', data)
}In the Supabase dashboard:
1. Go to Authentication → Users
2. Find the user or click Invite User
3. Enter the email and customize the redirect URL
4. Click Send Invitation
The new token will be valid for another 24-48 hours.
Verify the current state of invitations using the Management API:
// List all pending invitations
const { data: invitations, error } = await supabaseAdmin
.from('auth.invitations') // Internal table name may vary
.select('*')
.eq('email', '[email protected]')
.eq('status', 'pending')
if (error) {
console.error('Error checking invitations:', error)
} else if (invitations.length === 0) {
console.log('No pending invitations found')
} else {
console.log('Found invitations:', invitations)
// Check expiry
invitations.forEach(invite => {
const createdAt = new Date(invite.created_at)
const now = new Date()
const hoursDiff = (now.getTime() - createdAt.getTime()) / (1000 * 60 * 60)
if (hoursDiff > 48) {
console.log('Invitation expired ' + hoursDiff.toFixed(1) + ' hours ago')
}
})
}Note: The exact table name and schema may vary. Supabase may store invitations in an internal system table not directly queryable.
For self-hosted or enterprise setups, you can adjust the invitation token lifetime:
-- If using custom invitation tracking
UPDATE auth.invitations
SET expires_at = NOW() + INTERVAL '7 days'
WHERE email = '[email protected]'
AND status = 'pending';
-- For Supabase hosted, check project settings
-- Go to Project Settings → Authentication → Email Templates
-- Look for "Invitation validity period" or similarImportant: Extending expiry reduces security. Consider:
- Business needs vs security trade-offs
- Whether to implement manual approval workflows
- Adding MFA for invitation acceptance
Prevent users from clicking invitation links multiple times:
// Frontend: disable button after click
function handleInviteAccept(inviteToken) {
const acceptButton = document.getElementById('accept-invite')
acceptButton.disabled = true
acceptButton.textContent = 'Processing...'
// Call Supabase accept endpoint
fetch('https://' + process.env.NEXT_PUBLIC_SUPABASE_URL + '/auth/v1/invite/accept', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ token: inviteToken }),
})
.then(response => {
if (!response.ok) {
if (response.status === 400) {
// Show user-friendly message
showError('This invitation has already been used. Please contact an administrator for a new one.')
}
acceptButton.disabled = false
acceptButton.textContent = 'Accept Invitation'
}
return response.json()
})
}Also implement server-side checks to prevent race conditions.
### How Supabase invitation tokens work
Supabase generates a cryptographically signed JWT token for each invitation, containing:
- Target email address
- Inviter's user ID
- Organization/project ID
- Expiration timestamp (typically 24-48 hours)
- Single-use flag
When the user clicks the link, Supabase:
1. Validates the JWT signature
2. Checks expiration timestamp against current time
3. Verifies the token hasn't been used before
4. Creates the user account and adds them to the organization
### Security considerations
Short expiry periods (24-48 hours) are a security feature:
- Prevents stolen email links from being used later
- Limits damage from accidental sharing
- Encourages prompt acceptance
For critical systems, consider:
- Manual approval workflows instead of auto-accept
- Requiring MFA before organization join
- Audit logs for all invitation events
### Custom invitation systems
If Supabase's built-in invitations don't meet your needs, you can build a custom system:
- Store invitations in your own table with longer expiry
- Implement your own token generation/validation
- Add additional metadata (roles, permissions, etc.)
- Create admin interfaces for managing invitations
email_conflict_identity_not_deletable: Cannot delete identity because of email conflict
How to fix "Cannot delete identity because of email conflict" in Supabase
mfa_challenge_expired: MFA challenge has expired
How to fix "mfa_challenge_expired: MFA challenge has expired" in Supabase
conflict: Database conflict, usually related to concurrent requests
How to fix "database conflict usually related to concurrent requests" in Supabase
phone_exists: Phone number already exists
How to fix "phone_exists" in Supabase
StorageApiError: resource_already_exists
StorageApiError: Resource already exists