Skip to content

feat(api): add project listing#6473

Merged
ogzhanolguncu merged 4 commits into
mainfrom
06-15-feat_api_add_project_listing
Jun 19, 2026
Merged

feat(api): add project listing#6473
ogzhanolguncu merged 4 commits into
mainfrom
06-15-feat_api_add_project_listing

Conversation

@ogzhanolguncu

@ogzhanolguncu ogzhanolguncu commented Jun 16, 2026

Copy link
Copy Markdown
Collaborator

Important

Unkey is not accepting external pull requests at this time. Pull requests from people outside the Unkey team will not be reviewed or merged.

What does this PR do?

Adds the POST /v2/projects.listProjects endpoint, which returns a cursor-paginated list of projects scoped to the authenticated workspace. Results are ordered by project ID and support configurable page sizes (1–100, defaulting to 100).

This also introduces the read_project RBAC action (project.*.read_project), required to call the new endpoint. The permission is wired into both the root key permissions page and the root key creation dialog in the dashboard, and is registered in the shared RBAC TypeScript package.

The underlying database query (ListProjectsByWorkspace) uses a keyset/cursor pattern (id >= ?) for efficient pagination.

Fixes # (issue)

If there is not an issue for this, create one first. This is used for tracking purposes and helps us understand why this PR exists.

Type of change

  • Bug fix (non-breaking change which fixes an issue)
  • Chore (refactoring code, technical debt, workflow improvements)
  • Enhancement (small improvements)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • This change requires a documentation update

How should this be tested?

  • Call POST /v2/projects.listProjects with a valid root key bearing project.*.read_project — expect a 200 with a data array and pagination object.
  • Call with limit: 2 across a workspace with 5 projects and paginate using the returned cursor until hasMore: false — verify all 5 unique projects are returned with no duplicates.
  • Call with a root key that lacks project.*.read_project — expect 403.
  • Call with no Authorization header or an invalid token — expect 401.
  • Call with limit: 0 or limit: 101 — expect 400 with validation errors.
  • Create projects in two separate workspaces; verify that listing only returns projects belonging to the authenticated workspace.

Checklist

Required

  • Filled out the "How to test" section in this PR
  • Read Internal Workflow Guide
  • Self-reviewed my own code
  • Commented on my code in hard-to-understand areas
  • Ran pnpm build
  • Ran pnpm fmt
  • Ran mise run fmt
  • Checked for warnings, there are none
  • Removed all console.logs
  • Merged the latest changes from main onto my branch with git pull origin main
  • My changes don't cause any responsiveness issues

Appreciated

  • If a UI change was made: Added a screen recording or screenshots to this PR
  • Updated the Unkey Docs if changes were necessary

@vercel

vercel Bot commented Jun 16, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
dashboard Ready Ready Preview, Comment Jun 19, 2026 1:42pm
design Ready Ready Preview, Comment Jun 19, 2026 1:42pm

Request Review

@ogzhanolguncu ogzhanolguncu force-pushed the 06-15-feat_api_add_project_listing branch from db9ca4f to 081038c Compare June 16, 2026 10:59
@ogzhanolguncu ogzhanolguncu force-pushed the 06-15-feat_api_add_project_listing branch from 081038c to 2c23b7a Compare June 16, 2026 11:20
@ogzhanolguncu ogzhanolguncu force-pushed the 06-15-feat_api_add_project_listing branch from 2c23b7a to cd47d0d Compare June 16, 2026 11:37
@ogzhanolguncu ogzhanolguncu marked this pull request as ready for review June 16, 2026 12:23
@suzunn

suzunn commented Jun 16, 2026

Copy link
Copy Markdown

I noticed the new list endpoint treats the cursor as a raw project id lower bound and intentionally returns the first row of the next page as the next cursor. The current pagination test covers the generated cursor path well; I would add one more regression around a caller-supplied cursor that is malformed or belongs to another workspace.

That would lock in two useful guarantees: arbitrary cursor values do not leak projects across workspaces because the workspace predicate still wins, and the endpoint terminates cleanly with an empty page or a valid next page instead of depending on the cursor having come from this exact response stream.

@pullfrog pullfrog 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.

ℹ️ No critical issues — one informational note on index coverage.

Reviewed changes — the new POST /v2/projects.listProjects endpoint, its cursor pagination, the read_project RBAC action, and the supporting OpenAPI + dashboard wiring.

  • Add listProjects handlersvc/api/routes/v2_projects_list_projects/handler.go authorizes project.*.read_project, fetches limit+1 rows, and derives hasMore / nextCursor from the extra row before trimming.
  • Keyset query ListProjectsByWorkspaceIdWHERE workspace_id = ? AND id >= ? ORDER BY id ASC LIMIT ?, an inclusive-boundary cursor scan over the unique id column.
  • New read_project RBAC action — registered in pkg/rbac/permissions.go, the web/internal/rbac zod enum, and both dashboard permission lists.
  • OpenAPI surface — request/response/data schemas, examples, and Speakeasy cursor-pagination hints, with regenerated Go and yaml.
  • Tests — 200 (empty / populated / stale cursor / pagination / workspace isolation), 400 (limit bounds + invalid JSON), 401, 403.

Pagination is sound: project IDs are random (pkg/uid/new.go uses math/rand/v2, not a monotonic scheme), but a keyset scan over the unique id column with an inclusive id >= cursor boundary and a limit+1 lookahead is still stable, complete, and duplicate-free — the pagination test confirms this. Workspace scoping on the query also means a cursor borrowed from another workspace cannot leak rows.

ℹ️ efficient pagination claim isn't backed by a composite index

The PR description frames the keyset query as efficient pagination, but the projects table only has workspace_idx on (workspace_id) — the primary key is the auto-increment pk, and there is no (workspace_id, id) index. MySQL can use workspace_idx to narrow to the workspace, but the id >= ? range plus ORDER BY id ASC then requires sorting that workspace's rows rather than walking an index in order. This is negligible at typical project counts and not a correctness issue, but it does mean the scan is not a true index-ordered keyset walk as workspaces grow.

Technical details
# `efficient pagination` claim isn't backed by a composite index

## Affected sites
- `pkg/db/queries/project_list_by_workspace.sql``WHERE workspace_id = ? AND id >= ? ORDER BY id ASC LIMIT ?`
- `pkg/mysql/schema/projects.sql` — table has `PRIMARY KEY(pk)`, `UNIQUE(id)`, `UNIQUE(workspace_id, slug)`, and `CREATE INDEX workspace_idx ON projects (workspace_id)`. No `(workspace_id, id)` index.

## Required outcome
- Either add a composite `(workspace_id, id)` index so the keyset scan is index-ordered and the `ORDER BY` is satisfied without a filesort, or soften the "efficient pagination" wording so the claim matches the current index coverage.

## Open questions for the human
- Is this endpoint expected to serve workspaces with large project counts? If projects-per-workspace stays small (tens), the missing index is a non-issue and this can be dropped. If it can grow large, the composite index is the cheap fix and belongs in a migration alongside this change.

Pullfrog  | Fix it ➔View workflow run | Using Claude Opus𝕏

@ogzhanolguncu ogzhanolguncu force-pushed the 06-15-feat_api_add_project_listing branch from 086a639 to 2147117 Compare June 19, 2026 13:39
@ogzhanolguncu ogzhanolguncu enabled auto-merge June 19, 2026 13:52
@ogzhanolguncu ogzhanolguncu added this pull request to the merge queue Jun 19, 2026
Merged via the queue into main with commit cd822e7 Jun 19, 2026
14 checks passed
@ogzhanolguncu ogzhanolguncu deleted the 06-15-feat_api_add_project_listing branch June 19, 2026 14:08
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.

4 participants