Last updated: 21 October 2026. Version 2026-10-21.2. The German text is the binding version. This English text is a translation.
The controller under Article 4 (7) GDPR for the processing described in this notice is:
Christian Wahnsiedler
Markgrafendamm 33, 10245 Berlin, Deutschland
Email: christian@meddox.net
Telephone: +49 173 412 5665
No data-protection officer is configured in the application. If the law requires one, that appointment is not shown here.
Meddox is a web application for keeping a personal health record and for decision support. Meddox is not a medical device and does not diagnose, treat, or monitor disease. This notice describes what the code does today. It covers local use in the browser, demo accounts, and a pilot account whose record is stored on a server.
Account data: email address, display name, Firebase user id or the Google sign-in id, role (patient, clinician, admin), and the stored consent (time, version, language, and whether the AI transfer was ticked).
Health data within Article 9 GDPR, where you enter or upload it: cases, personal details on the record, diagnoses, medications, lab values, questions for a physician, image and document files, study and document metadata, AI summaries, and text a model returns. Demo accounts are meant for sample records. If real health data is typed there, it stays in that browser.
Usage and security data: time, provider, model name, purpose, token counts, an estimated amount, user id, and whether the call succeeded, in ai_usage. New rows do not store the prompt or the patientId. An error field can hold a short provider error; the code does not prove that field never contains clinical text. Audit rows store the action, the actor, the target id, and counts. Rate limits store counters. Application server logs are written to hold ids and counts, not record content. Google Cloud platform logs (IP address, URL, status) were not inspected for this notice.
Account and sign-in, so the service can be tied to you: Article 6 (1) (b) GDPR.
Keeping the health record and showing decision support: Article 6 (1) (a) GDPR and, for health data, explicit consent under Article 9 (2) (a) GDPR. On a pilot account the application asks for that consent before the record is stored on the server. The version is stored. Pilot writes are refused without the current version. When the record stays in this browser, the beta screen asks for an acknowledgement and, separately, for the AI transfer. It does not use the same Article 9 wording. Whether that local store needs the same explicit consent is a review point in docs/legal/REVIEW.md.
Sending health data to a model: only with explicit consent under Article 9 (2) (a) GDPR for the current version of this notice. The server routes for health-file upload and for AI calls that contain health data read users/{uid}/consents. Without a valid consent they answer consent_required and do not call a provider. The checkbox in the browser is not that check. A new version of this notice asks for consent again. Whether Article 6 (1) (a) also applies is a point for counsel and is not decided here.
Abuse prevention, rate limits, the cost ceiling, and audit rows of ids and counts: Article 6 (1) (f) GDPR. The legitimate interest is running and protecting the service. Counsel has to confirm that basis. Withdrawal deletes that user’s ai_usage rows, because an error field can contain clinical text. A failed limit check can still let the AI call through. The Article 9 check before it does not.
Queries to ClinicalTrials.gov use fixed disease names, not the text of your record: Article 6 (1) (f) GDPR, an interest in showing public trial information. Counsel should confirm that basis. Stripe, only when Stripe is configured and you start a checkout, receives a user id to begin a payment: Article 6 (1) (b) GDPR.
Showing that consent was given (Article 7 (1) GDPR): storage of the consent row. Whether Article 6 (1) (c) GDPR also applies is listed for review in docs/legal/REVIEW.md and is not stated here as settled law.
Without the pilot flag, and always for an address ending in @demo.medinsight, the record stays in this browser’s localStorage under the medinsight_ key, and consent under meddox_consent_. Sign-in does not copy it to the server. A copy into the pilot record happens only after a separate confirmation, and only when the current consent is present.
On a pilot account with access_users/{uid}.pilot set to exactly true, the application writes to Firestore and, when a bucket is set, files to Cloud Storage. The Terraform default region is europe-west3 (EU). The application code does not set the region itself. This version was not re-checked against the live system. With no bucket variable, an export contains no file bytes, and a pilot erasure fails closed instead of clearing only the browser.
Google Cloud runs Cloud Run, Firestore, Cloud Storage, Secret Manager, and logging where the service is deployed there. The Terraform file says Google-managed default encryption is used and no customer-managed key (CMEK) is set up. Firebase Authentication stores the account; the location of that Auth service is not set in the repository. Google sign-in uses NextAuth (OpenID scopes openid, email, profile). The session cookie can hold the Google tokens. The access token is not copied into the session object sent to the browser.
Health content (the record, labs, images, voice, transcripts, translations, and summaries) goes only to Google Vertex AI. The code default is the EU multi-region eu (host aiplatform.eu.rep.googleapis.com). The global endpoint is not used for that. eu is not a promise of Frankfurt and not a promise of processing in one country. An endpoint alone does not show that processing stays in the region. The model cards of 21 October 2026 list gemini-4-argon, gemini-3.8-flash, and gemini-3.5-transcribe for eu, not for europe-west3 (Frankfurt), which is why the default is eu. If an operator sets VERTEX_LOCATION to europe-west3 or another EU region and the card does not list the model there, the call fails and is not moved to eu or to global. europe-west3 stays allowed in the router if a later card lists the model. London (europe-west2) and locations outside the EU are refused. Grok (xAI) and the direct Anthropic API do not receive that health content while XAI_HEALTH_ENABLED and VERTEX_CLAUDE_HEALTH are off. That is the default. Literature without record text, model and roadmap text, interface text, and synthetic tests may use the direct APIs, and for a pilot account only inside PILOT_AI_PROVIDERS. If that variable is unset, the wired non-health providers stay reachable. The value none or an empty value turns them off for pilot accounts. Standard contractual clauses or the EU-US Data Privacy Framework apply only if that provider actually offers them and the contract includes them. This application does not check a certification and does not claim one. Accepting the Google CDPA, a Vertex abuse-logging exception, a DPIA, and a contract with xAI are not done by this version. See docs/DATA_PROCESSING.md. That is a review point, not a legal conclusion.
lib/generateAnalysis.ts is imported only from lib/api.ts, which runs in the browser. Before any fetch it asks the health router. The three direct providers are refused for health content, so the fetch does not run. The keys are not named NEXT_PUBLIC_ and are not shipped in the browser bundle. That path is not behind PILOT_AI_PROVIDERS. The error is swallowed and a local text generator continues. That is not a model call.
The imaging route does not call MedGemma. If the checkbox is on, the response stores the reason health_vertex_only and does not open an endpoint. lib/medgemma.ts stays a stub. No default endpoint is configured.
Stripe (api.stripe.com) receives a user id only when Stripe keys are set and you start a checkout. Otherwise the route says payments are not configured. ClinicalTrials.gov is queried with fixed search terms, not with the text of your record. The German trial list is shipped inside the application. An AI match can send record text to an enabled AI provider.
The following list reads the current server variable. It is the configuration of this process, not a promise about another time.
The list is loading.
Data in the browser stays until you delete it or clear the browser storage. The NextAuth session max age is 7 days. The browser cache of the trial query is set to 24 hours.
A pilot record stays until the erase function removes it. The application sets no expiry of its own on Firestore documents. In terraform/main.tf a lifecycle rule deletes bucket objects after 90 days when the environment is not prod, and after 2555 days when it is prod. That applies only while pilot_storage_hardening is off (that is the default). Bucket versioning is on. The rule does not say that old versions are deleted. Firestore point-in-time recovery and delete protection are off in that default. The live bucket was not re-checked for this version.
“Withdraw consent and delete data” writes the withdrawal event first and thereby blocks processing. It then deletes health documents in Firestore, files under that user’s prefix in the configured bucket, derived AI outputs, and that user’s ai_usage rows. The deletion log stores state, attempts, and counts, not record content. If the bucket is missing, the Firestore rows are still deleted. If a step fails, the job stays incomplete and can be run again. Not deleted: users/{uid}/consents, access_users including a pilot flag, invites, rate_limits, the audit row of counts, and the Firebase Auth user. Whether those leftovers may be kept is open and is not stated here as a legal duty. Objects outside the prefix, including older Cloud Function paths, are not deleted. Local storage in another browser is not reached. Deleting the login itself is an open point.
Under the GDPR you have the right of access, rectification, erasure, restriction, portability, and objection, where the relevant article provides it, and the right to withdraw consent for the future.
Privacy & consent shows this notice, the consent status with dates, a JSON export (Article 20, schema meddox-privacy-export-v1, without file bytes), and the button “Withdraw consent and delete data”. The dialog states the consequences. Processing is then blocked, health data is deleted where the job can reach it, and this session is signed out. The ZIP from Account & data remains available as well. In pilot mode GET /api/pilot/export returns only your own rows. The audit entry stores counts. More than 200 files or 100 MB is refused there.
For rights the application does not perform itself, write to christian@meddox.net once that address is set.
You may lodge a complaint with a supervisory authority, in particular in the member state of your residence, your place of work, or the place of the alleged infringement (Article 77 GDPR). Which authority is competent for the controller depends on the configured address. The German Federal Commissioner for Data Protection publishes a list of authorities (bfdi.bund.de). No particular state authority is named here, because the seat is not hard-coded.
Meddox does not make a decision that produces legal effects concerning you or similarly significantly affects you (Article 22 GDPR). Model output is decision support for you and a physician to review. A trial row is not an enrollment decision. An imaging comparison must not claim that a tumor has gone or returned.
The public site is intended to be served over HTTPS. Strict-Transport-Security is set only when Next.js runs in production, with no preload. Local development does not send that header. Firestore and the bucket use Google-managed default encryption according to the Terraform file, not a customer-managed key. Pilot files go through the server and the Admin SDK. The Storage rules in the repository deny every browser read and write. The Firestore rules in the repository let a signed-in user read a personal case only when createdBy is that user and the case has no organizationId. Organization cases follow organization roles. Clinical-trial documents are readable by any signed-in user. Unmatched paths are denied. Whether those rules are published on the live project was not checked for this version. Model calls are limited in number. A failed limit check can still let the call through. A certification, a bug-bounty programme, and a promise of a particular availability are not configured and are not claimed.
The application sets no analytics cookie and no advertising cookie. Google sign-in uses a NextAuth 4 session cookie. The code does not change the cookie names. The library defaults are next-auth.session-token and, on HTTPS, __Secure-next-auth.session-token, plus a CSRF cookie. The session cookie is strictly necessary to keep the sign-in. Firebase Authentication is called without setPersistence; the SDK default applies, typically IndexedDB in the browser, not a cookie.
localStorage, all used for the function you asked for: onco_lang (language), medinsight_current_role and medinsight_role_{uid} (role), medinsight_{uid} (the record, when it is local), meddox_consent_{uid} (consent), clinical-trials-cache and clinical-trials-curated (trial list), offlineQueue_{studyId} (names in an offline queue). The code has no separate cookie banner, because no analytics cookie is set. Whether storing the record in local storage also needs consent is for counsel; the pilot consent covers the server-side record.
Meddox is not a medical device and does not diagnose, treat, or monitor disease. All AI output is decision support and may be wrong. Always check with your physician and primary sources. In an emergency, call 112 (EU) or your local emergency number.