Data protection

Heardfix is built for apps that handle health data. This page lists what Heardfix stores, where, for how long, and what the operator of a back office must set up. It is technical guidance, not legal advice: your data protection officer validates the final setup and your privacy policy.

What is personal or health data

Data Kind Why
Reporter email Personal Identifies the reporter
Title, description, replies Health, potentially Free text: a user of a breathing app may mention a condition or a treatment
Images (screenshots, gallery photos) Health, potentially A screenshot can show session history or measurements
Logs and caught errors Personal, potentially Depends on what the app logs. See the Flutter guide. The web module sends the uncaught errors of the page, the addresses in them cut before their query string
Store review (text, rating, author pseudonym, country or language) Health, potentially, and already public Imported from the store, where anyone reads it. Treated like a report once in the back office
Public reply to a store review Public Written by a triager, who confirms it holds no health data before it leaves. It greets the reviewer by the pseudonym the store already shows
Installation id Personal (pseudonymous), never health Random UUID, but the back office links it to a report and an email. Sent to Sentry and Crashlytics. The web module sends a new one with each report
Page address (websites) Health, potentially Without query string nor fragment, but a path can name a treatment or a record (/treatments/insulin, /patients/123). The buttons send the real path. A site can send a route template instead, with pageUrl in Heardfix.open, see web.md. A page opened from a local file (desktop app) sends no address without pageUrl: its path may name the user of the computer
Device model, OS, browser, window size, locale, app version, battery, free memory and storage, app install, update and start dates Technical Not identifying alone. Personal once attached to a report. Sent with suggestions too, since a triager may turn a suggestion into a bug report
Sentry event ids, crashed previous run flag Technical Ids of events already held by Sentry
Sentry issue title and link Technical Read from Sentry with the organization's read only token. Only the links go to the task tracker
Access log Personal Names the triager, and the report or attachment they opened, sent to the task tracker or answered on a store
Topic title and description written by a triager Anonymised Written by the triager, never public
Public title and description of a topic, its tags Public Written by a triager for the users, shown on the roadmap
ClickUp task or Trello card (description, pictures) Personal, never health Report excerpts with masked emails and chosen pictures, cleaned of any health data by the triager under the review rule

Treat a whole report as health data. That is the simplest rule and the one Heardfix applies.

Where data lives

The public roadmap only shows the public texts, status, period and tags of the topics a triager made public. It sets no cookie and needs no consent.

The public form sends its reports to the back office only, like the web module, and loads nothing from a third party. It sets no cookie and needs no consent.

What never leaves the back office

Outgoing calls from the back office: ClickUp and Trello APIs (the task reviewed by a triager, with report excerpts, masked emails and ticked pictures, technical custom fields in ClickUp, the same technical data at the end of the Trello card description; the lists and boards the token reaches; for Trello, the webhooks of the boards of the apps), alert emails, Slack and Discord (app name, then the title, kind, time, system, browser, app version, or "Public form" with its browser and system, and image count of each new report, count of lowered store ratings, links), Firebase (App Check public keys), emails to reporters, the Google Play and App Store Connect APIs (reading reviews, publishing the public replies a triager reviewed), and Mistral when the instance has a Mistral key and the plan of the organization includes AI (title and description of visible reports with emails masked, titles of topics and of the proposals waiting for triage, the public title and description of the topic being closed with its kind, the name and description of the app and the example reply its team wrote). Mistral is not HDS certified: an instance storing health data leaves HEARDFIX_MISTRAL_KEY empty, see decisions.md.

Reporter emails and alerts go through your mail provider. Choose one whose servers and logs meet your hosting constraints, since an email names the bug, and the confirmation email quotes the title of the report.

On the device

Retention

Data Kept
Report (email, description, images, context, diagnostics, messages) 13 months after the report's last activity
Closing email record (topic, email) 13 months after the email, once the reporter has no report left in the topic. Deleted at once on erasure
Store reply record (topic, report) Deleted with its report
AI proposal, and AI miss log (what Mistral received and answered) Deleted with its report
AI usage line (organization, app, model, tokens, cost) and storage measure (organization, app, size) Without limit. They hold no personal data
Access log line 12 months
Reception log line (organization, report id, date, visible, store review or not) Without limit. It holds no personal data and counts the reports against the quota of the plan, see laravel.md
Refusal line (organization, date) Until the plan or its quota changes, and with the organization. It holds no personal data and no report, see laravel.md
Anonymised topic (texts written by triagers, status, period, tags, link to its task) Without limit
ClickUp task, Trello card Managed in the tracker, under the client's contract with ClickUp or Atlassian. Purging a report does not delete what was sent: delete the task or the card in the tracker when needed

Excerpts sent to the task tracker have no automatic retention period: neither the 13 month purge nor erasure covers them for a reporter without email. The rule is to be set with the client's data protection officer.

Last activity is the latest of: report received, message sent to the reporter or received from them, resolution email sent, store review edited by its author. Only the reviews of the last 7 days are imported, so a purged review never comes back unless its author edits it again.

Example: a report received on 3 September 2026, whose reporter is emailed when the bug closes on 20 October 2026, is purged on 20 November 2027. Without any email or reply, it would be purged on 3 October 2027.

A bug closed after its reports were purged emails nobody: the purged reporters are no longer known.

The purge runs daily (heardfix:purge, see laravel.md). It deletes the stored files with the database rows.

Erasure on request

A user may ask for their data to be deleted. In the back office, open a report of this user and run "Delete this reporter's data". It deletes every report and message sent with this email, across all the apps of the organization, files included. The same email in another organization's apps is another client's data, left untouched. It cannot be undone.

The action is logged: who ran it, when, how many reports were deleted. The log does not keep the email, so the erasure leaves no trace of the person.

Hidden reports, received beyond the quota of the plan, are erased too. The reception log keeps their lines, which hold neither the email nor any content.

Anonymised bugs stay, with their remaining reports.

The task trackers keep what was sent: report excerpts and pictures of this reporter may remain in the ClickUp tasks or Trello cards of their bugs. The back office cannot delete them. After the erasure, the notification lists the tasks of the bugs the reporter's reports belong to, and of the bugs whose task received one of them before it was removed from them: open each one and delete the reporter's excerpts and pictures by hand, to complete the erasure in the tracker too (GDPR articles 17 and 19).

Store reviews have no email either: the author deletes their review on the store, and the 13 month purge deletes the report. A public reply stays on the store until it is deleted in the store console.

A triager can also delete selected reports from the list, such as test or spam reports: the same deletion of files and messages, logged with the count. Store reviews are left out of it, so the import does not bring them back. The task trackers keep what was already sent.

Reports sent without an email cannot be erased by reporter. The back office only links them to the installation id, shown to the user in the form's "Attached information" block. The prototype has no erasure by installation id: the 13 month purge deletes them. A report from a website carries an id of its own, never shown to the visitor: without an email, only the purge deletes it.

Access log

Backups

Purge and erasure act on the live database and the attachment disk. Host backups keep the deleted data until their rotation removes it (for example 30 days).

State this period in your privacy policy, for example: "Deleted data remains in encrypted backups for up to 30 days, then is destroyed." Make sure backups are stored on the same certified host as the back office.

Sentry and Crashlytics

Sentry and Crashlytics are outside the health data boundary. Send them an allowlist of data, never a blocklist: new fields stay out until someone adds them on purpose.

Recommended Sentry setup for Flutter:

const allowedBreadcrumbCategories = {'navigation', 'http', 'app.lifecycle'};

await SentryFlutter.init((options) {
  options.dsn = sentryDsn;
  options.sendDefaultPii = false;
  options.replay.sessionSampleRate = 0;
  options.replay.onErrorSampleRate = 0;
  options.attachScreenshot = false;
  options.attachViewHierarchy = false;
  options.beforeBreadcrumb = (breadcrumb, hint) =>
      allowedBreadcrumbCategories.contains(breadcrumb?.category) ? breadcrumb : null;
  options.beforeSend = (event, hint) {
    Heardfix.maybeInstance?.recordSentryEventId(event.eventId.toString());
    event.user = SentryUser(id: event.user?.id);
    return event;
  };
});

Rules:

Crashlytics: set only the user identifier (installation id). Do not use setCustomKey or log with user data.

On the server side, enable Sentry's server side data scrubbing as a second line of defence. It does not replace the allowlist in the app.