Wybber Safety and Moderation Policy
Last updated: 2026-10-02
This document describes what Wybber actually does today for user safety; it does not promise any feature that does not yet exist. Final legal approval rests with the company's lawyer. The Turkish version of this document (safety-policy.md) is the primary text; this English version is meant to be identical in meaning.
1. Identity and Age
- Wybber is open only to users aged 18 and over. The sign-up screen shows the statement "By continuing, you declare that you are over 18."
- Age is based on the age band declared at sign-up (e.g. "18-22"); no official ID/document scan is performed — this is a limit that is common in the industry and is stated honestly, as also explained in
age-rating.md. - Accounts are tied to a cryptographic identity (DID, Ed25519 key pair); an email address or phone number is not asked for at sign-up. The identity is derived from a recovery phrase (BIP39 seed); the same recovery phrase regenerates the same DID on a new device, or the identity can be transferred device-to-device via QR (see
docs/security/2026-09-19-recovery-restore-spec.md).
2. Profile Photo Verification — the "Liveness verified" Badge
- Users can optionally pass a camera liveness test: blinking, smiling, and turning the head left and right are all requested. For the verification to be accepted, more than one of these steps must be passed; which step failed is not shown in detail on screen.
- It is processed entirely on the device: Apple Vision on iOS, Google ML Kit Face Detection on Android. The raw photo, video frame, or face embedding is never sent to the backend; the files of the verification flow contain no call that sends these over the network (evidence:
mobile-app/src/services/FaceVerificationService.ts,faceBadgeClaim.ts,mobile-app/src/screens/FaceVerificationScreen.tsx, native bridgesmobile-app/ios/Wybber/FaceVerificationModule.mm,android/.../FaceVerificationModule.kt). - Comparison with the profile photo uses a measured method only on iOS: on iOS the live face is compared with the profile photo on the device using a face-recognition model (SFace, a MobileFaceNet-based CNN, CoreML). Android has no such calibrated face-comparison model (it has only a method based on face geometry that has not been calibrated); therefore on Android the badge does not carry a claim of matching the profile photo.
- The result is stored on the device as a signed local attestation: verification time, method, liveness result and the kind of evidence, match score, comparison summary,
did,publicKey,hash= SHA256(payload),signature= Ed25519 over that hash (with the user's own DID key). The raw photo/video/embedding never enters this record; the record is kept encrypted on the device and does not go over the network (FaceVerificationService.ts::signAndSaveAttestation). - What the badge does and does not say: in both languages the badge reads "Canlılık doğrulandı" / "Liveness verified" — it is never "identity verified"; the underlying system is not an identity-verification system, it is a "a live person is in front of the camera" test. A match with the profile photo is claimed only when the measured method (iOS) ran and the primary profile photo matched; otherwise the badge reads "Liveness verified — profile photo not matched" (
mobile-app/src/screens/VerifiedBadgeScreen.tsx,mobile-app/src/components/VerifiedBadge.tsx,faceBadgeClaim.ts). - Honest limit: the badge is a signal that lowers the chance of someone using a fake/old photo or recording; it is not an absolute guarantee. Anti-spoofing (presentation-attack) detection relies on on-device texture analysis, but its threshold constants have not yet been calibrated on large-scale real samples; it does not give full protection against presentation attacks such as a video played from a screen or a carefully prepared printout (evidence:
memory-bank/Features/Face-Verification.md).
3. Reporting (Report) and Blocking (Block)
The path in the app. To report or block the other person, you use the "Report" and "Block" buttons in the top bar of an open chat (mobile-app/src/screens/HumanChatScreen.tsx; "Report" opens the full flow in mobile-app/src/screens/ReportBlockScreen.tsx). These buttons are reachable only after a match has been opened for chat; there is no separate report/block screen during a Wybber negotiation. There is no report screen in Settings; Settings → Blocked users lists the people you have blocked and lets you unblock them (BlockedUsersScreen.tsx). The rest of this document describes what happens behind the scenes (backend POST /community-safety/report — backend/app/api/community_safety_v1.py; app side mobile-app/src/services/CommunitySafetyGuardianService.ts).
Report flow — what happens end to end:
- The user picks a category:
harassment,spam,fake_profile,inappropriate_content,scam,violence,other— a value outside these seven categories is rejected by the backend (community_safety_v1.py::_ALLOWED_REPORT_CATEGORIES). The violence, scam and harassment categories are sent with a single tap and the reported person is automatically blocked at the same time; the other categories ask for a second confirmation step and do not automatically block the person (ReportBlockScreen.tsx,CommunitySafetyGuardianService.ts::reportUser). - The device sends the reported person's identity only as SHA-256(DID). The reporter's identity is always determined on the server, as SHA-256(DID) derived from the reporter's own JWT — the "reporter" field sent by the client is deliberately ignored, so a user cannot frame someone else by posing as another reporter (
community_safety_v1.py::submit_report). - The backend writes the report durably to the
abuse_reportstable: reporter hash, reported hash, category, (if any) evidence hash, status (pending), creation time. A free-text description field is never filled by this endpoint — theAbuseReport.descriptioncolumn exists in the schema, but the only writer wired in production (submit_report) always leaves it empty; the report is entirely meta-only. Because the app does not attach evidence (screenshot/message text) to a report today, the evidence-hash field is also effectively empty; no content/evidence ever goes to the server (evidence:backend/app/models/__init__.py::AbuseReport,community_safety_v1.py::submit_report). - If there is no internet or the server does not respond, the report is queued on the device and the app retries on its next launch/return to foreground (
CommunitySafetyGuardianService.ts::flushPendingReports); until it reaches the server it does not appear in theabuse_reportstable (and therefore not in your data export).
Seeing your own reports. The app has no separate "report history" screen. You can reach the reports you submitted through the data file you download with Settings → Privacy → "My data" (wybber-data-<date>.json): the abuse_reports_filed_by_me list in the file's server section contains, for each report you submitted, the hashed identity of the reported person, the category, the evidence hash, the status and the timestamp (GET /api/users/me/export; it can be downloaded once per hour). This list shows no name (the identity is hashed) and does not show the outcome of a report: the status field is written as pending today and the app has no screen or endpoint that updates it. Reports filed about you are not included in this file — this is a deliberate limit to protect the reporter (see Privacy Policy §6 for details).
Who reviews, and how fast: Honesty is required here. A report is written to the abuse_reports table as pending; there is no automatic email, no entry that lands in a support inbox, no moderation panel or queue — the application contains no component that reads this table and notifies a person. Reports can be reviewed only by a Wybber operator with access to the server database, by querying that table by hand. We therefore do not give a review-time commitment, and we also do not claim that every report is reviewed by a human. In an emergency call your local emergency number (see §6); do not rely on the report channel for urgent intervention. To contact us outside of a report: support@wybber.com.
Block semantics:
- Any user can unilaterally block another. A block is first saved on the device and then sent to the server (
POST /api/blocks/by-hash/{did_sha256}—backend/app/api/blocks.py); if the server cannot be reached it is queued on the device and retried (HumanChatService.ts::blockPeerByDid/flushPendingBlockSync). The matching/signaling exclusions below take effect once the server record exists. - Blocking is silent and one-sided: the app does not send the blocked person a message or notification, and neither does the server. The blocking side can see and remove their own block list; the blocked side should know that the app will not tell them — they simply cannot see/match/message the other person again.
- At the moment of a block, any existing mutual-match state between the two users is also deleted (
backend/app/api/matching.py::purge_mutual_state_for_block, in the same transaction asblocks.py::_do_block) — so "I was blocked" and "the match ended" become indistinguishable to the other side; this is intentional (to avoid creating a "block oracle"). Unblocking does not restore the deleted match. - Matching/signaling queries always check the mutual block list (
matching.py::_is_blocked_pair,is_mutual_match): the blocked side is neither suggested as a candidate for a new Wybber round nor able to establish a signaling (WebRTC) connection. - Even if an account is deleted (
DELETE /api/users/me), the reports that account gave or received are not deleted; however, the identity link on both sides is irreversibly anonymized (replaced with a one-way value carrying adeleted:prefix;backend/app/api/users.py,abuse_reportsrows) — this is to prevent abuse from being hidden by "deleting my account to hide my report history". Because their link to your account is severed, the reports of a deleted account also do not appear in a later data export of yours.
4. Automated Policy Checking (Guardian) — on the device
- A Guardian policy engine running on the device (
mobile-app/src/services/GuardianPolicyEngine.ts) checks: the outputs of the Wybber negotiation, and the chat messages you send — before a message goes to the other side it passes through a rule set on your device (HumanChatService.ts::sendMessage). Phone numbers, email addresses and card numbers in a message are replaced with[PHONE]/[EMAIL]/[CARD]before it goes to the other side; messages that match spam, profanity/hate-speech and personal-information-request patterns are not sent; there are warning rules for meeting and money-request patterns. The engine also has topic-based cooldowns and topic-based explicit-consent gates, plus prompt-injection classification and automatic repair. - Honest limit: today's content-blocking rules are mainly English keyword/pattern matching; they are not a classifier that comprehensively catches profanity/harassment in Turkish and other languages. Guardian is not full content moderation — user reporting and blocking (§3) are the main protection mechanism.
- Guardian checks text on the user's own device, not content that reaches the backend; Guardian decisions/logs are kept on the device. In the code scan done while preparing this document, no caller was found in the app's live flows that sends these decisions to the server (scanned: the callers of the server-
fetch-ing methods of the Guardian decision-record and transparency services); the scan is an absence-of-evidence scan, not a formal data-flow audit. - The Wybber negotiation/round is conducted with deterministic, predefined category codes (LLM output is not in the decision path); free text is not forwarded to the other device (evidence:
agents/agent-represented-matching.md).
5. The Server Has No Access to Chats — What That Means for Law-Enforcement Requests
- All chat messages between two matched users are end-to-end encrypted (E2EE: X25519 key exchange + XSalsa20-Poly1305,
nacl.secretbox) and travel device-to-device over WebRTC. If a direct connection cannot be established, traffic passes through Wybber's own self-hosted TURN relay server; the relay cannot see message content (the traffic is end-to-end encrypted) but sees your IP address during the connection. The ICE candidates sent while the connection is set up also carry IPs; they pass through the signaling server at transfer time and are not stored permanently (see Privacy Policy §3.2). - In the
conversationstable the backend keeps only metadata about who talked with whom (internal participant ids), when, and whether the session is active; there is no server-side column for message content (evidence:backend/app/models/__init__.py,Conversation). - As a result: Wybber cannot provide the content of a user's chats in response to any request, including a court order — because this content never reached the company's servers and the company holds no decryption key. What can lawfully be shared is the metadata actually held on the server; for example: account registration information (DID, gender/interested-in gender/age band declared at sign-up, chosen province/district), who matched/talked with whom and when, push-notification tokens, purchase records (product, the store's transaction id, receipt hash, status), and the report records in the
abuse_reportstable (with hashed identities, without content). This list is an example; the complete and current list of what is held on the server is in the table in Privacy Policy §3. This limit is explained with the same clarity to users and to the competent authorities.
6. Meeting Safety — Advice for Users
The app does not automatically show this advice when a match is opened for chat; it is general advice in this document:
- Hold first meetings in a public place.
- Tell someone you trust about your plan.
- Arrange your own transport.
- When you see suspicious behavior (a request for money, an excessive request for personal information at an early stage, an inconsistent story), use the "Report" / "Block" buttons (§3).
- In an emergency call your local emergency number; Wybber is not an emergency service.
7. Scope and Method Note
This document was verified by directly reading the following code paths: backend/app/api/community_safety_v1.py (mounted, the real report writer), backend/app/api/blocks.py, backend/app/api/matching.py (purge_mutual_state_for_block, _is_blocked_pair), backend/app/models/__init__.py (AbuseReport, Block, Conversation), backend/app/api/users.py (delete_me/export_my_data), mobile-app/src/screens/{HumanChatScreen,ReportBlockScreen,BlockedUsersScreen,SettingsScreen}.tsx, mobile-app/src/services/{FaceVerificationService,faceBadgeClaim,GuardianPolicyEngine,CommunitySafetyGuardianService,HumanChatService,GDPRDataSubjectRightsService}.ts, memory-bank/Features/Face-Verification.md. An older/alternative report endpoint, backend/app/api/trust_safety.py, is not mounted in production (_INTERNAL_OPS_ROUTERS) — this document describes only the live community-safety/report flow.
Related Documents
privacy-policy.tr.md/.en.md— full data flowterms-of-service.tr.md/.en.mdage-rating.md— age ratingapp-privacy-nutrition-label.md— App Store "App Privacy" answers