Skip to content

feat(groups): add scoped leaderboards with invites - #524

Merged
junhoyeo merged 3 commits into
junhoyeo:mainfrom
IvGolovach:codex/group-scoped-leaderboards
May 24, 2026
Merged

feat(groups): add scoped leaderboards with invites#524
junhoyeo merged 3 commits into
junhoyeo:mainfrom
IvGolovach:codex/group-scoped-leaderboards

Conversation

@IvGolovach

@IvGolovach IvGolovach commented May 6, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Add group-scoped leaderboards so teams, workspaces, and cohorts can compare usage independently from the global leaderboard.
  • Add public/private groups, owner/admin/member roles, hashed invite links, optional username-targeted invites, and member management APIs.
  • Add the /groups UI flow for listing groups, creating groups, joining by invite, viewing a scoped leaderboard, copying invite links, leaving groups, and managing members.
  • Invalidate group leaderboard caches when a user's submitted usage is created, replaced, or deleted.

Why

The global leaderboard is useful for public ranking, but it does not cover private teams or custom communities that need scoped usage comparisons. This change keeps the existing global leaderboard intact and adds a membership-gated leaderboard surface for groups. Related to #462.

Diff scope

  • Database: adds groups, group_members, and group_invites through packages/frontend/src/lib/db/migrations/0006_add_groups.sql, with schema relations and supporting indexes in Drizzle.
  • Server APIs: adds group creation/list/detail, invite, join, leave, member listing, member role update/removal, user-group listing, and group leaderboard routes.
  • Leaderboard logic: adds packages/frontend/src/lib/groups/getGroupLeaderboard.ts with all-time, weekly, and monthly scoped ranking support over group membership.
  • Auth and permissions: adds request-session lookup plus group role helpers for owner/admin/member access checks.
  • UI: adds /groups, /groups/new, /groups/[slug], and /groups/join/[token], plus navigation entry.
  • Cache invalidation: clears group leaderboard caches after submit and submitted-data delete paths.
  • Tests: adds API and library coverage for invite/join behavior, scoped leaderboard behavior, helper logic, and cache invalidation integration.

Branch integrity

  • Base branch: main
  • Validated base SHA: 3a9045bc1b9aa245ca44ffcdca68bdaaa09520eb
  • Head branch: IvGolovach:codex/group-scoped-leaderboards
  • Head SHA: 68ad3c4f25ac75f43b68ffbf24c7f1e2fee0ea37
  • Ahead/behind: 0 behind, 1 ahead
  • Merge base: 3a9045bc1b9aa245ca44ffcdca68bdaaa09520eb
  • Fast-forward safety: origin/main is an ancestor of this branch.

Commit integrity

  • 68ad3c4f25ac75f43b68ffbf24c7f1e2fee0ea37 feat(groups): add scoped leaderboards with invites
  • One logical feature commit.
  • Final PR diff contains only the group leaderboard feature, its migration, its API/UI/library tests, and required submit/delete cache invalidation.

Diff hygiene

  • git diff --name-status origin/main...HEAD: PASS, 38 intended frontend files changed.
  • git diff --check origin/main...HEAD: PASS, no output.

Validation mode and proof

  • Validation mode: Mode 3 — this adds a frontend database migration, access-controlled API routes, cache invalidation paths, and a user-facing group leaderboard flow.
  • git fetch --no-tags origin main:refs/remotes/origin/main: PASS
  • git diff --check: PASS
  • git diff --cached --check: PASS
  • git diff --check origin/main...HEAD: PASS, no output
  • bunx vitest run: PASS, 25 files and 188 tests
  • bun run lint: PASS, 0 errors; existing warnings remain outside this change
  • bun run build: PASS
  • Remote CI: Pending — this PR page has not been submitted yet.

Migration notes

  • Migration file: packages/frontend/src/lib/db/migrations/0006_add_groups.sql
  • Classification: additive.
  • New tables: groups, group_members, group_invites.
  • Existing data: no existing users, submissions, sessions, API tokens, or leaderboard rows are rewritten.
  • Backfill: not required.
  • Existing-state guards: tables and indexes use IF NOT EXISTS; group slugs, group membership, and invite token hashes are protected by uniqueness constraints.
  • Rollback caveat: if the migration has already been applied, rollback requires removing the new group tables and related indexes after deciding whether any post-deploy group data should be retained.

Runtime safety

  • Reviewed runtime paths: group API routes, group permission helpers, invite token hashing/lookup, scoped leaderboard queries, submit cache invalidation, and submitted-data delete cache invalidation.
  • No new blocking locks, unbounded queues, background jobs, or external service calls are introduced.
  • No invariant regression introduced.

Documentation integrity

Not applicable — no docs, runbooks, release procedures, commands, or operational instructions changed.

Rollback plan

  • Code rollback: revert this PR.
  • DB downgrade: if the migration was applied, drop the new group tables and their indexes/constraints after preserving any group data that must survive rollback.
  • Data repair: not applicable for existing usage submissions or global leaderboard data.
  • Operational caveats: the group UI/API requires the additive migration before use.

Known residual risks

  • The feature is intentionally additive, but production use depends on the database migration being applied before users access group routes.

@vercel

vercel Bot commented May 6, 2026

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
tokscale Ready Ready Preview, Comment May 24, 2026 4:32pm

Request Review

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

8 issues found across 38 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="packages/frontend/src/lib/groups/getGroupLeaderboard.ts">

<violation number="1" location="packages/frontend/src/lib/groups/getGroupLeaderboard.ts:1">
P2: All-time leaderboard ranking is non-deterministic for ties because SQL ordering lacks tie-breakers before rank assignment</violation>
</file>

<file name="packages/frontend/src/app/api/groups/[slug]/leave/route.ts">

<violation number="1" location="packages/frontend/src/app/api/groups/[slug]/leave/route.ts:41">
P2: Cache revalidation failure can mask a successful DB delete, causing inconsistent client-visible state</violation>
</file>

<file name="packages/frontend/src/app/(main)/groups/[slug]/GroupDetailClient.tsx">

<violation number="1" location="packages/frontend/src/app/(main)/groups/[slug]/GroupDetailClient.tsx:491">
P2: Clipboard copy is awaited without error handling, so copy failures can become unhandled promise rejections and leave the UI with no feedback.</violation>
</file>

<file name="packages/frontend/src/lib/groups/utils.ts">

<violation number="1" location="packages/frontend/src/lib/groups/utils.ts:37">
P1: Regex ordering bug: underscores removed before they can be converted to hyphens, causing avoidable slug collisions</violation>
</file>

<file name="packages/frontend/src/app/api/submit/route.ts">

<violation number="1" location="packages/frontend/src/app/api/submit/route.ts:434">
P2: Async group leaderboard revalidation before username path revalidation creates a cascading failure point — a DB query failure in revalidateUserGroupLeaderboards silently prevents revalidateUsernamePaths from running, leaving user profile pages stale.</violation>
</file>

<file name="packages/frontend/src/app/(main)/groups/[slug]/page.tsx">

<violation number="1" location="packages/frontend/src/app/(main)/groups/[slug]/page.tsx:71">
P1: Private group slug enumeration via differing HTTP status codes (200 vs 404)</violation>
</file>

<file name="packages/frontend/src/app/api/groups/[slug]/route.ts">

<violation number="1" location="packages/frontend/src/app/api/groups/[slug]/route.ts:110">
P2: PATCH accepts non-string `description`/`avatarUrl` values and silently turns them into `null`, which can wipe existing group data instead of rejecting invalid input.</violation>
</file>

<file name="packages/frontend/src/lib/groups/invites.ts">

<violation number="1" location="packages/frontend/src/lib/groups/invites.ts:188">
P1: TOCTOU race condition in acceptGroupInvite: invite eligibility is checked outside the transaction and never re-validated inside, allowing concurrent double-use of single-use invite tokens</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Comment thread packages/frontend/src/lib/groups/utils.ts Outdated
Comment thread packages/frontend/src/app/(main)/groups/[slug]/page.tsx
Comment thread packages/frontend/src/lib/groups/invites.ts Outdated
Comment thread packages/frontend/src/lib/groups/getGroupLeaderboard.ts
Comment thread packages/frontend/src/app/api/groups/[slug]/leave/route.ts Outdated
Comment thread packages/frontend/src/app/(main)/groups/[slug]/GroupDetailClient.tsx Outdated
Comment thread packages/frontend/src/app/api/submit/route.ts
Comment thread packages/frontend/src/app/api/groups/[slug]/route.ts Outdated
IvGolovach and others added 3 commits May 25, 2026 01:29
Validation
* Validation tier: Tier 3 — frontend database migration, access-controlled API routes, and user-facing group leaderboard flow.
* git diff --check: PASS
* git diff --cached --check: PASS
* bunx vitest run: PASS, 25 files and 188 tests
* bun run lint: PASS, 0 errors; existing warnings remain outside this change
* bun run build: PASS
* Ledger: not applicable — not required for selected validation tier/change family.
* Version: not applicable — not required for selected validation tier/change family.

Rollback
* git revert HEAD
Validation
* Validation tier: Tier 3 — post-review corrections for group access control, invite mutation atomicity, cache invalidation behavior, and leaderboard ordering.
* git diff --check: PASS
* git diff --cached --check: PASS
* bunx vitest run packages/frontend/__tests__/lib/groupHelpers.test.ts packages/frontend/__tests__/lib/groupInvites.test.ts packages/frontend/__tests__/lib/getGroupLeaderboard.test.ts packages/frontend/__tests__/api/groupRoute.test.ts packages/frontend/__tests__/api/groupLeaveRoute.test.ts packages/frontend/__tests__/api/groupLeaderboardRoute.test.ts packages/frontend/__tests__/api/groupInviteJoinRoute.test.ts packages/frontend/__tests__/api/submitAuth.test.ts packages/frontend/__tests__/api/settingsSubmittedDataDelete.test.ts: PASS, 9 files and 28 tests
* bunx vitest run packages/frontend/__tests__: PASS, 28 files and 195 tests
* bun run lint: PASS, 0 errors; existing warnings remain outside this change
* bun run build: PASS
* Ledger: not applicable — not required for selected validation tier/change family.
* Version: not applicable — not required for selected validation tier/change family.

Rollback
* git revert HEAD
…o 0009

PATCH /groups/[slug]/members/[userId]/role already rejects nextRole === "owner"
and only permits actors whose role outranks the target's, so the sole owner of
a group could still demote themselves to admin/member and orphan the group
(no owner, no further role-change recourse without DB intervention).

Add an explicit guard: when demoting an existing owner, count the remaining
owners; if zero would remain, refuse with 400 and tell the caller to transfer
ownership first. Matches the spirit of the existing /leave route, which already
forbids owners from leaving without first transferring or deleting the group.

Also renamed the migration from 0007_add_groups.sql to 0009_add_groups.sql
to avoid filename collision with main's 0007_add_submitted_devices.sql and
0008_add_time_metrics_to_submissions.sql.

Constraint: cannot promote-to-owner via this route (existing line 36 check)
Rejected: enforce in canManageGroupRole | wrong layer — that helper doesn't know group cardinality
Confidence: high
Scope-risk: narrow
Directive: do NOT relax this without first adding an explicit "transfer ownership" route — there is currently no recovery path for an orphaned group from the public API
Not-tested: no unit test for the last-owner branch yet (filed as a followup)
@junhoyeo
junhoyeo force-pushed the codex/group-scoped-leaderboards branch from edd29f2 to fc2fc1a Compare May 24, 2026 16:31
@junhoyeo
junhoyeo merged commit 1fa26f5 into junhoyeo:main May 24, 2026
3 of 4 checks passed
@junhoyeo

Copy link
Copy Markdown
Owner

@IvGolovach this has been merged to v3.0.0: https://github.com/junhoyeo/tokscale/releases/tag/v3.0.0 thanks for the contribution!

This was referenced May 30, 2026
junhoyeo added a commit that referenced this pull request Jul 6, 2026
… modernize cache invalidation

Review follow-up after merging main into this branch. Main moved on
while this PR was open, so DELETE /api/settings/account is brought up
to the current baseline:

- Authenticate via getSessionFromRequest(request) instead of bare
  getSession(), inheriting the CSRF Origin-allowlist gate that #615
  (f2bf654) retrofitted onto every other cookie-mutating settings
  route (submitted-data, devices, tokens).
- Use normalizeUsernameCacheKey() for all user:/user-rank:/embed-user:
  revalidateTag calls and revalidateUsernamePaths() for path
  revalidation, matching the post-#485 baseline.
- Call revalidateUserGroupLeaderboards() (#524) before the users-row
  delete, since the helper needs the group membership rows that the
  delete cascades away. Best-effort like all other revalidation.
- Update the route tests for the request-based signature: requests now
  carry an Origin header exercised against the real
  getSessionFromRequest, plus new cases asserting missing/disallowed
  Origin yields 401 with no DB delete, mirroring the sibling routes'
  CSRF tests.

Constraint: Cookie-auth mutations must pass the Origin allowlist convention adopted in #615
Constraint: revalidateUserGroupLeaderboards reads group membership rows that ON DELETE CASCADE removes
Rejected: Bearer personal-token path for account deletion | web-Settings-UI action; session-only is the conservative default and answers the parity question raised in review
Rejected: Invalidate group leaderboards after the delete | membership rows are already cascaded away by then
Confidence: high
Scope-risk: narrow
Not-tested: CSRF_ALLOWED_ORIGINS env override branch (covered indirectly by requestSession's own tests)
junhoyeo added a commit that referenced this pull request Jul 6, 2026
…letion (#379)

* feat(settings): add Danger Zone with self-service data and account deletion

Add a Danger Zone section to the Settings page with two destructive actions:

- Delete submitted data: removes leaderboard entries, profile stats, and
  usage history while keeping the account active
- Delete account: permanently deletes the user and all associated data
  (sessions, tokens, submissions) via ON DELETE CASCADE

Both flows use a 3-step confirmation modal (intent → warning → typed
confirmation) matching the CLI's triple-confirmation pattern.

New API route: DELETE /api/settings/account
New tests: settingsAccountDelete.test.ts (5 cases)

* Update packages/frontend/src/app/settings/SettingsClient.tsx

Co-authored-by: devin-ai-integration[bot] <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* fix(settings): gate account deletion behind CSRF Origin allowlist and modernize cache invalidation

Review follow-up after merging main into this branch. Main moved on
while this PR was open, so DELETE /api/settings/account is brought up
to the current baseline:

- Authenticate via getSessionFromRequest(request) instead of bare
  getSession(), inheriting the CSRF Origin-allowlist gate that #615
  (f2bf654) retrofitted onto every other cookie-mutating settings
  route (submitted-data, devices, tokens).
- Use normalizeUsernameCacheKey() for all user:/user-rank:/embed-user:
  revalidateTag calls and revalidateUsernamePaths() for path
  revalidation, matching the post-#485 baseline.
- Call revalidateUserGroupLeaderboards() (#524) before the users-row
  delete, since the helper needs the group membership rows that the
  delete cascades away. Best-effort like all other revalidation.
- Update the route tests for the request-based signature: requests now
  carry an Origin header exercised against the real
  getSessionFromRequest, plus new cases asserting missing/disallowed
  Origin yields 401 with no DB delete, mirroring the sibling routes'
  CSRF tests.

Constraint: Cookie-auth mutations must pass the Origin allowlist convention adopted in #615
Constraint: revalidateUserGroupLeaderboards reads group membership rows that ON DELETE CASCADE removes
Rejected: Bearer personal-token path for account deletion | web-Settings-UI action; session-only is the conservative default and answers the parity question raised in review
Rejected: Invalidate group leaderboards after the delete | membership rows are already cascaded away by then
Confidence: high
Scope-risk: narrow
Not-tested: CSRF_ALLOWED_ORIGINS env override branch (covered indirectly by requestSession's own tests)

---------

Co-authored-by: devin-ai-integration[bot] <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants