Problem
Deleting a signal today is a hard delete, performed by the client: _confirmDeleteSignal in lib/src/widgets/signal_details_screen.dart best-effort deletes the Storage photos, batch-deletes the comments subcollection, then deletes the signal document. Nothing survives.
That is at odds with where the case system is heading (Master Specification §4.5 case ownership history, §4.6 case timeline, §18.7 audit and oversight), and it causes concrete problems now:
- The signal history is destroyed with the signal. The new
events subcollection (signal timeline: status/urgency changes with mandatory update notes) is deleted along with everything else, so there is no record that a signal ever existed, or of who changed what.
- The reporter can delete individual history entries. Firestore rules allow a comment/event delete by the parent signal's reporter — required for the delete cascade to work at all from the client. An audit trail its own subject can prune is not an audit trail.
- The cascade is client-side and best-effort. If the app is killed mid-delete, subcollection documents are orphaned (Firestore keeps subcollection docs when the parent document is deleted) and nothing ever cleans them up.
- Moderation has nowhere to go. §18.3 wants "hide post" as a moderator action distinct from deletion, and §4.10 wants expiry/archiving with reduced-quality media rather than erasure.
Idea to explore
Replace hard delete with hiding / soft delete:
- A
hidden (or deletedAt + hiddenBy) field on the signal, with rules excluding hidden signals from client queries, so the document and its history survive.
- Move the actual destructive path server-side into a
deleteSignal callable using an Admin SDK recursive delete, mirroring the existing deleteAccount callable. With deletion no longer client-driven, events can become allow delete: if false and the tamper hole closes.
- Decide the retention story alongside §4.10 (archive after ~6 months, media deleted after 6–12 months) and the GDPR erasure path — soft delete must not become a way to retain personal data indefinitely.
Questions to settle
- Who may hide: reporter only, or reporter + moderator/admin once those roles exist?
- Is a hidden signal still reachable by direct link / share link, or 404?
- Do profile statistics keep counting a hidden case? (§3.5.1 says stats survive case deletion.)
- What actually gets purged, and when — photos first, document later?
- Does the reporter still get a way to truly erase their own content, for GDPR?
Context
Came out of the signal-timeline work (Master Spec §4.6, "Case Timeline"), which introduces the signals/{id}/events subcollection. That change deliberately keeps allow delete: if isParentSignalReporter(...) on events so the existing cascade keeps working, and records the tamper hole as a known gap rather than fixing it — this issue is that fix.
Problem
Deleting a signal today is a hard delete, performed by the client:
_confirmDeleteSignalinlib/src/widgets/signal_details_screen.dartbest-effort deletes the Storage photos, batch-deletes thecommentssubcollection, then deletes the signal document. Nothing survives.That is at odds with where the case system is heading (Master Specification §4.5 case ownership history, §4.6 case timeline, §18.7 audit and oversight), and it causes concrete problems now:
eventssubcollection (signal timeline: status/urgency changes with mandatory update notes) is deleted along with everything else, so there is no record that a signal ever existed, or of who changed what.Idea to explore
Replace hard delete with hiding / soft delete:
hidden(ordeletedAt+hiddenBy) field on the signal, with rules excluding hidden signals from client queries, so the document and its history survive.deleteSignalcallable using an Admin SDK recursive delete, mirroring the existingdeleteAccountcallable. With deletion no longer client-driven,eventscan becomeallow delete: if falseand the tamper hole closes.Questions to settle
Context
Came out of the signal-timeline work (Master Spec §4.6, "Case Timeline"), which introduces the
signals/{id}/eventssubcollection. That change deliberately keepsallow delete: if isParentSignalReporter(...)oneventsso the existing cascade keeps working, and records the tamper hole as a known gap rather than fixing it — this issue is that fix.