Privacy Policy — DRAFT v0
Status: DRAFT. Must be reviewed by a qualified lawyer before any user other than the owner accesses the production environment. Health data is a "special category" under GDPR Art. 9.
Last updated: 2026-10-09.
1. Data controller
The operator of my-medical-data is the data controller for all personal data processed by the Service.
Contact: the support address shown in the app's footer and on the Terms page. It is also where requests to exercise the rights in Section 9 can be sent; the operator answers within one month (Art. 12(3) GDPR).
2. Categories of data we process
- Account data: email address, hashed password (handled by Keycloak), MFA secret, social-login identifier (if used), Terms acceptance record.
- Special category — health data (GDPR Art. 9): clinical reports, lab results, vital signs, fitness/wearable measurements, sleep data, continuous glucose readings, medications.
- Technical data: IP address, browser user-agent, audit log of actions.
- Sharing data: when the User shares records with someone else, that person's email address, and the scope and expiry the User chose for the share (Section 8).
- Waitlist data: when someone who has no account joins the waitlist on the home page, their email address, an optional one-line note on what they would use the Service for, and when they consented and confirmed (Waitlist).
- Phones signed in to the phone app: for each phone, its platform (iOS or Android), the app version, when it was registered and last checked in, and the push token Apple or Google issued for it, which is what a notification is addressed to (Section 7).
- Billing data, for a User who subscribes to a paid plan: the identifiers Stripe gives the User's customer record and subscription, the subscription's status (for example active, or a payment being retried), which plan it is for, when the current period ends and whether it was cancelled. The card, the billing address and the invoices are held by Stripe, not by the Service (Section 7).
- A health-data export the User uploads for import (e.g. an Apple Health export): held only in a temporary, per-User working file while it is parsed and shown back to the User, never stored anywhere else unless and until the User commits it into their account (Section 8).
3. Legal basis
- For account data: Art. 6(1)(b) — necessary for performance of the contract (the Service).
- For health data: Art. 9(2)(a) — explicit consent given by the User at sign-up and at the time of each provider connection.
- For waitlist data: Art. 6(1)(a) — the consent given by ticking the waitlist form's box and confirming the address from the email.
4. Purposes
The data is used only to:
- Authenticate the User
- Display the User's own data back to them
- Compute charts and trends from the User's own data
- Maintain a security audit log
- Send a share link, and the one-time code used to open it, to someone the User chooses to share records with
- Parse a health-data file the User uploads for import and show it back to the User before they choose to save any of it
- Tell someone on the waitlist when they can sign up
We do not profile, sell, share, or use User data for any other purpose.
5. Where the data is stored
All personal data is stored in data centres located in the European Union:
prodenvironment: Hetzner Online GmbH, Falkenstein, Germany.testenvironment: Hetzner Online GmbH, Falkenstein, Germany.- Encrypted backups of the
prodenvironment: Scaleway SAS, Paris, France.
6. Data residency
Both environments run on Hetzner Online GmbH, a German company operating German data centres. Off-VM backups are held by Scaleway SAS, a French company operating French data centres. Both are EU-incorporated and EU-operated, so no transfer to a third country takes place and no Standard Contractual Clauses are required for this processing.
Earlier versions of this policy named Oracle Cloud Infrastructure for the test environment. That has not been accurate since May 2026.
7. Sharing with third parties
We do not sell, rent, or trade personal data, and we do not disclose it for anyone else's purposes.
The third parties below are nevertheless involved in running the Service:
Infrastructure providers. Hetzner Online GmbH hosts the servers and storage. Scaleway SAS stores off-VM backup copies. Brevo delivers transactional email — address verification, password reset, and, when a User shares records with someone (Section 8), the link sent at creation and the one-time code sent when the recipient opens it, and the confirmation email for the waitlist — and receives email addresses only, never health data or the shared records themselves.
Backups are encrypted before they leave our infrastructure. Backup copies held by Scaleway are encrypted with age using a public key, and the corresponding private key is not present on any of our servers and is not held by Scaleway. Scaleway therefore stores data it has no means of reading.
Push notification services. When the User signs in to the phone app, it registers the phone so the Service can tell it that something is ready (a report was read, for example). The notice goes through Apple Push Notification service (APNs, Apple Inc.) for an iPhone, or Firebase Cloud Messaging (FCM, Google LLC) for an Android phone, each acting as a processor. They receive the phone's push token and a notice made of a type and an identifier (such as report.ready and a document id). A notice never contains health data, names or text: the phone app fetches what the notice refers to from the Service itself, signed in. These services may process this data outside the EEA; Apple's and Google's transfers rely on the European Commission's adequacy decision for the EU–US Data Privacy Framework and on standard contractual clauses.
Payments. A User who subscribes to a paid plan pays through Stripe (Stripe Payments Europe, Ltd., Ireland), which processes the payment as an independent controller of the payment data under its own privacy policy, and as our processor for the subscription it manages on our behalf. Stripe receives the User's email address, billing address and card details (entered on Stripe's own checkout page, never on ours), and the plan and price chosen. Stripe never receives health data: nothing from the User's records, documents, readings or profile is sent to it. Stripe calculates VAT from the billing address. Stripe may process this data outside the EEA, relying on the European Commission's adequacy decision for the EU–US Data Privacy Framework and on standard contractual clauses.
Providers the User connects. When the User connects a third-party provider (e.g. Garmin, Withings), the Service exchanges OAuth2 credentials with that provider in order to pull the User's own data from that provider. We do not push health data to those providers.
8. Retention
Data is retained for as long as the account exists. Upon account deletion, data is permanently deleted within 30 days, except where retention is required by law.
Shares. A share's recipient email address, scope and expiry are stored with that share and removed whenever the share itself is removed — that is, on account deletion or when the patient it was created for is removed. Revoking a share stops its link at once but does not remove it: the share stays on the owner's Sharing page, marked revoked, with its history, so the owner can still see who opened what. Every time the link is opened, or a document is downloaded through it, that is recorded as its own row in the owner's audit log — the same log every other action in the Service writes to — visible to the owner on their audit log page and under that share on their Sharing page, and included in their Art. 20 export, and never shown to anyone but the owner.
Phones and push tokens. A phone's registration, push token included, is kept while the phone stays signed in. It is deleted when the User signs out of the phone app on that phone, when someone else signs in to the app on the same phone (the token then belongs to their account), when Apple or Google reports the token is no longer valid, or with the account. The push token is never shown in the User's export or written to the audit log: it is a credential for reaching the phone, not information about the User, so the export lists each phone without it.
Billing. The Stripe customer and subscription identifiers and the subscription's status are kept while the account exists. On account deletion they are deleted, and the Service asks Stripe to delete the User's customer record, which also cancels any subscription at once. Stripe keeps the invoices and payment records it is required to keep by tax and anti-money-laundering law, under its own retention rules; if Stripe cannot be reached at deletion, the request is logged and the operator deletes the customer record by hand.
Apple Health import. An export the User uploads is parsed into a temporary, owner-only holding file kept for at most one hour; nothing from it is written anywhere else in the Service — not even the account's own data store — until the User chooses to commit it. The commit step does not exist yet, so today every uploaded export, and everything parsed from it, is discarded within that hour regardless of what the User intended to keep. The holding file is deleted immediately if the account is deleted first, and is never part of the Art. 20 export, since it never becomes part of the account's data.
Test names the Service could not read. When a report prints a test under a name the Service does not know, or a row it could not read, the Service keeps that name as printed, the laboratory and form it was printed on, and how many of the User's reports printed it, so that the name can be added for everyone. It is kept as the account's own data: never with the result, the report or the person the report is about, never written to a log, exported with the account (9.1) and deleted with it (9.2). A name that is plainly not a test's (one carrying a long number or an e-mail address, or longer than a test's name is) is not kept at all. The operator sees such a name, or a name the User matched to a test themselves, only once more than one account's reports have printed it, and then only with counts: how many accounts and reports, never which. What only one account's reports print is not shown to the operator, because a line of a report can have the shape of a result and be something else, a person's name included.
Logs. Each part of the Service writes a technical log on the server that runs it. The logs are not part of any backup and do not leave that server.
- Web server (reverse proxy) access log: one line per request, holding the IP address, browser user-agent, method, path, response status, size and duration. Secrets carried in a path (share links, the Withings callback) are replaced with
REDACTEDbefore the line is written. Cookies and authorisation headers are never recorded. - Application log: account and record identifiers, sizes, durations and errors. Never the content of health records, uploaded files, form fields or sign-in tokens.
- Sign-in service (Keycloak) log: server events and errors, including failed sign-in attempts with the username and IP address.
- Database, health-record store and file store logs: technical messages. An error may name the record or account identifier it concerns, never health content.
Logs are rotated by size: each component keeps at most 100 MB (the web server 250 MB), and the oldest entries are deleted as new ones arrive. How many days that covers depends on traffic. A component's log is also deleted whenever that component is replaced, which for the application happens at every software update.
The audit log is different: it is part of the User's account data (it records the User's own actions and is shown to them and included in their export), it is kept as long as the account exists, and on account deletion it is pseudonymised as described in section 9.
Waitlist
While sign-up is opened gradually, the home page may offer a waitlist instead of sign-up. Joining it stores the email address given, the optional note ("what would you use it for", at most 200 characters), and when the box agreeing to this was ticked. We send one email to the address, with a link to confirm it and a link to remove it (an address that is already confirmed is instead told so, with no links); nothing else is sent until sign-up opens for that person. The purpose is only that message: we do not use the address or the note for anything else, and the note is never put in an email.
- Unconfirmed addresses are deleted automatically once the confirmation link expires (7 days by default).
- Confirmed addresses are kept until we have written to tell the person they can sign up, after which the address is deleted; or until they leave the waitlist or ask us to remove them, whichever comes first. Signing up does not by itself remove an address from the waitlist.
- To be removed: use the remove link in the confirmation email, which works at any time, or write to the support address shown in the app's footer and on the Terms page.
The waitlist is not an account: no account exists for the address, so it is not part of any account's export or deletion. The security audit log records that an address joined, confirmed or left, with the address's domain (the part after the @), the time, and the IP address and browser of the request; removal, and expiry, delete the domain, IP address and browser from those records, and only the bare action and its time remain.
9. The User's rights
See Section 6 of the Terms of Service. Requests are processed within 30 days.
9.1 Right of access and data portability (GDPR Art. 15 + Art. 20)
Signed-in Users can download a complete copy of their data at any time via
GET /me/export (also linked from /profile as "Download my data"). The
endpoint streams a ZIP archive containing:
manifest.json— export timestamp and inventory of the files inside.patient.json— the User's FHIR Patient resource.observations.json— every FHIR Observation linked to the User's Patient.documents.json— every FHIR DocumentReference for the User.documents/*.pdf— the underlying binary for each DocumentReference.terms_acceptance.json— every Terms / Privacy Policy acceptance record.integrations.json— third-party connection metadata (provider, scope, connection date) but NOT access or refresh tokens.pins.json— the tests pinned to the dashboard (test code, position, date pinned) and when the default pins were set up.loinc_aliases.json— the test names the User matched to a test themselves on a report's review page (the name as printed, the test's code, the laboratory, and when).unrecognised_names.json— the names on the User's reports that the Service could not read (the name as printed, the laboratory and form, how many reports), without their results.billing.json— the Stripe customer and subscription identifiers, the subscription's status and plan, and when its period ends. Cards and invoices are held by Stripe and are available from "Manage billing".
The archive is generated on demand and streamed; nothing is persisted server-side.
Each record is exported as it is now. The earlier versions the record store keeps (see 9.3) are not part of the export.
Not included: active sign-in sessions. The Service keeps a session row per signed-in device so a restart of the app doesn't sign you out. These rows hold OAuth tokens alongside the account identifier, so — like the integration tokens above — they are excluded from the export. A session expires automatically after 8 hours of inactivity, and every session is deleted immediately when the account is deleted (see 9.2).
9.2 Right to erasure (GDPR Art. 17)
Signed-in Users can permanently delete their account and all associated
data via GET /me/account/confirm-delete → POST /me/account/delete
(also linked from /profile as "Delete my account"). A confirmation
field requiring the literal string DELETE guards against accidental
submission. The cascade removes:
- The User's FHIR Patient resource and every Observation linked to it.
- Every FHIR DocumentReference and the underlying PDF in object storage.
- Every other health record linked to the User's Patient (conditions, medications, immunisations, allergies, procedures, diagnostic reports, encounters).
- The stored history of all of the above. The FHIR record store keeps every earlier version of a record and, on a plain delete, only marks it deleted. Each deleted record is therefore also expunged: every stored version is physically removed, so nothing of it can be read back by its identifier afterwards. If expunging one record fails, the account deletion still completes (the record is already deleted and no longer reachable through the Service or the export), and the failure is logged with the record's identifier for the operator, who completes the expunge by hand.
- The User's Terms / Privacy Policy acceptance records.
- The User's dashboard pins and the record that the default pins were set up.
- The test names the User matched to a test themselves, and the names on the User's reports that the Service could not read.
- Any third-party integration tokens the User had stored.
- The User's billing record, and the User's customer record at Stripe, which cancels any subscription at once (Section 8, "Billing").
- The User-to-Patient link row in the application database.
- Every active sign-in session, on every device — the User is signed out everywhere, immediately, as part of this same request.
When. The account itself — the application-database rows above, the sign-in sessions and the integration tokens — is deleted in the request, before the confirmation is shown. The health records in the FHIR record store and the PDFs in object storage are removed straight after, in the background, because an account with tens of thousands of device readings (an Apple Health import) takes minutes to erase record by record; this finishes within the hour, and the confirmation says the records are still being removed rather than that they are gone. From the moment the account is deleted nothing in the Service leads to them any more — the patient links, share links and sessions are already gone — and if the record store is unreachable the removal is retried until it completes.
Audit log — pseudonymised rather than erased. The
audit_log rows recording the User's activity (and the row recording
this Art. 17 deletion itself) are pseudonymised: the PII columns —
IP address, browser user-agent, and the structured details JSONB
(which can contain things like the original PDF filename) — are nulled.
The operational columns survive: action name (e.g. document.upload),
resource type / id, timestamp, and the OIDC subject UUID. The subject
UUID is a pseudonymous identifier; once the Keycloak account is
removed (manual operator step, see below), it becomes a dangling
reference and the audit data is effectively anonymous. Retention of
the anonymised trail is required by Section 10 and the security
baseline for forensic / incident-response purposes.
Keycloak (SSO) account deletion is currently a manual operator step:
the Keycloak admin user must run kcadm.sh delete users/{id} against
the medicaldata realm. The User-facing endpoint logs them out locally;
re-signing-in with the same SSO account would land on a fresh, empty
workspace until the Keycloak account itself is removed. A User who wants
the sign-in account removed too can ask at the support address (Section
1). Automated Keycloak deletion is tracked as a follow-up.
9.3 Correcting or deleting one reading or document
The FHIR record store keeps every earlier version of a record unless told to remove it. The Service removes it as follows:
- Deleting a reading deletes it and expunges it at once: every stored version of it is removed, not only the current one.
- Deleting a document deletes the document record and every reading derived from it, and expunges each of them at once. Its PDF is removed from object storage unless another of the User's documents uses the same file.
- Editing a reading (value, unit or date) writes the corrected version and expunges every earlier version, so the value that was corrected is no longer held in the record store.
- Kept until the record is edited or deleted: earlier versions written by the Service's own maintenance rather than by the User — recording a report's date on a document and on its readings, recomputing a reading's high / low mark, or a connected provider's sync updating a reading it wrote. These carry no value the User corrected, and they are removed with the record when it, or the account, is deleted.
- The audit log keeps what the User changed. The
reading.editandreading.deleteentries of the User's own audit log record the value, unit and date before and after the change (the delete page says so). The User sees these entries on the audit-log page and receives them in the export. They stay until the account is deleted, when theirdetailsare erased with the rest of the log's personal data (9.2). - If an expunge fails, the edit or delete itself still stands; the failure is recorded in the audit entry and logged for the operator, who completes it by hand.
10. Security
See docs/security-baseline.md for the technical and organisational measures in place.
11. Breach notification
If a personal data breach occurs, the supervisory authority will be notified within 72 hours per Art. 33, and affected Users will be notified without undue delay per Art. 34.
12. Changes
Material changes are notified by email at least 14 days in advance.