Skip to content

Implement soft delete (recycle bin): enable.trash / softDelete are spec-only with zero runtime readers #3146

Open

Description

@baozhoutao

Current state

The spec declares soft-delete capability in two places, but neither has any behavior-changing reader — every delete today is a physical hard delete at the driver level.

  • enable.trash (default true, described as "Enable soft-delete with restore capability") — packages/spec/src/data/object.zod.ts (~L110)
  • SoftDeleteConfigSchema (enabled / field: 'deleted_at' / cascadeDelete) — packages/spec/src/data/object.zod.ts (~L288)
  • The permission model also reserves a Restore (soft-delete recovery) semantic (packages/spec/src/security/permission.zod.ts)

The June 2026 liveness audit (docs/audits/2026-06-objectschema-property-liveness.md) confirms:

trash is one of the 10 flags with zero behavior-changing readers in either repo; softDelete ↔ enable.trash — duplicated, both dead.

The default trash: true is actively misleading: schema authors reasonably assume deleted records are recoverable, but they are gone.

Note: deleted_at on some identity objects (e.g. sys_user) is a better-auth convention local to those objects, not a platform mechanism.

Proposal

Implement platform-level soft delete honoring the existing spec surface:

  1. Delete path: when soft delete is enabled for an object, delete stamps the configured timestamp field (default deleted_at) instead of removing the row.
  2. Query path: default-scope all reads to exclude soft-deleted rows; provide an explicit opt-in to include/only-trashed (recycle-bin views).
  3. Restore: a restore operation clearing the marker, gated by the reserved Restore permission.
  4. Purge: a real (physical) delete for emptying the trash, plus optional retention/TTL via the ADR-0057 LifecycleService.
  5. Cascade: honor cascadeDelete for related records (or explicitly descope it for v1).
  6. Spec reconciliation: collapse the duplicated enable.trash vs softDelete surface into one canonical config (and fix the misleading trash: true default until the runtime actually enforces it).

Open questions: interaction with unique indexes (soft-deleted rows still occupy unique keys), lookup integrity when a target is trashed, and REST/console surface for the recycle bin.

Activity

  1. self-assigned this
    on Jul 17, 2026
  2. added
    status:parkedParked — exempt from the PM sweep and half-state patrol until its owner migrates it (e.g. cloud PM)
    on Jul 18, 2026
  3. added
    domain:engineRetired 2026-10-22, alias of domain:server (engine surface); in-flight cards keep it until they land
    and removed on Aug 19, 2026
  4. github-actions commented on Oct 19, 2026

    @github-actions
    Contributor

    This issue has been automatically marked as stale because it has not had recent activity. It will be closed in 14 days if no further activity occurs. Thank you for your contributions.

  5. added
    domain:serverDomain lane — seat card indexed by label:pm:seat
    and removed
    domain:engineRetired 2026-10-22, alias of domain:server (engine surface); in-flight cards keep it until they land
    on Oct 23, 2026
  6. objectstack-fleet commented on Nov 4, 2026

    @objectstack-fleet
    Contributor

    coordinator#1 · hotlong · session_6eb9ec2e0e3f410b91239dab7a337b9e

    Ruling · 2026-11-04T11:50Z · maintainer, in session (cloud coordinator thread): direction for this card, and unparked.

    Maintainer's words, in order:

    • 「记录删除回收站的功能还没有开发」
    • 「其他主流类似系统是怎么处理的」
    • 「保留期按『Free 7 天 / 团队版及以上 30 天』开源项目怎么办呢?这个功能可以放在企业版吗?建议放在企业版吗?」
    • on the proposal below: 「同意,企业版功能在 cloud 中立卡后续开发」

    Why now: cloud#3565 rules that the AI chat may write records after a confirm card (Salesforce Agentforce as the reference). Its step 3, deleting from chat, is blocked until deletes are recoverable. The console's delete is permanent today.

    What is ruled

    1. Shape: a separate recycle-bin store, not a deleted_at column on every table. On delete, the engine snapshots the rows the delete removes (the cascade set from engine-delete-cascade.ts) into one system object, for example sys_recycle_bin, as one entry per delete operation, and then deletes physically as today. Restore writes the rows back under their original ids. Purge removes the entry. Every read path stays untouched: query, aggregate, REST, the analytics engine's SQL, rollups, lookup pickers, search, AI tools, MCP and export. Unique keys free immediately, and the per-tenant schema change is one table, not a column on every object. This is how Dataverse (a "bin" data source, restored with the same record id) and ServiceNow (sys_audit_delete) do it. Salesforce's IsDeleted column is the other shape, and it is not chosen here.
    2. Open source owns the mechanism. It covers snapshot, restore, empty, permanent delete, TTL purge, and permissions: a person restores what they deleted, and an admin sees all. Retention is configurable with a default of 30 days, for example OS_RECYCLE_BIN_RETENTION_DAYS. The framework knows days, never plans (ADR-0012). A host may inject retention through a resolver hook, the same pattern as resolveFeatures.
    3. Cloud sets per-plan retention through that hook: Free 7 days, Team and above 30 days. The enterprise value layer goes in a cloud card: per-object retention, legal hold, longer retention, delete and restore audit reports, bulk restore, and erasure proof. It is not in this card and not in open source.

    Proposed v1 scope, adopting mainstream rules rather than inventing new ones

    • In:
      • business objects (not sys_*);
      • single delete and its master-detail cascade, restored together; a cascaded child cannot be restored on its own (Dataverse);
      • an admin recycle-bin page (list, restore, empty);
      • TTL purge on the existing LifecycleService, following the sys_file tombstone pattern (attachment-lifecycle.ts, 30-day reap);
      • a 「已删除 · 撤销」 toast;
      • restore keeps created_by / created_at (ServiceNow's script-restore pitfall);
      • permanent delete stays available for privacy erasure and never enters the bin (HubSpot).
    • Restore refuses with a reason, and never forces a write, when:
      • a unique value has since been taken;
      • the parent was purged;
      • the object's package was uninstalled.
    • Lookups: a reference that set_null cleared is re-linked only if nothing has changed it since (Salesforce: 「only restores lookup relationships that have not been replaced」). Flow enrollments are not restored (HubSpot). A restore emits its own 「restored」 event, never a create, so record-after-create flows do not fire, and rollups recompute.
    • Forward-only: deletes made before the bin existed are not recoverable (Dataverse). Release notes say so.
    • Later: bulk delete into the bin with a size cap; holding attachments and sys_secret rows until purge (today a delete releases sys_secret, objectql: deleting a record does not release the sys_secret row its secret field names #27581); member-facing 「我删除的」.

    Spec and ADR

    enable.trash, softDelete and allowRestore / allowPurge were removed in 16.0 under ADR-0049 (enforce-or-remove), because nothing read them. This card brings back a new surface: new keys, not the tombstoned ones. It needs an ADR, or an ADR-0049 amendment, naming the readers that will exist.

    Risks to plan for

    • Restore fidelity: references, cascades, files, approvals pending on the record. Mitigation: a narrow v1 that refuses the rest, with a test per refusal.
    • Delete latency: an extra read and write per delete, which hosted Turso shows. Mitigation: one snapshot write, and a cap on bulk.
    • Bin storage counts toward quota. Show it separately.
    • Privacy: snapshots hold every field. Apply FLS when listing; keep secret fields out of the snapshot or encrypt them; purge on organization or environment deletion.

    Labels: status:parked → removed; pm:queue, priority:p2.

    🤖 Generated with Claude Code

  7. added
    pm:queueReady for the PM dispatch loop
    and removed
    status:parkedParked — exempt from the PM sweep and half-state patrol until its owner migrates it (e.g. cloud PM)
    on Nov 4, 2026
  8. objectstack-fleet commented on Nov 4, 2026

    @objectstack-fleet
    Contributor

    triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U

    Path: none

    Triage: first-touch audit · p1 · domain:server kept · 2026-11-04T13:08Z

    ⛔ Not a claim.

    • Read fresh. The ruling of 2026-11-04T11:50Z unparked this card and set the shape: a separate recycle-bin store with one entry per delete, restore under the original ids, and configurable retention (default 30 days) through a host hook. The framework knows days, never plans.
    • It brings back a new spec surface with an ADR, a protocol addition, and it blocks step 3 of cloud#3565. p1 under 「协议新增定 p1」 (was p2).
    • The per-plan retention and the enterprise layer are cloud's own card, as ruled. This card is the open-source mechanism only.

    Generated by Claude Code

  9. added
    priority:p1High: required for production / M2
    and removed on Nov 4, 2026
  10. objectstack-fleet commented on Nov 5, 2026

    @objectstack-fleet
    Contributor

    triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U

    Path: none

    Triage: split by lane · this card becomes the tracking parent · 2026-11-05T02:55Z

    ⛔ Not a claim.


    Generated by Claude Code

  11. added
    trackingGenerated anchor/tracker issue — not dispatchable; exempt from the half-state sweep's findings
    and removed
    pm:queueReady for the PM dispatch loop
    on Nov 5, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    domain:serverDomain lane — seat card indexed by label:pm:seatenhancementNew feature or requestpriority:p1High: required for production / M2trackingGenerated anchor/tracker issue — not dispatchable; exempt from the half-state sweep's findings

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions