Skip to content

feat(resource): soft-delete resources and keep the URN unique over live rows only - #1937

Open
AmanGIT07 wants to merge 4 commits into
mainfrom
soft-delete-resources
Open

AmanGIT07 wants to merge 4 commits into
mainfrom
soft-delete-resources

Conversation

@AmanGIT07

@AmanGIT07 AmanGIT07 commented Sep 20, 2026

Copy link
Copy Markdown
Contributor

What

  • DeleteProjectResource sets deleted_at on the resource row instead of removing it. SpiceDB tuples are still removed.
  • Reads, updates, and deletes in the resource repository skip rows with deleted_at set, through the fromLive and live helpers.
  • The resources_urn_key constraint is replaced by the partial unique index uq_resources_urn_live over live rows. The upsert's conflict target names that index, through a new liveConflictTarget helper in postgres.go. Migration and query change ship together.
  • The service writes a resource.deleted audit record. The app.resource.deleted audit log is unchanged.
  • Creating a resource with the id of a soft-deleted one returns 409 instead of 500.
  • The project delete cascade purges resources for good instead of soft-deleting them. The project row is still removed for good, and a resource row cannot outlive the project it points to. The cleanup lists soft-deleted rows too, through IncludeDeleted on the resource filter, so they are purged as well. Both carry a TODO(fix) to switch to the soft delete once project delete is soft.
  • The resource list is ordered by created_at, so its order no longer depends on the query plan.

Why

Soft delete keeps the row. A deleted resource must stop appearing in reads, and must not hold its URN forever. With the old plain constraint, a URN could never be used again once its resource was deleted.

Behaviour change

A deleted resource stays in the table and is hidden from the API. A create with the URN of a deleted resource inserts a new row, and the deleted row stays as history. A create with the URN of a live resource still updates it in place.

Rollout

Between the migration running and the new binary starting, the old binary's ON CONFLICT (urn) no longer matches an index, so resource creates fail for that window. Once a resource has been soft-deleted in an environment, a release without this change would show it as live again, and the down migration fails if a deleted row and a live row share a URN.

Tested

  • Repository suite against Postgres 13 in Docker: a live URN upsert updates in place, a deleted URN gets a new row, a deleted id returns conflict, delete sets deleted_at, and later gets, lists, updates, and deletes skip the row.
  • Service tests for the delete path and the audit record.
  • Migration applied, rolled back, and re-applied on a local Postgres 15.
  • New e2e case: delete a resource, get and delete again return not-found, the list is empty, the same name can be created again with a new id, and the org delete succeeds with one live and one deleted resource.
  • e2e regression suites TestOrganizationAPI and TestResourceAPI pass locally against Docker.
  • golangci-lint reports no issues on the changed packages.

SQL Safety

  • Values flow through goqu.Ex{} and goqu.Record{}. The conflict target and now() are constants with no caller input.
  • ToSQL() params are forwarded unchanged.
  • No ? placeholders inside quoted SQL literals.
  • No new //nolint or #nosec annotations.

…ve rows only

DeleteProjectResource now sets deleted_at instead of removing the row, and
reads, updates, and deletes skip rows that have it set. The URN unique
constraint becomes a partial unique index over live rows, and the upsert
names it, so a deleted URN can be used again. The delete also writes a
resource.deleted audit record.
@vercel

vercel Bot commented Sep 20, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated
frontier Ready Ready Preview Sep 21, 2026 7:30am UTC

@coderabbitai

coderabbitai Bot commented Sep 20, 2026

Copy link
Copy Markdown
Contributor

Review Change StackReview Change Stack

Understand this PR’s impact

Explore downstream dependencies and potential security impact with Blast Radius.

View blast radius →

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository: raystack/frontier/.coderabbit.yaml

Review profile: CHILL

Plan: Advanced

Run ID: 3fe07937-bed4-4c53-a333-b71dc3dac947

📥 Commits

Reviewing files that changed from the base of the PR and between 9cdf0fe and 8ffcf75.

📒 Files selected for processing (5)
  • core/deleter/service.go
  • core/resource/filter.go
  • core/resource/service.go
  • internal/store/postgres/postgres.go
  • internal/store/postgres/resource_repository.go
🚧 Files skipped from review as they are similar to previous changes (5)
  • internal/store/postgres/postgres.go
  • core/resource/filter.go
  • core/deleter/service.go
  • core/resource/service.go
  • internal/store/postgres/resource_repository.go

Included review availability: Your plan provides up to 2 included reviews per hour; 1 remains after this review.


📝 Summary

Summary by CodeRabbit

  • New Features

    • Deleted resources are retained for audit purposes while hidden from normal retrieval and listing.
    • Resource deletion activity is recorded in the audit history.
    • Previously deleted resource identifiers can be reused when creating resources.
    • Resources are permanently removed when their project is permanently deleted.
    • Deleted resources can be included in administrative listings when needed.
  • Bug Fixes

    • Prevented duplicate live resource identifiers and improved conflict handling.
    • Resource deletion now consistently reports missing resources when no active record exists.

Walkthrough

Resource deletion now uses soft deletion, excludes deleted resources from normal repository operations, supports live-only URN uniqueness, separates hard purging for project cleanup, and records a deletion audit event after successful service deletion.

Changes

Resource lifecycle

Layer / File(s) Summary
Soft-delete repository behavior
internal/store/postgres/migrations/*, internal/store/postgres/postgres.go, internal/store/postgres/resource_repository.go, internal/store/postgres/resource_repository_test.go
Postgres now enforces URN uniqueness only for live resources. Repository reads and updates exclude soft-deleted rows. Delete sets deleted_at, while Purge permanently removes rows. Tests cover these behaviors.
Service deletion and purge flow
pkg/auditrecord/consts.go, core/resource/resource.go, core/resource/service.go, core/resource/service_test.go, core/resource/mocks/repository.go
Service.Delete loads the resource and project, deletes the resource, and creates ResourceDeletedEvent data after success. Service.Purge removes the relation and permanently deletes the row.
Project cleanup purge integration
core/resource/filter.go, core/deleter/service.go, core/deleter/service_test.go, core/deleter/mocks/resource_service.go, test/e2e/regression/api_test.go
Project deletion includes soft-deleted resources and calls ResourceService.Purge for each resource. The API regression test verifies hidden deleted resources, repeated-delete behavior, URN reuse, and cleanup.

Priority: ➖ Normal

Estimated code review effort: 3 (Moderate) | ~25 minutes

Change: Feature

🚥 Pre-merge checks | ✅ 2
✅ Passed checks (2 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coveralls

coveralls commented Sep 20, 2026

Copy link
Copy Markdown

Coverage Report for CI Build 35573237422

Coverage increased (+0.1%) to 50.417%

Details

  • Coverage increased (+0.1%) from the base build.
  • Patch coverage: 6 uncovered changes across 2 files (59 of 65 lines covered, 90.77%).
  • No coverage regressions found.

Uncovered Changes

File Changed Covered %
internal/store/postgres/resource_repository.go 36 32 88.89%
core/resource/service.go 23 21 91.3%
Total (4 files) 65 59 90.77%

Coverage Regressions

No coverage regressions found.


Coverage Stats

Coverage Status
Relevant Lines: 41173
Covered Lines: 20758
Line Coverage: 50.42%
Coverage Strength: 16.19 hits per line

💛 - Coveralls

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

Actionable comments posted: 1


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository: raystack/frontier/.coderabbit.yaml

Review profile: CHILL

Plan: Advanced

Run ID: 80fa62af-722a-46c0-a014-7eada53e34b6

📥 Commits

Reviewing files that changed from the base of the PR and between f840f22 and 0cd9a44.

📒 Files selected for processing (8)
  • core/resource/service.go
  • core/resource/service_test.go
  • internal/store/postgres/migrations/20260918100000_resources_urn_live_unique.down.sql
  • internal/store/postgres/migrations/20260918100000_resources_urn_live_unique.up.sql
  • internal/store/postgres/postgres.go
  • internal/store/postgres/resource_repository.go
  • internal/store/postgres/resource_repository_test.go
  • pkg/auditrecord/consts.go

Included review availability: Your plan provides up to 2 included reviews per hour; 1 remains after this review.

@AmanGIT07

Copy link
Copy Markdown
Contributor Author

End-to-end run of the resource RPCs on this branch (commit 0cd9a44), against a local Frontier with Postgres 15 and SpiceDB 1.34 in Docker. Calls were made as a super admin, with a regular user as the resource owner. 31 checks, all passed.

Scenario Result
Create, then get and list 200. Row is live. Owner has access in SpiceDB. resource.created audit row.
Update 200. Title saved. updated_at changes.
Delete 200. deleted_at set and row kept. Owner access gone in SpiceDB. resource.deleted audit row, with the title as target name.
Get after delete 404
List after delete Not listed
Delete again 404
Update after delete 403, see note
Re-create with the same URN 200. New id. Table holds one live and one deleted row for the URN.
Create with a deleted row's id 409
Create the same URN while live 200. Same id, title updated. The upsert's conflict target matches the partial index.

Note on the 403: the authorization rule for UpdateProjectResource checks SpiceDB with the id from the request before the handler runs, so a deleted id gets 403. Get and delete load the row first and return 404. An id that never existed also gets 403, so this is not changed by this PR.

…r lists

The project delete cascade removes the project row for good, and a resource
row cannot outlive the project it points to, so the cascade purges resources
instead of soft-deleting them until project delete is soft. The resource list
is ordered by created_at so its order no longer depends on the query plan.

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

Actionable comments posted: 1


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository: raystack/frontier/.coderabbit.yaml

Review profile: CHILL

Plan: Advanced

Run ID: c20f423b-c9fb-41af-9fa2-9febe9d49f43

📥 Commits

Reviewing files that changed from the base of the PR and between 0cd9a44 and 407c1ee.

📒 Files selected for processing (9)
  • core/deleter/mocks/resource_service.go
  • core/deleter/service.go
  • core/deleter/service_test.go
  • core/resource/mocks/repository.go
  • core/resource/resource.go
  • core/resource/service.go
  • core/resource/service_test.go
  • internal/store/postgres/resource_repository.go
  • internal/store/postgres/resource_repository_test.go

Included review availability: Your plan provides up to 2 included reviews per hour; 1 remains after this review.

Comment thread internal/store/postgres/resource_repository.go Outdated
The project cleanup listed only live resources, so a resource deleted through
the API kept pointing at the project and the project row could not be removed.
The cleanup now lists deleted rows as well and purges them. A new e2e case
covers the soft delete of a resource and the org delete that follows.
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