fix(invitation): auto-delete expired invitations on a schedule#1744
fix(invitation): auto-delete expired invitations on a schedule#1744whoAbhishekSah wants to merge 3 commits into
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Warning Review limit reached
Next review available in: 28 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (6)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
6ff8cc9 to
f9286aa
Compare
Nothing removed an invitation once it expired. Trying to accept an expired invite just fails with an error, and creating a new invite for the same user writes over the old one — neither path deletes the expired invite. So an invite that was never accepted or deleted kept its invitations row and both SpiceDB tuples (#user and #org) forever. Add a daily background job (robfig/cron, "0 0 * * *" UTC, same pattern as the domain-verification and session-cleanup jobs) that finds expired invitations and deletes them. The job deletes through the existing invitation.Delete, which removes both SpiceDB tuples and the invitations row together. This is important: the repository's old GarbageCollect ran a raw row DELETE, which would have left the SpiceDB tuples behind as orphans. GarbageCollect is removed and replaced with a read-only ListExpired that the service iterates over. - core/invitation: InitInvitationCleanup / DeleteExpiredInvitations / Close, and a ListExpired repository method. - cmd/serve: start the job at boot and stop it on shutdown, next to the other periodic jobs. - store/postgres: GarbageCollect -> ListExpired. - tests for the sweep and for ListExpired. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
f9286aa to
b8824f2
Compare
…ention The domain-verification and session-cleanup jobs both declare their daily schedule as a package const named refreshTime. Use the same name and inline comment here instead of a bespoke const. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Coverage Report for CI Build 29004637891Coverage increased (+0.005%) to 44.884%Details
Uncovered Changes
Coverage RegressionsNo coverage regressions found. Coverage Stats
💛 - Coveralls |
Coverage Report for CI Build 29002914971Coverage increased (+0.001%) to 44.88%Details
Uncovered Changes
Coverage RegressionsNo coverage regressions found. Coverage Stats
💛 - Coveralls |
Only delete invitations that expired at least 7 days ago, instead of everything past its expiry. A recently expired invite still shows up in the list APIs; the daily job removes it once it crosses the retention window. The retention window lives in the service (business logic) as a cutoff time (now minus 7 days) that is passed to ListExpired, so the repository just filters by the given time. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
How this was testedRan locally against a real Frontier (Postgres + SpiceDB). Set the cleanup cron to
The invite is removed through |
@whoAbhishekSah Can you validate these scenarios too -
|
| // retention window) so recently expired invites are skipped, and deletes the | ||
| // results through the invitation service so both the SpiceDB tuples and the | ||
| // invitations row are removed together — a raw row delete here would leak the | ||
| // invitation's #user / #org tuples behind. |
There was a problem hiding this comment.
row-only delete would leak the #user / #org tuples behind. - this line is repeated multiple times, we can clean it up
The problem
Expired invitations are never cleaned up.
If you try to accept an expired invite, it just fails. If someone sends a new invite to the same person, it writes over the old one. Neither of these actually removes the expired invite.
So an invite that is never accepted and never deleted stays around forever. Each invite is stored in three places: one row in the
invitationstable, and two records in SpiceDB (one links the invite to the user, one links it to the org). All three keep piling up over time.The fix
Add a job that runs once a day, at midnight UTC, and deletes expired invites. This matches how the domain and session cleanup jobs already work. If the job ever crashes, the crash is caught so it can't take down the server.
The job does not delete an invite the moment it expires. It waits until the invite has been expired for 7 days. That way a recently expired invite still shows up in the list for a week, and the job cleans it up after that.
To delete, the job uses the existing
invitation.Delete. That one call removes the table row and both SpiceDB records together.This is the important part. The code already had a
GarbageCollectfunction that deleted only the table row, with a plain SQL delete. That would have left the two SpiceDB records behind with nothing pointing to them. So I removed it and replaced it with a read-onlyListExpiredthat just finds the old invites. The job then deletes each one the safe way.If one invite fails to delete, the job logs it and moves on, so the rest still get cleaned up.
🤖 Generated with Claude Code