Skip to main content
← Back to list
01Issue
BugOpenSwamp ClubPublic
AssigneesNone

Relationships

#1810 Collective invitations expire after 48h and the auto-claim is gated on that window, so a late signup is never bound

Opened by keeb · 8/24/2026

Found by code scan of the collective invite and claim paths.

The defect

invitationExpiresIn is never set in the Better Auth organization plugin config in lib/auth.ts, so the plugin default applies: 3600 * 48 seconds, i.e. 48 hours. Every collective invitation dies two days after it is sent.

The auto-claim that binds an invitee to the collective when they establish a session filters on that same window — lib/app/claim-pending-invitations.ts:147:

const invites = await db.collection("invitation").find({
  email,
  status: "pending",
  expiresAt: { $gt: new Date() },
}).toArray();

The claim hook itself is wired correctly: lib/auth.ts:583 fires it on every session-establishing path, not just signup, so an operative who already had an account gets claimed on their next sign-in. But past the 48-hour mark the query returns nothing, forever. There is no re-send, no grace period, and no path back.

So the sequence that loses a member is entirely ordinary:

  1. Owner invites a colleague by email.
  2. Colleague does not act on it within two days — busy week, mail buried, waiting until they had time to look properly.
  3. Colleague signs up (or signs in) on day three with the exact invited address.
  4. Nothing binds. They land in the product with no collective.

The invitation row does not reflect any of this

The document is left at status: "pending" indefinitely. Nothing sweeps it to expired. So the stored state says the invitation is outstanding while every read path treats it as gone — see the sibling filters at lib/infrastructure/better-auth-collective-queries.ts:599 and :650, and #1653.

Suggested fix

  1. Set invitationExpiresIn explicitly rather than inheriting the default. Two days is short for something delivered by email to a work address; a week or a fortnight is more honest about how people read mail.
  2. Decouple the claim from the expiry. An invitation that has expired but was never declined still represents a real intent by the owner. Either let the claim bind on status: "pending" regardless of expiresAt, or transition expired invitations to a distinct state that the owner can re-send from with one click.
  3. Whichever route, move the row off pending when the window closes, so stored state and read behaviour agree.

Related: #1653 (nobody is told when an invite mis-lands), #1186 and #1287 (earlier claim-path failures, both shipped).

02Bog Flow
OPENTRIAGEDIN PROGRESSSHIPPED

Open

8/24/2026, 11:57:02 PM

No activity in this phase yet.

03Sludge Pulse

Sign in to post a ripple.