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

Relationships

#1653 Tell the inviter when a collective invite mis-lands, instead of leaving it pending forever

Opened by keeb · 8/14/2026

Problem

A pending collective invitation looks identical whether nobody has opened it yet or the invitee opened it, signed up, and ended up on an account the invitation can never match.

That second case is real and is the root of #1474: an invitee signing up with an OAuth provider gets whatever address the provider considers canonical. For a Google Workspace account invited at a plus-addressed alias, that is the base address — a different operative as far as the invitation is concerned, since implicit account linking is disabled. The invite silently stays pending.

The fix that just shipped for #1474 and #1204 makes this legible to the INVITEE: the post-auth destination now survives the OAuth round trip, so they land back on /o/SLUG/accept-invite and see the WRONG_ACCOUNT state naming the mismatch. But it does nothing for the collective OWNER, who is the person who noticed and reported the original problem. They still see one undifferentiated row in the pending-invites list.

Proposed solution

Surface the state on the collective settings pending-invites list (islands/PendingInvites.tsx). Something that distinguishes:

  • never opened
  • opened, but the viewer signed up or signed in as a different address

and offers the one-click remedy for the second case: re-invite the address they actually landed on.

The signal already exists at the moment of failure — accept-invite.tsx resolves the invitation, compares the signed-in address, and renders WRONG_ACCOUNT. Nothing records that it happened. Stamping the invitation at that point is probably the cheapest version of this.

Notes

  • Deliberately excluded from the #1474 / #1348 / #1204 PR to keep that change to the correctness fixes; it is a separable deliverable on a different surface.
  • Do NOT solve this by loosening the address match. Treating a provider-canonicalized address as equal to a subaddressed invitation grants collective membership on an inference about a third party mail configuration — see the reasoning recorded in CLAUDE.md and the #1474 ripple.
  • Whatever gets stamped must not leak the invited address to a non-recipient: accept-invite.tsx is careful to show only the viewer own email today, and the owner-side view should stay symmetric with that.
02Bog Flow
◉OPEN○TRIAGED○IN PROGRESS○SHIPPED

Open

8/14/2026, 7:25:46 PM

No activity in this phase yet.

03Sludge Pulse
Editable. Press Enter to edit.

keeb commented 8/24/2026, 11:58:26 PM

Code-scan detail on the mechanism, from a sweep of the invite paths.

The disappearance is not just a missing notification — the invitation is filtered out of every read surface the moment it expires, on both sides, while the stored row still says pending.

Three filters, all the same predicate:

  • lib/infrastructure/better-auth-collective-queries.ts:599 — listPendingInvitationsByEmail, the invitee-facing list, expiresAt: { $gt: new Date() }
  • lib/infrastructure/better-auth-collective-queries.ts:650 — listPendingInvitationsByCollective, the inviter-facing roster on the collective settings page, same predicate
  • lib/app/claim-pending-invitations.ts:150 — the auto-claim, same predicate

So past the expiry window the invitation is invisible to the invitee, invisible to the owner who sent it, and unclaimable — but no write ever moves it off pending, and nothing sweeps it. Stored state and read behaviour disagree permanently.

The owner-side effect is the one this issue is about: the pending row does not sit there looking stale, it silently drops off the roster. From the owner point of view the invitation is neither outstanding nor failed. It just stops existing, with no event and no message.

Two things would close the gap:

  1. Transition the row to an explicit terminal state when the window closes, rather than leaving pending rows that every reader treats as absent. That gives this issue something concrete to render and to notify on.
  2. Keep expired invitations on the owner roster in a distinct state with a re-send action, instead of filtering them out at :650.

The 48-hour window itself and the claim gating are filed separately as #1810.

Sign in to post a ripple.