Skip to content

feat(api): add project delete#6476

Merged
ogzhanolguncu merged 5 commits into
mainfrom
06-16-feat_api_add_project_delete
Jun 19, 2026
Merged

feat(api): add project delete#6476
ogzhanolguncu merged 5 commits into
mainfrom
06-16-feat_api_add_project_delete

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 a POST /v2/projects.deleteProject endpoint that allows callers to delete a project by its slug. Deletion is asynchronous and eventually consistent — the handler resolves the project, checks delete protection, delegates the actual teardown to the control plane via a durable Restate workflow, and writes a project.delete audit log entry. The project row is not removed directly by the API handler.

Alongside the handler, this PR introduces:

  • A ProjectDeleteEvent audit log constant and a DeleteProject RBAC action type
  • A CtrlProjectClient on the Services struct, wired up in run.go with the same auth interceptor pattern used by the deployment client
  • A MockProjectClient test double that records CreateProject and DeleteProject calls for use in handler tests
  • Full test coverage across the 200, 400, 401, 403, 404, and 412 response paths
  • OpenAPI spec additions (split and generated YAML, Go types) for the new request/response bodies and path
  • The delete_project permission surfaced in both the root key permissions page and the create-key dialog in the dashboard

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.deleteProject with a valid root key bearing project.*.delete_project and a known slug — expect 200 with an empty data object.
  • Repeat with a slug that does not exist in the caller's workspace — expect 404.
  • Repeat with a project that has delete protection enabled — expect 412 with the protection message and confirm the control plane RPC was not invoked.
  • Repeat with a root key that lacks delete_project — expect 403.
  • Repeat with no or an invalid bearer token — expect 401.
  • Submit a malformed JSON body or a slug that violates the pattern (uppercase, underscores, >256 chars) — expect 400.
  • Verify a project.delete audit log entry is created for the workspace on a successful call.
  • Confirm the delete_project permission appears in the root key permissions UI and the create-key dialog.

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 3:30pm
design Ready Ready Preview, Comment Jun 19, 2026 3:30pm

Request Review

@ogzhanolguncu ogzhanolguncu force-pushed the 06-16-feat_api_add_project_update_endpoint branch from f765ace to ec990a0 Compare June 16, 2026 10:59
@ogzhanolguncu ogzhanolguncu force-pushed the 06-16-feat_api_add_project_delete branch from 752bdd5 to d88764b Compare June 16, 2026 10:59
@ogzhanolguncu ogzhanolguncu force-pushed the 06-16-feat_api_add_project_update_endpoint branch from ec990a0 to b9bc707 Compare June 16, 2026 11:20
@ogzhanolguncu ogzhanolguncu force-pushed the 06-16-feat_api_add_project_delete branch from d88764b to ceefcf8 Compare June 16, 2026 11:20
@ogzhanolguncu ogzhanolguncu force-pushed the 06-16-feat_api_add_project_update_endpoint branch from b9bc707 to 96584f9 Compare June 16, 2026 11:37
@ogzhanolguncu ogzhanolguncu force-pushed the 06-16-feat_api_add_project_delete branch from ceefcf8 to 1a38c2e Compare June 16, 2026 11:37
@ogzhanolguncu ogzhanolguncu force-pushed the 06-16-feat_api_add_project_delete branch from 1a38c2e to d658814 Compare June 16, 2026 12:13
@ogzhanolguncu ogzhanolguncu force-pushed the 06-16-feat_api_add_project_update_endpoint branch from 96584f9 to 8048b4e Compare June 16, 2026 12:13
@ogzhanolguncu ogzhanolguncu force-pushed the 06-16-feat_api_add_project_delete branch from d658814 to 3e86500 Compare June 16, 2026 12:18
@ogzhanolguncu ogzhanolguncu force-pushed the 06-16-feat_api_add_project_update_endpoint branch from 26ec6eb to 5cb28c9 Compare June 16, 2026 12:20
@ogzhanolguncu ogzhanolguncu force-pushed the 06-16-feat_api_add_project_delete branch from 3e86500 to 3183dec Compare June 16, 2026 12:20
@ogzhanolguncu ogzhanolguncu force-pushed the 06-16-feat_api_add_project_delete branch from dae840b to 796688d Compare June 17, 2026 10:11
@ogzhanolguncu ogzhanolguncu force-pushed the 06-16-feat_api_add_project_update_endpoint branch from 3f4580f to 21b765d Compare June 17, 2026 10:11
@ogzhanolguncu ogzhanolguncu force-pushed the 06-16-feat_api_add_project_update_endpoint branch from 21b765d to a1acb00 Compare June 17, 2026 11:05
@ogzhanolguncu ogzhanolguncu force-pushed the 06-16-feat_api_add_project_delete branch from 796688d to 4776f31 Compare June 17, 2026 11:05
@ogzhanolguncu ogzhanolguncu force-pushed the 06-16-feat_api_add_project_delete branch from 4776f31 to 81552ca Compare June 17, 2026 17:30
@ogzhanolguncu ogzhanolguncu force-pushed the 06-16-feat_api_add_project_update_endpoint branch from a1acb00 to 1853fec Compare June 17, 2026 17:30
@ogzhanolguncu ogzhanolguncu force-pushed the 06-16-feat_api_add_project_delete branch from 81552ca to 9264c5e Compare June 18, 2026 08:33
@ogzhanolguncu ogzhanolguncu force-pushed the 06-16-feat_api_add_project_update_endpoint branch from 1853fec to cef4b82 Compare June 18, 2026 08:33

@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 new issues found.

Reviewed changes — incremental review of the audit-to-ctrl migration since the prior pullfrog review at 33bd80b (fix: regex). The project.delete audit write moved out of the API handler and into the control plane, with caller identity plumbed through the proto, plus a new dashboard tRPC delete router.

  • Moved audit write API → ctrlsvc/ctrl/services/project/delete_project.go now resolves the project via FindProjectById, enqueues the Restate teardown, then inserts the project.delete audit log itself (non-transactional nil tx). The API handler no longer writes the audit log.
  • Actor plumbingDeleteProjectRequest.actor (field 2, ActorInfo) added to the proto and generated Go/TS; the API builds it via ctrlclient.Actor(s) and ctrl asserts it non-nil (assert.All) and maps it back through svc/ctrl/internal/actor.
  • New dashboard tRPC delete routerweb/apps/dashboard/lib/trpc/routers/deploy/project/delete.ts does the workspace-scoped lookup, 404 / precondition pre-checks, then calls ctrl.project.deleteProject with an inline actor (no name, matching the create convention).
  • Tests updated — the 200 handler test now asserts the ctrl RPC carried the resolved project id and ACTOR_TYPE_ROOT_KEY rather than a DB audit row, since the audit write lives in ctrl now.

The actor wiring is consistent end-to-end (API helper, dashboard inline, proto field, ctrl assertion + mapping) and mirrors the create endpoint. The prior review's note about the audit insert happening after the external enqueue and outside a transaction is unchanged — it has simply relocated from the API handler into ctrl, where the same shape applies (the Restate enqueue cannot share a DB transaction with the audit write).

ℹ️ Delete audit write in ctrl has no integration test

The create sibling has a dedicated ctrl integration test (svc/ctrl/integration/create_project_test.go) asserting the project.create audit row reaches the outbox. The delete path moved its audit write into ctrl in this delta but added no analogous test, so nothing verifies the project.delete audit log is actually emitted with the right workspace and actor. The API handler test only checks the RPC was called — it cannot see the audit write that now happens inside ctrl. Mergeable as-is; flagging for parity with create.

Technical details
# Delete audit write in ctrl has no integration test

## Affected sites
- `svc/ctrl/services/project/delete_project.go:50-72``project.delete` audit insert, untested
- `svc/ctrl/integration/create_project_test.go` — the pattern to mirror (`TestCreateProjectWritesAuditLog`)
- `svc/api/routes/v2_projects_delete_project/200_test.go:51-56` — API test only asserts the mocked RPC call, not the audit row

## Required outcome
- An `//go:build integration` test in `svc/ctrl/integration/` that calls `project.Service.DeleteProject` with a valid actor and asserts a `project.delete` audit log lands in the outbox with the expected `WorkspaceID`, actor id/type, and resource meta (`name`/`slug`).

## Open questions for the human
- Is a ctrl integration test for delete in scope for this PR, or deferred to a follow-up? Create having one and delete not is the only asymmetry.

| Using Claude Opus

Pullfrog  | View workflow run | Using Claude Opus𝕏

@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 new issues found.

Reviewed changes — incremental review of the refactor: use id instead of slug commit (db48c18) since the prior pullfrog review at fce9d28. The delete endpoint now identifies the target project by projectId rather than by slug.

  • Switch delete identifier slug → idhandler.go resolves via FindProjectByWorkspaceAndId (was FindProjectByWorkspaceAndSlug), keyed on req.ProjectId and principal.WorkspaceID; the RBAC tuple still uses the resolved project.ID, so authorization is unaffected.
  • Align OpenAPI request bodyV2ProjectsDeleteProjectRequestBody field slugprojectId with minLength: 8, maxLength: 255, pattern: "^[a-zA-Z0-9_]+$", mirroring getProject/updateProject byte-for-byte (split + generated YAML + gen.go struct).
  • Update test inputs200/401/403/404/412 now pass ProjectId: project.ID; 400 swaps the slug-shape cases for id-shape cases (projectId too short proj_1, invalid-char proj-1234abc, too-long 256 chars) and the malformed-JSON body to {"projectId": }.

The dashboard tRPC delete router already keyed on projectId, so it stays consistent. The remaining Slug: references in the test files are all on seed.CreateProjectRequest setup (projects still carry slugs), not on the handler request. The prior review's notes (audit write in ctrl is non-transactional / lacks an integration test) are untouched by this commit and remain non-blocking.

Pullfrog  | View workflow run | Using Claude Opus𝕏

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.

3 participants