Privacy Policy
Last reviewed: May 2026
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 hours | Until you delete your account, plus up to 30 days in immutable backups |
| Bug reports | 90 days, then auto-purged. Deleted immediately if you delete your account. |
| Aggregated statistics published from your contributions | Retained 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 tokens | 30 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 transit | TLS 1.2+ for all client–server traffic | docker-compose.yml (Caddy reverse-proxy with auto-TLS) |
| Encryption at rest | Hetzner-managed disk encryption + immutable backups (COMPLIANCE-mode Object Lock) | data-retention-policy.md |
| Pseudonymisation | Random UUID user IDs; emails stored only as HMAC-SHA256 hashes with a server-side secret key | security.py L25-28 |
| K-anonymity in published stats | Cells with fewer than 5 contributors are not published; dominance rule prevents any single user from being more than 30% of a group's contribution | dp_group_stats/config.py |
| Differential privacy | Laplace noise added to published values; per-user annual ε budget cap of 150 | mechanisms.py · accounting.py |
| Authentication tokens | JWT with 30-day expiry; stored in the device's hardware-backed secure storage (iOS Keychain / Android Keystore) | AuthStorage.ts |
| Rate limiting | 5 auth requests per minute per IP; 10 feedback submissions per minute per IP | rate_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 HondrichKarl-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