Privacy Policy

Last reviewed: May 2026

Deutsche Version

This policy is for users of the Open Working Hours mobile app. Every concrete claim links to the line of code that implements it — the full source is public at github.com/lukashondrich/open_workinghours.

1. At a glance

Collected — and tied to a name you picked

  • Your hospital affiliation — only if you pick a hospital from the list of ~1,220 German hospitals. You can opt out with "Prefer not to share", which keeps your hours out of all published statistics.
  • The category fields you chose (specialty, role, seniority, state, profession)

→ Details and code: §2

Collected — pseudonymous (no name attached)

  • A random user ID (UUID)
  • A hash of your email (email sign-in) — derived with a secret key
  • Your Apple or Google identifier (only if social sign-in)
  • Confirmed weekly hours: planned + actual totals only (no daily breakdown)
  • Optional: device model, OS version, recent location events — only when you tap "Report Issue"

→ Details and code: §2

Not collected

  • Your name or address
  • Your daily schedule (we only ever see weekly totals)
  • GPS coordinates (geofence runs on your device; coordinates never reach our backend)
  • Sick days (stay on your device)
  • Free-text employer names (you can only pick from the list)
  • Biometric data (Face ID / fingerprint stays in your device's secure storage)
  • Advertising or analytics identifiers (we have none)
  • IP addresses in our application database (hosting-layer logs may briefly retain them — see §7)
  • Hospital affiliation — if you chose "Prefer not to share"

How it's protected

  • K-anonymity (groups need ≥ 5 users) + dominance rule
  • Differential privacy (Laplace noise on published statistics)
  • EU-only data residency (servers in Germany)
  • Pseudonymisation (random user IDs; emails stored only as a salted hash with a server-side secret key)
  • Encryption in transit (TLS) and at rest

→ Code references: §2 and §8

2. What we collect and why

2.1 Account identity

What we collect:

  • A random user ID (UUID) — internal identifier, never shown
  • A hash of your email address (HMAC-SHA256 with a server-side secret key) — only if you registered with an email
  • Apple's or Google's user identifier for you — only if you used social sign-in (your actual email address is discarded)

Why: To recognize you across sessions and link your saved data to your account. The hash lets us look you up at login without ever storing your actual email; the Apple/Google identifier does the same job for social sign-in.

When: Created on account creation, persists until you delete your account.

Legal basis: Art. 6(1)(b) GDPR — necessary to perform our contract with you (account use).

Verify in code: models.py L76-96 · security.py L25-28

2.2 Profile category fields

What we collect:

  • Hospital affiliation — optional; pick "Prefer not to share" and nothing is stored
  • Specialty / department group
  • Role / seniority
  • Federal state (Bundesland)
  • Profession

Why: To group your contributions with similar users when producing K-anonymous statistics. Without these, the published statistics could not exist.

When: At account creation; you can edit them later in your profile.

Legal basis: Art. 6(1)(b) GDPR for storing them as part of your account; Art. 6(1)(a) GDPR (consent) for using them to contribute to aggregated statistics.

Verify in code: models.py L85-103

2.3 Confirmed weekly hours

What we collect:

  • Per finalized week: planned hours total + actual hours total (no daily breakdown)
  • The week's start date
  • Your country, state, specialty, hospital affiliation (or NULL if you opted out)

Why: To produce K-anonymous, differentially-private statistics about working hours by group. Daily data is never transmitted — only weekly totals after you confirm.

When: When you confirm all 7 days of a week and the week finalizes.

Legal basis: Art. 6(1)(a) GDPR (consent specifically for statistical contribution).

Verify in code: models.py L147-185 · aggregation.py L54-90

2.4 Optional — Calendar export

What we collect: Nothing. Calendar export writes to your device's calendar; no shift data reaches our backend.

What is shared with your device's calendar: Shift name, start/end time, color, absence type (vacation/sick).

Where it goes after that: Your device's calendar app. If your calendar syncs with iCloud, Google Calendar, or is shared with anyone, those events appear there too. You control this in your device's settings.

When: Only if you enable the "Export to calendar" toggle in Settings.

Legal basis: Art. 6(1)(a) GDPR (consent — toggled by you).

Verify in code: CalendarExportManager.ts L42

2.5 Optional — Bug reports

What we collect (only when you tap "Report Issue" and then "Send report"):

  • Your optional text description of the problem
  • Device model (e.g., "iPhone 15 Pro")
  • OS name and version
  • App version and build number
  • Counts of saved work locations and work events
  • The last ~100 geofence events on your device — timestamps, event type, GPS accuracy in meters, ignored flag/reason, and aggregate accuracy counts. By default, this does not include saved workplace names or coordinates.
  • If you're signed in: your user ID, so we can correlate the report with your account and delete it if you delete your account. We do not store hospital affiliation, specialty, role, or state with bug reports.

Optional location diagnostics: The confirmation sheet has an unchecked "Include location diagnostics" checkbox. If you enable it, the report also includes saved workplace names, approximate saved workplace coordinates rounded to 3 decimals (about 100 m precision), and recent geofence events with workplace names.

What the backend enforces: If location diagnostics are not explicitly enabled, the backend strips saved workplace details and geofence location names even if an older app version sends them.

Why: To diagnose technical problems — especially geofencing reliability on Android, where we depend on real-world telemetry to find and fix bugs.

When: Only when you tap "Report Issue" in Settings and confirm submission.

Retention: Bug reports are kept for 90 days, then automatically purged. Reports are also deleted immediately when you delete your account.

Legal basis: Art. 6(1)(a) GDPR (consent — initiated by you each time).

Verify in code: reportIssue.ts · feedback.py L19-81 · models.py L342-367

2.6 Consent records

What we collect:

  • Which version of the Terms of Service you accepted
  • Which version of the Privacy Policy you accepted
  • The timestamp of your acceptance

Why: GDPR Art. 7 requires us to be able to demonstrate that you have given consent. These three fields are the legal record of that.

When: At account creation; again if a major policy update requires re-acceptance.

Legal basis: Art. 6(1)(c) GDPR — legal obligation (Art. 7 imposes the recordkeeping requirement).

Verify in code: models.py L106-108

3. What stays on your device

These categories of data are processed on your device only and never reach our backend.

What Where on device Verify
GPS coordinates Geofence service (in-memory) — detection runs locally; only the resulting weekly hour totals are transmitted GeofenceService.ts
Shift templates, planned shifts, absences Local SQLite database (calendar.db) CalendarStorage.ts
Tracking records (unconfirmed sessions) Local SQLite (tracking_records) — only weekly totals are submitted after you confirm CalendarStorage.ts
Sick days Local SQLite (absence_instances with type='sick') — never transmitted CalendarStorage.ts L139
Biometric data (Face ID / fingerprint) iOS Secure Enclave / Android Keystore — managed by the OS; we never see it Handled by expo-local-authentication
Onboarding state, dismissed prompts AsyncStorage — local UI preferences n/a

4. Third-party recipients

When you use Open Working Hours, your data may pass through these services. Most are always involved (hosting, geocoding); some only if you choose specific features (social sign-in, calendar export). Every claim links to the line of code that implements it.

Purpose Always or optional Recipient Location Triggered by Source
Backend hosting, database, backups Always Hetzner Online GmbH Germany All app use Hetzner DPA (standard terms) · signed 2026-01-13
Email verification codes Email sign-in only Brevo (Sendinblue) EU Registering or logging in via email Brevo DPA (standard terms) · email.py L28
Workplace search Always (only when searching) Komoot GmbH (Photon) Germany Searching for a workplace in setup GeocodingService.ts L105
Identity verification (Apple) Optional Apple Inc. USA (under EU-US Data Privacy Framework) Choosing "Sign in with Apple" social_auth.py L24-25
Identity verification (Google) Optional Google LLC USA (under EU-US Data Privacy Framework) Choosing "Sign in with Google" social_auth.py L26
Calendar export Optional iOS Calendar (Apple) or Google Calendar Your device + the sync service you have set up yourself (e.g., iCloud, Google) Enabling the "Export to device calendar" toggle CalendarExportManager.ts L42

All source references current as of commit 49ea57b. The full source code is public at github.com/lukashondrich/open_workinghours.

5. How long we keep your data

Data Retention
Account data (user_id, email hash or social-auth identifier, profile fields, consent records)Until you delete your account
Confirmed weekly hoursUntil you delete your account, plus up to 30 days in immutable backups
Bug reports90 days, then auto-purged. Deleted immediately if you delete your account.
Aggregated statistics published from your contributionsRetained indefinitely — these are anonymous, not personal data
Application logs (backend)7 days
Database backups (Hetzner Object Storage, COMPLIANCE-mode Object Lock)30 days, immutable
Hosting-layer logs (Hetzner, may contain IP addresses)Per Hetzner standard retention; we have no application-layer access
Verification codes (email sign-in)Deleted after use or expiry (typically minutes)
Social registration tokens30 minutes

When you delete your account, the corresponding rows are removed within seconds. The 30-day backup window means a deleted account's data may persist in encrypted backups until that backup rotates out — this is documented in data-retention-policy.md.

6. Your rights under GDPR

Right How to use it
Access (Art. 15) In-app: Settings → Data & Privacy → Export Data. Returns JSON of all your records. DataPrivacyScreen.tsx
Rectification (Art. 16) In-app: Settings → Profile. All profile fields are editable.
Erasure (Art. 17) In-app: Settings → Data & Privacy → Delete Account. auth.py L406-457
Restriction (Art. 18) Contact us via the email below.
Portability (Art. 20) In-app: Settings → Data & Privacy → Export Data. JSON format.
Objection (Art. 21) Pick "Prefer not to share" as your hospital — your weekly hours no longer enter any published statistic. Or contact us for broader objection.
Complaint You can complain to a supervisory authority at any time. For Berlin, the responsible authority is the Berliner Beauftragte für Datenschutz und Informationsfreiheit (www.datenschutz-berlin.de).
Privacy budget transparency (Art. 15) Authenticated endpoint exposes your ε spend: GET /auth/me/privacy-budget. auth.py L390-403

We do not make automated decisions or profile you (Art. 22 — not applicable).

7. Where your data goes

Your account, your hours, and our analytics — all in Germany. Backend, database, and backups run on Hetzner servers in Germany. Email codes route through Brevo (EU). Workplace search uses Komoot (Germany). See §4 for the full recipients matrix.

Two situations involve transfers to countries outside the EU/EEA — both are optional and triggered by your choices:

  • If you choose Apple Sign-In or Google Sign-In: identity verification involves Apple Inc. (USA) or Google LLC (USA). These transfers occur under the EU-US Data Privacy Framework adequacy decision. The data sent during sign-in is what Apple/Google use to confirm the identity token is valid; we receive only an opaque identifier in return. Your email is not retained by us for social-auth users.
  • If you enable Calendar Export and your device's calendar syncs to iCloud or Google Calendar: the calendar entries follow your device's sync configuration, which may include US servers. This is between you and your calendar provider; we don't see or control it.

About hosting-layer IP addresses: like every web server on the internet, Hetzner's infrastructure briefly sees and logs your IP address when your device makes a request. Our application database does not store IPs. Hetzner is based in Germany; their infrastructure logs stay in Germany.

8. Security measures

Layer What we do Verify
Encryption in transitTLS 1.2+ for all client–server trafficdocker-compose.yml (Caddy reverse-proxy with auto-TLS)
Encryption at restHetzner-managed disk encryption + immutable backups (COMPLIANCE-mode Object Lock)data-retention-policy.md
PseudonymisationRandom UUID user IDs; emails stored only as HMAC-SHA256 hashes with a server-side secret keysecurity.py L25-28
K-anonymity in published statsCells with fewer than 5 contributors are not published; dominance rule prevents any single user from being more than 30% of a group's contributiondp_group_stats/config.py
Differential privacyLaplace noise added to published values; per-user annual ε budget cap of 150mechanisms.py · accounting.py
Authentication tokensJWT with 30-day expiry; stored in the device's hardware-backed secure storage (iOS Keychain / Android Keystore)AuthStorage.ts
Rate limiting5 auth requests per minute per IP; 10 feedback submissions per minute per IPrate_limit.py

Honest about limits: pseudonymisation reduces but does not eliminate re-identification risk. At smaller hospitals, the combination of profile fields (hospital + specialty + seniority) could in principle allow identification by someone who already knows you work there. The K-anonymity threshold and differential privacy mechanisms above are designed to prevent this in published statistics, but the underlying operational data is still pseudonymous personal data — not anonymous — and is subject to all the protections in this notice. This is acknowledged in our Data Protection Impact Assessment, risk R7.

9. Responsible person

Lukas Hondrich
Karl-Marx-Str. 182
12043 Berlin
Germany

Email: lukashondrich@googlemail.com

For German legal compliance, this matches the Impressum on the website. Update both pages together when this changes.

Data Protection Officer: Not required — sole proprietor below GDPR Art. 37(1) thresholds.

Supervisory authority: Berliner Beauftragte für Datenschutz und Informationsfreiheit, Friedrichstraße 219, 10969 Berlin, www.datenschutz-berlin.de