0029 — Product deletes archive; destruction is out of band

Decision

Gera, verbatim:

"only admins could ever quote unquote delete. But deleting data should actually never delete it, it should just mark it as archive so we don't see it. If we ever wanna delete it, it should be done with like claude code or something behind the scenes."

As three rules:

  1. Only admins may reach a "delete". Ordinary in-company acts open to every workspace member under ADR-0135 / ALIGN1; a delete does not. A delete is not "an otherwise valid in-scope settings write", so ALIGN1 does not retire its gate.
  2. A product "delete" never destroys. It sets an archive mark, the record and every child row stay exactly where they are, and the mark is reversible by clearing it.
  3. Real destruction is out of band — a script or hand-run action behind the scenes, never a product button. No product route hard-deletes the record this rule covers.

This restates an earlier ruling; it does not supersede one

Decision b89009726 / D-0909-14 (2026-09-09), also Gera, recorded verbatim in propflowai at src/lib/data/types.ts:9784-9786:

"we never truly delete, we just kinda mark the data as archived. So we should always like keep it. Just have an archived sort of way of describing it. I'm not sure what's like the common archiving strategy."

b90010259 is the same rule, applied to a second door, plus the answer to the mechanism question the earlier one left explicitly open. Nothing in b89009726 is reversed. A reader who finds only one of the two has the rule but not its reach; this ADR exists so neither is found alone.

The earlier ruling already has one implementation in the tree — the withdrawn unrouted-thread marker at types.ts:9784 — which its own docblock is careful to scope: "NOT A GENERAL 'ARCHIVED CONVERSATION' STATE." That caution stands. This ADR does not make it general either; it settles properties, names the idiom, and lists what is still owed.

What changed in code

propflowai p7-actor-view part 4b. POST /api/settings/ignored-properties with alsoDelete: true called deleteProperty(id) — queryByPK(PROP#{id}) followed by batchDelete over every item in the partition: the META row, every UNIT#, every occupancy, every appliance, every knowledge row, and every conversation, tour and work order born on that line.

It now writes one archivedAt + archivedBy stamp on the META row and removes nothing.

Consequences

Out-of-band destruction still has no tool. The ruling's "claude code or something behind the scenes" half is not built. Until it is, archived rows accumulate and are reachable only by direct table access. That is the intended trade — accumulation is recoverable, destruction is not.

The archived state has no UI. Nothing lists archived properties or offers un-archive; recovery today means clearing the attribute by hand.

The archive stops reads, not sends — and that is the next part. archivedAt has exactly ONE consumer, filterVisibleProperties, which is a list visibility predicate. So an archived building disappears from every screen while its conversations, work orders, tours and renewal / turnover / collections timers keep running: no outbound path consults the field. The cascade this replaced could not produce that state, because destroying the partition is what stopped those workflows. A building nobody can see that is still texting its residents is worse than either previous state, and closing it is the next part of this work, not a nicety.

The predicate a send path would consult is the same one, applied to the building a message is about rather than to a list: a send is refused when the Property it concerns carries archivedAt — read through the property the outbound path already resolves, never a second spelling of the rule. The surfaces that would have to ask: Clara's conversation reply paths, maintenance dispatch, tour confirmations and reminders, the renewal saga's timers, turnover and the collections walker.

Fan-out readers do not inherit the hiding. filterVisibleProperties hides the property. The ~25 queryAllPropertyPartitions call sites enumerate partitions through getAllPropertyIds (dynamo/helpers.ts:2348), which does not consult it — so an archived building's work orders and conversations can still surface in account-wide fan-outs. Naming it rather than fixing it: that predicate is also what cron, sync and Lambda paths read, and narrowing it would hide rows from legitimate background work. Its own slice, with its own ruling on whether background work should see archived buildings at all.

Other product-facing hard deletes — follow-ups, NOT built here

Found by git grep -nE "deleteItem\(|batchDelete\(|DeleteCommand" -- src/app/api plus a sweep for delete-shaped facade calls. Each needs its own read of whether rule 2 applies; this ADR does not pre-judge them.

Route Call Note
properties/[id]/route.ts:316 deleteProperty Same primitive, already refused in production (:292), so dev-only. Closing it makes rule 3 literally true.
conversations/[id]/route.ts:121 deleteConversation The cohort b89009726 was originally about.
leasing/prospects/[id]/route.ts:221 deleteProspect
activity-log/route.ts:267 deleteActivityLogEntry
maintenance-manuals/route.ts:330 deleteMaintenanceManual
email/ingestion-log/[id]/route.ts:85 deleteEmailIngestionRecord
admin/users/[id]/route.ts:297 deleteUser Admin surface.
admin/companies/route.ts:515 deleteOrganizationRow A create-rollback, not a user act — likely out of scope for rule 2.
admin/maintenance/work-orders/kill-all, admin/pipeline-test/delete-conversation, admin/spine-orphans/*, admin/smoke-results/live various Staff/bench tooling — arguably already "behind the scenes" in the ruling's sense.
mfa/disable/route.ts:82,108, mfa/enroll/verify/route.ts:103,156 DeleteCommand Credential material. Rule 2 should probably NOT extend here — retaining a revoked MFA factor is a security liability, not a recovery aid. Named so the exception is deliberate.

The send-side gate — the next part, not built here

Surface What it would ask
Clara's conversation reply paths the Property the thread is about carries archivedAt → do not send
Maintenance dispatch same, on the work order's property
Tour confirmations and reminders same, on the tour's property
Renewal saga timers same, at the timer's wake
Turnover, collections walker same, at each step

One question, asked of the building the message concerns — not a second copy of the visibility rule.

The rule is about records a customer can lose, not about every delete in the tree. A reader applying rule 2 mechanically to the MFA rows would make the product less safe, which is why the table carries a verdict column rather than a checklist.