Skip to content

feat(domain): soft-delete domains and write a domain.deleted audit record - #1960

Merged
AmanGIT07 merged 3 commits into
mainfrom
soft-delete-domains
Sep 30, 2026
Merged

AmanGIT07 merged 3 commits into
mainfrom
soft-delete-domains

Conversation

@AmanGIT07

Copy link
Copy Markdown
Contributor

Summary

DeleteOrganizationDomain now sets deleted_at instead of removing the row, and each delete writes a domain.deleted audit record. Callers get the same responses as before.

Changes

  • DomainRepository.Delete uses softDelete (feat(store): add a helper that marks rows as deleted #1959), so the row stays with deleted_at set. A domain that is already deleted reports not found.
  • The daily cleanup of expired pending requests, DeleteExpiredDomainRequests, skips deleted rows. It still removes old pending domains that nobody deleted.
  • domain.Service.Delete reads the domain and its organization, marks the domain deleted, then writes the audit record. The organization is read with GetRaw, so a superuser can still delete a domain of a disabled organization.
  • The record has the organization as its resource and the domain as its target, with the event domain.deleted and the new domain entity type. The actor comes from the request. A failed record write is logged and does not fail the delete.
  • The OrgService mock gains GetRaw, and AuditRecordRepository has a new mock.

Technical Details

Reads already skip deleted domains, so get, list, verify, join, and the joinable organizations list treat a deleted domain as gone.

Domain names are unique among live rows only (uq_domains_org_id_name_live), so a deleted name can be added again.

Organization delete is still a hard delete. The domains.org_id foreign key is ON DELETE CASCADE, so it also removes the organization's deleted domain rows.

Test Plan

  • go test ./internal/store/postgres/ passes. New cases: the row stays after a delete, a second delete reports not found, the same name can be added again, and the cleanup keeps deleted rows.
  • go test ./core/domain/ passes. TestService_Delete checks the full audit record, and that no record is written when the domain is unknown, the organization lookup fails, or the delete fails.
  • New e2e case in TestOrganizationDomainsAPI: delete, get returns not found, the same name can be created again, exactly one domain.deleted record with the caller as its actor, and the organization delete still works.
  • Each new test fails when the code it covers is reverted.
  • golangci-lint run reports no issues.
  • End to end against a local server. The same checks ran against main on the same database, with only the binary swapped.
Check main this branch
Delete the domain ok ok
Get the deleted domain not found not found
List includes the deleted domain no no
Verify the deleted domain not found not found
Delete it again not found not found
Database row of the deleted domain gone kept, deleted_at set
Audit records for the deleted domain none one domain.deleted, all fields match, actor is the caller
Create the same name again ok, new id ok, new id
Before the delete, a user with a matching email sees the organization as joinable yes yes
Before the delete, a user with a matching email joins ok ok
After the delete, the organization is joinable no no
After the delete, a user with a matching email joins invalid argument, domains do not match invalid argument, domains do not match
Delete the organization while it holds a live and a deleted domain ok, no domain rows left ok, no domain rows left
Cleanup job, old live pending domain removed removed
Cleanup job, old deleted pending domain gone kept
Cleanup job, new pending domain kept kept

The joinable checks mark the domain verified in the database, because DNS verification cannot run locally. The cleanup rows run the function the daily job calls. On main the deleted row is removed by the delete itself, before the cleanup runs.

SQL Safety

  • Values flow through ? placeholders, goqu.Ex{}, or goqu.Record{} — never fmt.Sprintf or + building a query that gets executed.
  • ToSQL() callers capture and forward params (query, params, err := stmt.ToSQL(); db.…Context(ctx, …, query, params...)). Never query, _, err := ….
  • No ? placeholders inside single-quoted SQL literals in goqu.L.
  • No new //nolint:forbidigo or // #nosec G20x annotations.

… deleted domains

Delete sets deleted_at through softDelete instead of removing the row, and a
domain that is already deleted reports not found. The daily cleanup of expired
pending requests skips deleted rows, so it no longer removes them.
The service reads the domain and its organization, marks the domain deleted,
then records the delete with the organization as the resource and the domain as
the target. A failed record write is logged and does not fail the delete.
@vercel

vercel Bot commented Sep 29, 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 29, 2026 9:44am UTC

@coderabbitai

coderabbitai Bot commented Sep 29, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their 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: ccc823ca-d9cd-4ff6-9afd-569f9bd9649e

📥 Commits

Reviewing files that changed from the base of the PR and between 58583d2 and fe8b7d6.

📒 Files selected for processing (9)
  • cmd/serve.go
  • core/domain/mocks/audit_record_repository.go
  • core/domain/mocks/org_service.go
  • core/domain/service.go
  • core/domain/service_test.go
  • internal/store/postgres/domain_repository.go
  • internal/store/postgres/domain_repository_test.go
  • pkg/auditrecord/consts.go
  • test/e2e/regression/api_test.go

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


📝 Summary

Summary by CodeRabbit

  • New Features
    • Domain deletions now generate an audit record with details about the domain, organization, and actor. If recording the audit event fails, the deletion still succeeds.
  • Bug Fixes
    • Deleted domains are no longer permanently removed, but they are unavailable through domain lookups and cannot be deleted again. Their names can be reused for new domains.
    • Expired domain requests are cleaned up without affecting deleted domains or fresh requests.

Walkthrough

Domain deletion now uses soft deletion in PostgreSQL and records a domain.deleted audit entry after a successful delete. Service, repository, and API regression tests cover deletion results, audit details, and domain-name reuse.

Changes

Domain deletion

Layer / File(s) Summary
Audit contract and service behavior
pkg/auditrecord/consts.go, core/domain/service.go, cmd/serve.go, core/domain/mocks/*, core/domain/service_test.go
The service receives an audit-record repository, loads the domain and organization before deletion, and writes a domain.deleted audit record after a successful delete. Audit-write failures are logged and do not fail deletion. Mocks and service tests cover the new dependency and behavior.
PostgreSQL soft deletion
internal/store/postgres/domain_repository.go, internal/store/postgres/domain_repository_test.go
Domain deletion now marks records as deleted. Expired-request cleanup excludes soft-deleted domains. Repository tests cover these behaviors and domain-name reuse.
API regression coverage
test/e2e/regression/api_test.go
The regression test checks deletion responses, domain-name reuse, and the deletion audit record’s fields.

Priority: ⬇️ Low

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

Change: Feature

Suggested reviewers: whoabhisheksah

Merge Risk: ⚪ Minimal · up to fe8b7

No actionable merge-blocking risk remains after normal checks.

Security Architecture Review

Security architecture risk: 🟠 High · up to fe8b7

A code-only rollback could make deleted verified domains usable for organization joining again. Deletions can also succeed without a durable audit record if the subsequent audit write fails.

Retained concerns

  • High · security · inferred: A code-only rollback after soft deletions would restore reads of deleted verified domains; the existing domain-based organization-joining flow consumes those reads.
  • Medium · security · observed: A successful soft-delete can remain durable without its domain.deleted audit record after an audit-write failure or interruption between the two operations.
Security review details

Security Blast Radius

  • inferred — The rollback exposure applies to organizations with verified domain rows deleted while this version runs; the membership flow lists verified domains and can add a matching-email user as an organization viewer.

Security Findings and Attack Paths

  • inferred — If application code is rolled back without addressing retained deleted rows, a user with an email matching a formerly verified domain could again satisfy the existing domain-based membership condition. No current-version path through the inspected repository List has that behavior.

Trust Boundaries and Controls

  • observed — The public delete interceptor checks the domain's organization identity and organization UpdatePermission; authorization also checks the target organization activity state before the handler calls the service. GetRaw in the service does not itself grant an untrusted API caller a bypass of those checks.

Resilience and Maintainability Implications

  • observed — An audit insert error is logged but not returned, and a retry after the completed soft-delete encounters not-found rather than replaying the missing audit write.

Hardening Proposals

  • proposed — Define a rollback procedure that preserves live-row filtering or reconciles soft-deleted rows before older application queries are deployed.
  • proposed — If a deletion audit must be durable, coordinate it with the state change through a transaction or a recoverable outbox, and specify how incomplete audit writes are detected and retried.
🚥 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

Copy link
Copy Markdown

Coverage Report for CI Build 36551101108

Coverage increased (+0.1%) to 52.89%

Details

  • Coverage increased (+0.1%) from the base build.
  • Patch coverage: 1 uncovered change across 1 file (44 of 45 lines covered, 97.78%).
  • No coverage regressions found.

Uncovered Changes

File Changed Covered %
cmd/serve.go 1 0 0.0%
Total (3 files) 45 44 97.78%

Coverage Regressions

No coverage regressions found.


Coverage Stats

Coverage Status
Relevant Lines: 41303
Covered Lines: 21845
Line Coverage: 52.89%
Coverage Strength: 17.0 hits per line

💛 - Coveralls

@AmanGIT07
AmanGIT07 merged commit 5b74374 into main Sep 30, 2026
8 checks passed
@AmanGIT07
AmanGIT07 deleted the soft-delete-domains branch September 30, 2026 08:57

This branch was successfully deployed

1 active deployment
Preview — fe8b7d67 Deployed Sep 29, 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