WingerApp Privacy Policy (v0 — code-verified draft)
Last updated: 2026-09-17 · Status: Draft, pending legal sign-off (this document does not substitute a lawyer's final review).
This policy describes what WingerApp actually does with your data, based on the code as it exists today. Every claim that could not be verified, or that needs confirmation from backend-engineer/security-privacy-reviewer, is marked [VERIFY: ...] and must be closed before publication. The Turkish version (privacy-policy.tr.md) is the primary text; this English version is meant to be identical in meaning for App Store submission.
Data controller: Winger App (see kvkk-aydinlatma-metni.md — legal entity [VERIFY: company name/address]).
2026-09-18 update note (web team): S96/S98 (
docs/security/backend-router-inventory-2026-09-17.md, commit0044e1fb) landed in code on 2026-09-17, and this document has been updated to match (§1 item 2, and thelocation_snapshotsrows in §3 and §5 below).compliance-docs-writershould verify this update during final legal review.
1. Summary — the architectural principle and its known exceptions
WingerApp's design principle is "personal data never leaves the device; the server only ever receives an irreversible SHA-256 hash" (CLAUDE.md, memory-bank/Architecture.md). Code review, however, surfaces known exceptions to this principle, and this policy does not hide them:
- At registration, gender, gender of interest, city, and age band are sent to and stored on the server in plain text (not hashed) — required for candidate filtering (
backend/app/models/__init__.py,Usertable;backend/app/api/auth.py::RegisterRequest). "Gender of interest" can indicate who you want to date (and therefore, indirectly, sexual orientation) — see §9 andacik-riza-metni.md. Registration also sends district and country to the server in plain text and stores them on theuserstable, but the matching engine never reads either field, and they are no longer returned viaGET /auth/meeither (S96, 2026-09-17) — both columns are marked as drop-candidates for a future removal migration (seedocs/security/backend-router-inventory-2026-09-17.md). - (Fixed by S98, 2026-09-17) A location update now only sends your city to the server — raw latitude/longitude is no longer persisted at all. Previously,
POST /api/location/updatealso wrote your real GPS coordinates into thelocation_snapshotstable; that behaviour was removed (backend/app/api/location.py::update_location, commit0044e1fb) because no code anywhere read those fields (matching only usesusers.city) and the mobile app never called this endpoint in the first place. The endpoint may still accept lat/lng/accuracy fields, but no longer writes them anywhere. Thelocation_snapshotstable itself has not yet been dropped (see §3, §5) — that is left to a follow-up cleanup migration. - Purchase receipts (App Store/Google Play) are sent to Apple/Google for verification — this is the platforms' own mandatory in-app purchase infrastructure.
Outside of these three exceptions: your profile (name, bio, photos), preference answers, chats, face-verification data, and match scoring stay on your device.
2. Data that stays on your device and never reaches the server
| Data | Where it lives | Evidence |
|---|---|---|
| Profile name, bio, photos | Device (AsyncStorage), ProfileService.ts | No call site in the mobile codebase sends these fields to the backend (grep "/profiles/me" → 0 results); see §2.1 below |
| Preference questionnaire answers (relationship goal, smoking/alcohol, etc.) | Device, PreferenceProfileService | agents/agent-represented-matching.md |
| Human-to-human chat messages | Device-to-device, end-to-end encrypted (E2EE) P2P channel; the server never sees the content | backend/app/models/__init__.py::Conversation — only participant_1_id, participant_2_id, timestamps; no message-content column |
| Face-verification image/embedding | Entirely on-device (Apple Vision / Google ML Kit) | memory-bank/Features/Face-Verification.md; no call site was found that uploads a photo/embedding to the backend |
| Match score, preference weights, night-negotiation transcript | Device | agents/agent-represented-matching.md, "the backend outcome sent is only no_match | match" |
| Category answers you chose to share during the night agent negotiation | To the peer's device, end-to-end encrypted — never to the server | See §7 below |
2.1 An unused-but-existing endpoint — transparency note
The backend has a PUT /api/profiles/me endpoint that can accept and persist a profile name/bio/photo URLs into the user_profiles table (backend/app/api/profiles.py). However, no screen in the mobile app calls this endpoint (zero call sites confirmed in the codebase) — profile data is, in practice, kept only on-device today. [VERIFY: backend-engineer] — the purpose/future of this endpoint should be clarified; unused PII-accepting endpoints have previously turned into real security findings in this project (S15/S16, GET /api/profiles/{user_id}) and were removed. The same class of risk exists here and should be resolved (removed, or explicitly documented as future-use) before launch.
3. Data sent to the server (backend), table by table
| Table/endpoint | Content | Purpose | Evidence |
|---|---|---|---|
users | DID (self-certifying public identifier — not secret), SHA-256 hash of the DID, plan, gender, gender of interest, city, age band (plain text, actually used by matching) plus district, country (plain text, stored but never read by matching — drop-candidate, S96), negotiation_consent_enabled flag | Registration, match-candidate filtering | backend/app/models/__init__.py:33-84 |
location_snapshots | (as of S98, 2026-09-17) no longer receives new rows — the table/columns still exist in code, but POST /api/location/update no longer writes lat/lng/accuracy anywhere | None (unused table, slated for removal) | backend/app/api/location.py::update_location (comment: "lat/lng/accuracy are accepted but no longer persisted anywhere") |
match_outcomes | One-sided match declaration between two hashed identities | Mutual-match detection | backend/app/models/__init__.py:460-481 |
mutual_matches | Confirmed reciprocal match (two hashes + timestamps) | Chat authorization | backend/app/models/__init__.py:483-542 |
pair_cooldowns | Direction-less pair hash + cooldown expiry | Abuse/persistence prevention | backend/app/models/__init__.py:433-458 |
push_tokens | Expo push token + platform | Notification delivery | backend/app/models/__init__.py:274-293 |
payments, iap_transactions | Plan/product, amount, status, SHA-256 hash of the receipt (the raw receipt is never stored) | Purchase verification, accounting | backend/app/models/__init__.py:111-138, 372-415 |
abuse_reports | Reporter/reported DID, category (harassment/scam/fake profile/spam/inappropriate content/other), free-text description (user-authored), evidence hash | Moderation | backend/app/models/__init__.py:168-197; categories: mobile-app/src/screens/ReportBlockScreen.tsx:39-47 |
| Signaling (WebRTC offer/answer/ICE) | Which hashed identity is trying to connect to which, for what purpose (negotiation/chat) | P2P connection setup | backend/app/api/signaling.py |
| TURN relay server | Relays media/data traffic when a direct connection can't be established; sees your IP address while relaying (inherent to how TURN works — it does not see message content, traffic is E2EE) | Connectivity | memory-bank/Features/P2P-Chat.md; self-hosted coturn |
telemetry table / TelemetryService | Device id, session id, metric name/value, app/OS version | Product analytics | backend/app/models/__init__.py:141-165 — see §8, honest DP disclosure |
device_attributions, revenue_shares | SHA-256 hash of device identifier (IDFV/Android ID), influencer code, revenue share | Influencer attribution system | backend/app/models/__init__.py:299-366 |
What the backend NEVER sees (architectural constraint, code-verified): your raw profile, preference weights/thresholds, which categories were asked, the negotiation transcript, your photo preference/decision, the photo itself, or your match score/band (agents/agent-represented-matching.md — "what the backend never learns" list).
4. Third parties
- Apple App Store / Google Play — in-app purchase receipt verification (
IAPTransaction). - Apple Push Notification service (APNs) / Expo push infrastructure — notification delivery.
- Our self-hosted TURN server — not a third-party SDK, but it can see your IP address while relaying a connection.
- No advertising or analytics SDK is used.
mobile-app/package.jsondependencies were checked — no Facebook SDK, Adjust, AppsFlyer, Firebase, Sentry, Mixpanel, Amplitude, or Crashlytics (or any similar third-party tracking/analytics library) is installed (grep result: 0 matches). - Your data is not shared with, or sold to, any ad network or data broker.
5. Retention periods
| Data | Period | Evidence / status |
|---|---|---|
match_outcomes (unreciprocated) | Auto-deleted after 30 days | _sweep_expired_outcome_tables(), backend/app/models/__init__.py:466 |
mutual_matches | Deleted after 90 days of activity (chat) inactivity | backend/app/models/__init__.py:501-515 |
pair_cooldowns | no_match: 30 days, incomplete: 7 days, max 3 retries per 90 days | agent-represented-matching.md |
location_snapshots | No new rows written after S98 (2026-09-17); any existing rows are deleted on account deletion. [VERIFY: backend-engineer] — whether a one-off backfill cleanup of pre-S98 rows is needed should be clarified before launch. | |
telemetry table | [VERIFY: backend-engineer] — no automatic TTL found. | |
payments, iap_transactions, abuse_reports | Not deleted on account deletion; the DID link is irreversibly anonymized (see §6). Retained indefinitely for financial/moderation purposes. | |
| Everything else account-related | Deleted immediately and permanently on account-deletion request (see §6) |
6. Account deletion and data portability
You can delete your account from the Settings screen. This calls DELETE /api/users/me (backend/app/api/users.py::delete_me), which performs the following, atomically, in a single request:
- Deleted outright: your account record, profile, blocks, push token, raw location snapshots (
location_snapshots), location preference, usage ledger (usage_ledger), chat metadata (conversations), one-sided match declarations (match_outcomes), confirmed mutual matches (mutual_matches), retry-cooldown records (pair_cooldowns). - Retained, but with the identity link irreversibly severed: your payment/purchase records (financial audit trail) and any abuse reports filed by or against you (moderation-history integrity) — your DID is converted, via
HMAC-SHA256(secret, did), into an irreversible pseudonym (deleted:<hash>); the record can no longer be linked back to you. - Revoked: the session token (JWT) used for this request is blacklisted immediately; any open signaling WebSocket you had is closed.
This action is irreversible.
Data portability (KVKK Art. 11 / GDPR Art. 20): [VERIFY: backend-engineer] — within the scope of this task, no automated, self-service "export my data" endpoint was found in code that would let a user download a machine-readable copy of their own data end-to-end. Some backend module names suggest this exists (legal_compliance_rolling_audit_v2.py, data_minimization.py), but the one sampled (data_minimization.py) turned out to be a stub returning fixed/mock data — it cannot be relied on as a real user-data-export flow. Recommendation: before launch, either build a real self-service export endpoint, or serve this right manually via a support request (support@…), and update this policy accordingly.
7. Night agent negotiation (optional feature)
The "Night agent negotiation" toggle in Settings is off by default and can be turned off at any time (doing so immediately cancels any in-progress session). While on:
- Your device holds a private, end-to-end encrypted session with the agents on your match candidates' devices while you sleep or charge.
- Only the category answers you chose to share go to the peer's device — not the server. Your profile, importance weights, threshold, photos, and chats never enter this session.
- The server only ever sees: your hashed (irreversible) identity, the session outcome ("no match" / "match" — no other value exists), this consent flag, and the signaling metadata required to set up the connection (who you're trying to connect to, and when).
This paragraph is written to match, word-for-word in meaning, the current toggle description in mobile-app/src/screens/SettingsScreen.tsx (post S91 fix, 2026-09-16).
8. Telemetry — the honest state
The app collects general usage telemetry (crash/error, feature-usage counters). The architecture documentation states that "all telemetry is protected with Differential Privacy (DP, ε≤1.0)" — but this claim is currently NOT TRUE for the general telemetry pipeline: TelemetryService.trackEvent() adds no noise, carries millisecond-precision timestamps and a device id, and sends DID-signed batches (memory-bank/Security-Privacy.md, "Rule #2 correction", S87). As a result, event-level telemetry specific to the night-agent-negotiation feature is currently entirely disabled (NEGOTIATION_TELEMETRY_DP_READY = false, unconditional no-op) — no negotiation event reaches the server until a real DP-protected pipeline is built. This policy will be updated when that changes.
9. Special category (sensitive) data
- Sexual orientation indicator: the mandatory "gender of interest" field at registration is sent to the backend in plain text (see §1, §3). This may qualify as special category data under KVKK Art. 6 / GDPR Art. 9. See
acik-riza-metni.mdfor your explicit consent. - Belief practice, political opinion, intimacy/closeness preferences: these can only be shared, E2EE, with a matched peer's device during the night negotiation if you separately and explicitly grant a distinct, off-by-default consent ("Sensitive Preferences") — they are never sent to the backend. Evidence:
mobile-app/src/services/ConsentManagementV7Service.ts(SENSITIVE_PREFERENCES_CONSENT_ID). - Biometric (face) data: face verification is processed entirely on-device; no image/embedding is ever uploaded to the server (see §2). Honest limitation: this is not a real deep-learning face-recognition model; it is geometric landmark matching plus an uncalibrated texture-analysis (anti-spoofing) heuristic — see
safety-policy.mdandmemory-bank/Features/Face-Verification.md.
10. Security
- Authentication: self-certifying Ed25519 DID, no passwords.
- P2P messaging: X25519 key exchange + XSalsa20-Poly1305 (
nacl.secretbox) end-to-end encryption. - Every identifier sent to the backend is SHA-256 hashed (except the deliberate exceptions listed above).
- Transport security:
[VERIFY: infra-devops]— whether HTTPS/TLS and the TURN deployment are actually complete in production could not be verified within this task;memory-bank/Progress.mdlists this as a launch blocker.
11. Children's privacy
WingerApp is not directed at users under 18. The youngest age-band option in the registration flow is 18-25 (mobile-app/src/screens/OnboardingScreen.tsx:302, ageBandMinAge['18-25'] = 18). Accounts found to belong to a person under 18 are closed.
12. Contact and changes
For questions about this policy, contact support@wingerapp.com (see mobile-app/src/config/support.ts::SUPPORT_EMAIL). Material changes will be announced via an in-app notice.
Related documents
kvkk-aydinlatma-metni.md— KVKK Article 10 disclosure noticeacik-riza-metni.md— explicit consent texts for special category dataterms-of-service.en.md— terms of servicesafety-policy.md— safety and moderation policyapp-privacy-nutrition-label.md— Apple App Privacy questionnaire answers