Skip to content

fix: match sponsors on immutable github_id instead of mutable login#1764

Merged
Priyanshu-byte-coder merged 1 commit into
Priyanshu-byte-coder:mainfrom
Ridanshi:fix/sponsor-immutable-id-matching
May 31, 2026
Merged

fix: match sponsors on immutable github_id instead of mutable login#1764
Priyanshu-byte-coder merged 1 commit into
Priyanshu-byte-coder:mainfrom
Ridanshi:fix/sponsor-immutable-id-matching

Conversation

@Ridanshi
Copy link
Copy Markdown
Contributor

Closes #1751

Problem

The sponsor sync job (GET /api/sponsors/sync) previously identified active sponsors by matching github_login strings returned by the GitHub GraphQL API against the users.github_login column in the database.

github_login is mutable — GitHub account owners can rename themselves at any time. GitHub also recycles usernames after a grace period. This created two distinct failure modes:

Scenario A — sponsor loses status after renaming their account

  1. User A (login: "alice", github_id: 100) sponsors the project.
  2. Sync runs → alice found in sponsor list → is_sponsor = true set on the row where github_login = 'alice'.
  3. User A renames their account to alice2.
  4. Sync runs again → alice is no longer in the sponsor list → is_sponsor = false incorrectly set, even though User A is still an active sponsor (they now appear as alice2).

Scenario B — recycled username grants privileges to a different account

  1. User A (login: "alice", github_id: 100) is a sponsor; is_sponsor = true.
  2. User A deletes their account; username alice is eventually freed.
  3. User B (github_id: 999) creates a new GitHub account with login: "alice" and signs into DevTrack.
  4. Sync runs → alice is still in GitHub's sponsor list (associated with the original databaseId: 100) → .update({ is_sponsor: true }).in("github_login", ["alice"]) sets is_sponsor = true on User B's row (the one currently holding github_login = 'alice').

Root cause

The GraphQL query only requested login:

... on User {
  login        # mutable
}

All DB operations used github_login as the matching key. The users table already has github_id text unique not null storing GitHub's immutable numeric account ID, but the sync job never used it.

Fix

src/app/api/sponsors/sync/route.ts

  • Add databaseId to both User and Organization fragments in the GraphQL query. GitHub's databaseId is the same immutable numeric ID as users.github_id.
  • Collect { githubId: String(databaseId), login } pairs. login is kept only for the response payload and logging — it plays no role in DB matching.
  • Skip any node that has no databaseId (e.g. deleted/ghost accounts) rather than falling back to login matching.
  • Fetch existing sponsors with .select("github_id") and build the diff on github_id values.
  • Apply .update({ is_sponsor: true/false }).in("github_id", ids) so privilege assignment follows the account, not the name.
  • Extend the response body with granted and revoked counts.

No schema migration is needed. users.github_id already exists, is NOT NULL, and has a UNIQUE index.

Identity chain

GitHub OAuth sign-in  →  profile.id (numeric)  →  users.github_id  (immutable, never changes)
GitHub GraphQL        →  databaseId (numeric)  →  same value       (immutable, never changes)

With this fix:

  • A sponsor who renames their account (alicealice2) continues to hold is_sponsor = true because the match is on github_id = 100, which doesn't change.
  • A new user who claims the recycled username alice (github_id = 999) will not receive sponsor privileges because 999 is not in the sponsor ID set.

Tests

test/sponsors-sync.test.ts — 16 new tests:

Category Cases
Auth/config guards missing CRON_SECRET (500), wrong header (401), missing GITHUB_TOKEN (500)
GitHub API errors non-ok HTTP response, GraphQL errors field, null data.user
Core fix grant uses github_id not github_login; revoke uses github_id not github_login
Recycled username (regression #1751) new user with recycled login does not receive sponsor status
Account rename (regression #1751) renamed sponsor retains status, no spurious revoke
No-op no DB writes when list is already in sync
Combined run grant + revoke in the same sync run
Missing databaseId nodes without databaseId are silently skipped
Response shape success, sponsorCount, granted, revoked, sponsors

All 16 tests pass. The single pre-existing failure in test/dateUtils.test.ts (timezone boundary) is unrelated to this change and was already failing on main.

The sponsor sync job previously identified sponsors by github_login, a
mutable string. GitHub allows account owners to rename themselves at any
time, and recycles usernames after a grace period. This meant:

  1. A sponsor renames their account — subsequent syncs see the new login
     and no longer find a matching row, so their is_sponsor flag gets
     cleared even though they are still an active sponsor.
  2. A different GitHub user claims the freed username and signs into
     DevTrack — the next sync sees that login in the sponsor list and
     grants is_sponsor = true to the wrong account.

The users table already stores github_id (the immutable numeric GitHub
account ID) with a UNIQUE NOT NULL constraint, populated from the OAuth
profile at sign-in. GitHub's GraphQL Sponsors API exposes the same value
as databaseId on both User and Organization nodes.

Changes to src/app/api/sponsors/sync/route.ts:
- Add databaseId to both the User and Organization fragments in the
  GraphQL query.
- Collect SponsorIdentity objects containing githubId (String(databaseId))
  and login (kept for logging/response only).
- Skip nodes that have no databaseId (e.g. deleted/ghost accounts).
- Query existing sponsors with .select("github_id") and build the diff
  set from github_id values instead of github_login values.
- Apply both grant and revoke operations with .in("github_id", ids),
  not .in("github_login", logins).
- Extend the response to include granted and revoked counts alongside the
  existing sponsorCount and sponsors fields.

No schema changes are required; users.github_id already exists and is
indexed as the primary immutable identity column.

test/sponsors-sync.test.ts — 16 new tests covering:
- Auth and config guards (missing CRON_SECRET, wrong header, missing token)
- GitHub API error paths (non-ok response, GraphQL errors, null user)
- Core fix: grant and revoke are issued against github_id not github_login
- Regression: recycled username does not transfer sponsor privileges
- Regression: renamed sponsor retains sponsor status (no spurious revoke)
- No-op when sponsor list is already in sync
- Grant + revoke in the same run
- Nodes without databaseId are silently skipped
- Response shape includes sponsorCount, granted, revoked, sponsors

Closes Priyanshu-byte-coder#1751
@vercel
Copy link
Copy Markdown

vercel Bot commented May 31, 2026

@Ridanshi is attempting to deploy a commit to the PRIYANSHU DOSHI's projects Team on Vercel.

A member of the Team first needs to authorize it.

@github-actions github-actions Bot added gssoc26 GSSoC 2026 contribution type:bug GSSoC type bonus: bug fix type:testing GSSoC type bonus: tests (+10 pts) labels May 31, 2026
@github-actions
Copy link
Copy Markdown

GSSoC Label Checklist 🏷️

@Priyanshu-byte-coder — please apply the appropriate labels before merging:

Difficulty (pick one):

  • level:beginner — 20 pts
  • level:intermediate — 35 pts
  • level:advanced — 55 pts
  • level:critical — 80 pts

Quality (optional):

  • quality:clean — ×1.2 multiplier
  • quality:exceptional — ×1.5 multiplier

Validation (required to score):

  • gssoc:approved — counts for points
  • gssoc:invalid / gssoc:spam / gssoc:ai-slop — does not score

Type labels (type:*) are auto-detected from files and title. Review and adjust if needed.
Points formula: (difficulty × quality_multiplier) + type_bonus

@Priyanshu-byte-coder Priyanshu-byte-coder added level2 GSSoC Level 2 - Medium complexity (25 points) gssoc:approved GSSoC: PR approved for scoring labels May 31, 2026
@Priyanshu-byte-coder Priyanshu-byte-coder merged commit a13c637 into Priyanshu-byte-coder:main May 31, 2026
4 of 5 checks passed
@github-actions
Copy link
Copy Markdown

🎉 Merged! Thanks for contributing to DevTrack.

If the project has been useful to you, a ⭐ star on the repo is the easiest way to support it — it helps DevTrack get discovered by more developers.

Keep an eye on open issues for your next contribution!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

gssoc:approved GSSoC: PR approved for scoring gssoc26 GSSoC 2026 contribution level2 GSSoC Level 2 - Medium complexity (25 points) type:bug GSSoC type bonus: bug fix type:testing GSSoC type bonus: tests (+10 pts)

Projects

None yet

2 participants