feat(api): add project delete#6476
Conversation
f765ace to
ec990a0
Compare
752bdd5 to
d88764b
Compare
ec990a0 to
b9bc707
Compare
d88764b to
ceefcf8
Compare
b9bc707 to
96584f9
Compare
ceefcf8 to
1a38c2e
Compare
1a38c2e to
d658814
Compare
96584f9 to
8048b4e
Compare
d658814 to
3e86500
Compare
26ec6eb to
5cb28c9
Compare
3e86500 to
3183dec
Compare
dae840b to
796688d
Compare
3f4580f to
21b765d
Compare
21b765d to
a1acb00
Compare
796688d to
4776f31
Compare
4776f31 to
81552ca
Compare
a1acb00 to
1853fec
Compare
81552ca to
9264c5e
Compare
1853fec to
cef4b82
Compare
There was a problem hiding this comment.
✅ 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 → ctrl —
svc/ctrl/services/project/delete_project.gonow resolves the project viaFindProjectById, enqueues the Restate teardown, then inserts theproject.deleteaudit log itself (non-transactionalniltx). The API handler no longer writes the audit log. - Actor plumbing —
DeleteProjectRequest.actor(field 2,ActorInfo) added to the proto and generated Go/TS; the API builds it viactrlclient.Actor(s)and ctrl asserts it non-nil (assert.All) and maps it back throughsvc/ctrl/internal/actor. - New dashboard tRPC delete router —
web/apps/dashboard/lib/trpc/routers/deploy/project/delete.tsdoes the workspace-scoped lookup, 404 / precondition pre-checks, then callsctrl.project.deleteProjectwith an inline actor (noname, matching the create convention). - Tests updated — the
200handler test now asserts the ctrl RPC carried the resolved project id andACTOR_TYPE_ROOT_KEYrather 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
Claude Opus | 𝕏
There was a problem hiding this comment.
✅ 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 → id —
handler.goresolves viaFindProjectByWorkspaceAndId(wasFindProjectByWorkspaceAndSlug), keyed onreq.ProjectIdandprincipal.WorkspaceID; the RBAC tuple still uses the resolvedproject.ID, so authorization is unaffected. - Align OpenAPI request body —
V2ProjectsDeleteProjectRequestBodyfieldslug→projectIdwithminLength: 8,maxLength: 255,pattern: "^[a-zA-Z0-9_]+$", mirroringgetProject/updateProjectbyte-for-byte (split + generated YAML +gen.gostruct). - Update test inputs —
200/401/403/404/412now passProjectId: project.ID;400swaps the slug-shape cases for id-shape cases (projectId too shortproj_1, invalid-charproj-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.
Claude Opus | 𝕏


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.deleteProjectendpoint 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 aproject.deleteaudit log entry. The project row is not removed directly by the API handler.Alongside the handler, this PR introduces:
ProjectDeleteEventaudit log constant and aDeleteProjectRBAC action typeCtrlProjectClienton theServicesstruct, wired up inrun.gowith the same auth interceptor pattern used by the deployment clientMockProjectClienttest double that recordsCreateProjectandDeleteProjectcalls for use in handler testsdelete_projectpermission surfaced in both the root key permissions page and the create-key dialog in the dashboardType of change
How should this be tested?
POST /v2/projects.deleteProjectwith a valid root key bearingproject.*.delete_projectand a known slug — expect200with an emptydataobject.404.412with the protection message and confirm the control plane RPC was not invoked.delete_project— expect403.401.400.project.deleteaudit log entry is created for the workspace on a successful call.delete_projectpermission appears in the root key permissions UI and the create-key dialog.Checklist
Required
pnpm buildpnpm fmtmise run fmtconsole.logsgit pull origin mainAppreciated