Last updated: 2026-09-17

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, commit 0044e1fb) landed in code on 2026-09-17, and this document has been updated to match (§1 item 2, and the location_snapshots rows in §3 and §5 below). compliance-docs-writer should 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:

  1. 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, User table; backend/app/api/auth.py::RegisterRequest). "Gender of interest" can indicate who you want to date (and therefore, indirectly, sexual orientation) — see §9 and acik-riza-metni.md. Registration also sends district and country to the server in plain text and stores them on the users table, but the matching engine never reads either field, and they are no longer returned via GET /auth/me either (S96, 2026-09-17) — both columns are marked as drop-candidates for a future removal migration (see docs/security/backend-router-inventory-2026-09-17.md).
  2. (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/update also wrote your real GPS coordinates into the location_snapshots table; that behaviour was removed (backend/app/api/location.py::update_location, commit 0044e1fb) because no code anywhere read those fields (matching only uses users.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. The location_snapshots table itself has not yet been dropped (see §3, §5) — that is left to a follow-up cleanup migration.
  3. 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

DataWhere it livesEvidence
Profile name, bio, photosDevice (AsyncStorage), ProfileService.tsNo 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, PreferenceProfileServiceagents/agent-represented-matching.md
Human-to-human chat messagesDevice-to-device, end-to-end encrypted (E2EE) P2P channel; the server never sees the contentbackend/app/models/__init__.py::Conversation — only participant_1_id, participant_2_id, timestamps; no message-content column
Face-verification image/embeddingEntirely 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 transcriptDeviceagents/agent-represented-matching.md, "the backend outcome sent is only no_match | match"
Category answers you chose to share during the night agent negotiationTo the peer's device, end-to-end encrypted — never to the serverSee §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/endpointContentPurposeEvidence
usersDID (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 flagRegistration, match-candidate filteringbackend/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 anywhereNone (unused table, slated for removal)backend/app/api/location.py::update_location (comment: "lat/lng/accuracy are accepted but no longer persisted anywhere")
match_outcomesOne-sided match declaration between two hashed identitiesMutual-match detectionbackend/app/models/__init__.py:460-481
mutual_matchesConfirmed reciprocal match (two hashes + timestamps)Chat authorizationbackend/app/models/__init__.py:483-542
pair_cooldownsDirection-less pair hash + cooldown expiryAbuse/persistence preventionbackend/app/models/__init__.py:433-458
push_tokensExpo push token + platformNotification deliverybackend/app/models/__init__.py:274-293
payments, iap_transactionsPlan/product, amount, status, SHA-256 hash of the receipt (the raw receipt is never stored)Purchase verification, accountingbackend/app/models/__init__.py:111-138, 372-415
abuse_reportsReporter/reported DID, category (harassment/scam/fake profile/spam/inappropriate content/other), free-text description (user-authored), evidence hashModerationbackend/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 setupbackend/app/api/signaling.py
TURN relay serverRelays 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)Connectivitymemory-bank/Features/P2P-Chat.md; self-hosted coturn
telemetry table / TelemetryServiceDevice id, session id, metric name/value, app/OS versionProduct analyticsbackend/app/models/__init__.py:141-165see §8, honest DP disclosure
device_attributions, revenue_sharesSHA-256 hash of device identifier (IDFV/Android ID), influencer code, revenue shareInfluencer attribution systembackend/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


5. Retention periods

DataPeriodEvidence / status
match_outcomes (unreciprocated)Auto-deleted after 30 days_sweep_expired_outcome_tables(), backend/app/models/__init__.py:466
mutual_matchesDeleted after 90 days of activity (chat) inactivitybackend/app/models/__init__.py:501-515
pair_cooldownsno_match: 30 days, incomplete: 7 days, max 3 retries per 90 daysagent-represented-matching.md
location_snapshotsNo 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_reportsNot deleted on account deletion; the DID link is irreversibly anonymized (see §6). Retained indefinitely for financial/moderation purposes.
Everything else account-relatedDeleted 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:

  1. 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).
  2. 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.
  3. 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:

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


10. Security


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