Skip to content

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

Merged
AmanGIT07 merged 6 commits into
mainfrom
soft-delete-resources
Sep 30, 2026
Merged

AmanGIT07 merged 6 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.
  • The resource repository's Delete only touches live rows, so a second delete returns not-found. Reads and updates already skip deleted rows on main.
  • 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.
  • Unit test for liveConflictTarget asserting the exact ON CONFLICT (urn) WHERE (deleted_at IS NULL) clause, and the two-column form.
  • 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 ON CONFLICT target is built with + in liveConflictTarget from a constant column list, never from caller input, and a unit test pins the exact clause it produces. now() is a constant.
  • ToSQL() params are forwarded unchanged.
  • No ? placeholders inside quoted SQL literals.
  • No new //nolint or #nosec annotations.

@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 30, 2026 6:31am UTC

@coderabbitai

coderabbitai Bot commented Sep 20, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

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: 2b0bc30f-48fb-42f5-afb5-aa06bdce0e11

📥 Commits

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

📒 Files selected for processing (2)
  • internal/store/postgres/resource_repository.go
  • internal/store/postgres/resource_repository_test.go

Included review availability: This review used your included allowance. Your plan provides up to 2 included reviews per hour; 1 remain after this review.


📝 Summary

Summary by CodeRabbit

  • New Features

    • Deleted resources are retained for audit purposes while remaining hidden from standard retrieval and listings.
    • Resource deletion activity is recorded in the audit history.
    • Previously deleted resource identifiers can be reused for new resources, which receive new IDs.
    • Permanently deleting a project also permanently removes its resources, including previously deleted ones.
    • Administrative listings can include deleted resources.
  • Bug Fixes

    • Prevented duplicate identifiers among active resources and improved conflict handling.
    • Deleting a resource now reports when no active resource exists.

Walkthrough

Resource deletion now soft-deletes resources and records an audit event after successful service deletion. Repository queries exclude deleted resources by default and allow live resources to reuse deleted URNs. Project cleanup permanently purges both live and deleted resources.

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 enforces URN uniqueness only for live resources. Repository reads exclude soft-deleted rows by default. 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 relations, soft-deletes the resource, and records ResourceDeletedEvent after success. Service.Purge removes relations 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: ⬇️ Low

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

Change: Feature

Suggested reviewers: whoabhisheksah

Merge Risk: 🟡 Moderate · up to fe031

Resources can be deleted and recreated under the same URN, but that state blocks migration rollback. Resolve or explicitly accept this rollback limitation before merging.

Security Architecture Review

Security architecture risk: 🟡 Moderate · up to fe031

Resource creation and deletion now depend on a coordinated database and application rollout. An incompatible deployment order can interrupt resource creation, and reusing deleted resources’ identifiers can prevent a straightforward database rollback.

Retained concerns

  • Medium · reliability · inferred: The migration and resource writers require coordinated activation: after the table-wide constraint is dropped, an old writer’s ON CONFLICT (urn) has no matching unconditional unique constraint, interrupting resource creation while old instances remain active.
  • Medium · reliability · inferred: Once a deleted resource’s URN is reused by a live row, the down migration cannot restore table-wide URN uniqueness. Database rollback then requires a decision about the retained history rather than simply reversing the migration.
Security review details

Security Blast Radius

  • inferred — A mixed-version schema rollout can affect resource creation across instances still running the old writer. The inspected code does not expose the permanent purge operation through the ordinary resource API.

Trust Boundaries and Controls

  • observed — An authorization check exists separately from the inspected resource handlers. Its attachment to affected public RPCs was not established, so the inspected handler code alone cannot prove authorization coverage or a new bypass.

Resilience and Maintainability Implications

  • inferred — SpiceDB relation removal and the Postgres deletion transition are separate operations. A failure between them can leave a row and its relation temporarily divergent; the inspected source does not establish a cross-store recovery mechanism or that this ordering was introduced by this PR.

Hardening Proposals

  • proposed — Define a deployment gate that prevents incompatible old writers from running after the constraint change, and specify how rollback handles URNs already reused by live resources.
🚥 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.
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Commit to this branch
  • Create a new PR

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 36678992556

Coverage increased (+0.08%) to 52.86%

Details

  • Coverage increased (+0.08%) from the base build.
  • Patch coverage: 6 uncovered changes across 2 files (48 of 54 lines covered, 88.89%).
  • No coverage regressions found.

Uncovered Changes

File Changed Covered %
internal/store/postgres/resource_repository.go 25 21 84.0%
core/resource/service.go 23 21 91.3%
Total (4 files) 54 48 88.89%

Coverage Regressions

No coverage regressions found.


Coverage Stats

Coverage Status
Relevant Lines: 41315
Covered Lines: 21839
Line Coverage: 52.86%
Coverage Strength: 17.03 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.

@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
Comment thread internal/store/postgres/postgres.go
…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.
…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.
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.
…n main

The rebase onto main brought in a second TestSkipsSoftDeletedResources from
the read-filter change. Keep that one and add the IncludeDeleted list check
to it.
…Delete

A unit test asserts the exact ON CONFLICT clause the helper produces, for a
one-column and a two-column target, so a goqu change fails there instead of
at runtime. The resource delete now uses the shared softDelete helper.
@AmanGIT07
AmanGIT07 force-pushed the soft-delete-resources branch from fe03156 to 52530f1 Compare September 30, 2026 06:30
@AmanGIT07
AmanGIT07 enabled auto-merge (squash) September 30, 2026 06:32
@AmanGIT07
AmanGIT07 merged commit f09054e into main Sep 30, 2026
8 checks passed
@AmanGIT07
AmanGIT07 deleted the soft-delete-resources branch September 30, 2026 06:41

This branch was successfully deployed

1 active deployment
Preview — 52530f13 Deployed Sep 30, 2026 by vercel[bot]
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