myowjaYOY/social-app
April 19, 2026
Application: social-app (Bluesky / AT Protocol) Document Title: Data Flow & Privacy Map (Mobile) Date: April 2026 Applicable Regulations: GDPR (EU) 2016/679, CCPA/CPRA (California), Apple App Store Privacy Nutrition Label requirements, Google Play Data Safety requirements Platform Scope: iOS and Android (React Native / Expo); web platform noted where relevant Document Status: Working Document — DPO Review Required Before Distribution
Generated by DocAgent — automated codebase documentation analysis. Subject matter expert review is recommended before distribution.
This Data Flow & Privacy Map covers ten screens of the social-app (Bluesky) React Native mobile application, spanning the authentication flow (login, password reset, account selection) and the direct messaging feature (conversation view, inbox, chat list, and messaging settings). The overall privacy posture of the documented screens is assessed as Moderate. The application demonstrates several privacy-positive practices — notably the use of in-memory-only handling for passwords and reset tokens, server-side moderation enforcement, and email verification gating for messaging features. However, several areas require investigation and remediation before the application can be considered fully compliant with GDPR Article 32 and MASVS-STOR-1 requirements.
Across the ten documented screens, twelve data categories were identified: account credentials (username/handle, password), email address, authentication tokens (access JWT), two-factor authentication codes, reset tokens, user profile data (display name, handle, DID), direct message content, conversation metadata, messaging preferences, notification preferences, and analytics/behavioral data. The most privacy-sensitive categories are authentication tokens and direct message content. On-device storage mechanisms documented include the session state layer (which abstracts Keychain/Keystore access) and a custom native module for notification preferences (backed by UserDefaults on iOS / SharedPreferences on Android). No direct AsyncStorage or MMKV usage for sensitive data was documented within the ten screens reviewed; however, the session state layer's underlying storage implementation is not documented at the screen level and requires investigation by the iOS/Android leads. One device permission data flow was identified: push notification permission requested post-login.
The top three highest-risk privacy concerns are: (1) The session state layer's on-device storage mechanism for authentication tokens (accessJwt) is not documented at the screen level — if tokens are stored in AsyncStorage rather than Keychain/Keystore, this constitutes a Critical MASVS-STOR-1 and GDPR Article 32 violation; (2) Direct message content is processed in real-time via ConvoProvider and its transport mechanism (WebSocket or polling) is not documented, creating uncertainty about whether message content is transmitted over TLS and whether it is stored on-device between sessions; (3) The analytics abstraction (ax.metric) fires behavioral events (login success, password reset outcomes, chat open) whose destination service, data minimization posture, and GDPR legal basis are not documented.
Assessment limitations: This document is based exclusively on per-screen technical documentation review. It does not reflect live data flow tracing, network traffic analysis, database inspection, or device forensics. All findings marked "(documented behavior — verify with data flow analysis)" or "(inferred from [source] — verify against [check])" require live verification before regulatory reliance.
| Framework | Application |
|---|---|
| GDPR (EU) 2016/679 | Articles 5, 6, 9, 13, 17, 25, 30, 32, 35 — lawfulness, data minimization, right to erasure, privacy by design, ROPA, security, DPIA trigger assessment |
| CCPA/CPRA (California) | Consumer rights mapping, sale/sharing of personal information assessment, sensitive personal information categories |
| Apple App Store Privacy Nutrition Labels | App Privacy section — data types collected, data linked to user, data used to track |
| Google Play Data Safety | Data collected, data shared, security practices, data deletion |
ISO/IEC 27701:2019 — Privacy Information Management System. Controls referenced for processing inventory, data subject rights, and third-party processor management.
NIST Privacy Framework v1.0 — Five core functions applied throughout: Identify (data inventory), Govern (legal basis, policies), Control (data subject rights), Communicate (transparency, consent), Protect (technical safeguards).
OWASP MASVS-STOR (Mobile Application Security Verification Standard — Storage) — specifically MASVS-STOR-1 (sensitive data not stored in unprotected locations) and MASVS-STOR-2 (sensitive data not stored in backup-eligible locations without justification).
In scope: All ten screens provided in the documentation:
Mobile-specific scope additions: On-device storage mechanisms, device permission data collection (push notifications documented; camera/microphone/location not documented in these screens), biometric data handling, backup/restore behavior, native module storage.
Out of scope: Screens not provided in the documentation (profile screens, feed screens, onboarding, settings beyond messaging, server-side infrastructure, CDN, analytics backend).
This assessment is based on technical documentation review only. The following cannot be verified without live analysis:
ConvoProvider stores message content on-device between sessionsThe ten documented screens do not directly reference AsyncStorage, MMKV, or expo-secure-store by name in their component-level code. Storage is abstracted behind the session state layer (useSession/useSessionApi from #/state/session) and a custom native module (ExpoBackgroundNotificationHandler). The table below reflects what is documented and what requires investigation.
| Storage Mechanism | Encrypted | Data Stored | Survives Uninstall | Backup Eligible | Privacy Risk | MASVS-STOR |
|---|---|---|---|---|---|---|
Session state layer (#/state/session) — underlying mechanism not documented at screen level |
Not documented — requires investigation | Authentication tokens (accessJwt), account DID, handle, stored account list |
Not documented — depends on underlying mechanism | Not documented — depends on underlying mechanism | Critical if AsyncStorage; Low if Keychain/Keystore | MASVS-STOR-1 — requires verification |
ExpoBackgroundNotificationHandler native module — iOS: UserDefaults or shared app group; Android: SharedPreferences (inferred from documentation description of "native key-value storage") |
No — UserDefaults and SharedPreferences are not encrypted by default (inferred from platform behavior — verify against native module source) |
playSoundChat boolean preference |
iOS: persists (UserDefaults survives uninstall on some configurations); Android: cleared on uninstall (inferred from platform behavior — verify against native module source) | iOS: may be included in iCloud backup if not excluded; Android: may be included in Google Drive auto-backup | Low — non-sensitive boolean preference | MASVS-STOR-1 — low risk for this data type |
| React component state (in-memory) | N/A — in-memory only | Passwords, reset tokens, 2FA codes, email addresses, form field values — held during screen lifecycle only | No — cleared on app close | No | Low — ephemeral, never persisted | N/A |
| TanStack Query cache (in-memory) | N/A — in-memory only | User profiles, conversation lists, message metadata | No — cleared on app close | No | Low — ephemeral | N/A |
[Not documented — WHO: iOS lead and Android lead; WHAT: What is the underlying storage mechanism used by #/state/session for accessJwt and the stored account list? Is it AsyncStorage, MMKV, Keychain (iOS) / Keystore (Android), or SecureStore? Please provide the storage key names and encryption status.; WHERE: Insert in Section 4.1 Storage Mechanism Inventory — Session state layer row, and in Data Retention & Deletion table — DC-003 row]
[Not documented — WHO: iOS lead; WHAT: Does the ExpoBackgroundNotificationHandler native module use a shared app group container for UserDefaults, and is the data excluded from iCloud backup via NSURLIsExcludedFromBackupKey?; WHERE: Insert in Section 4.1 Storage Mechanism Inventory — ExpoBackgroundNotificationHandler row, and Section 4.5 Backup and Restore Consent table]
⚠️ Critical finding: The ChooseAccountForm screen reads account.accessJwt from the session store to determine whether a stored session token exists. This confirms that authentication tokens are persisted on-device between app sessions. The storage mechanism for these tokens is the single most important privacy and security question for this application. If tokens are stored in AsyncStorage, they are stored in plain-text SQLite on the device, included in device backups, and accessible to any process with file system access on a rooted/jailbroken device — a Critical MASVS-STOR-1 violation and GDPR Article 32 failure.
No biometric authentication is documented in any of the ten reviewed screens. The Login Form documentation explicitly states: "No biometric auth, jailbreak detection, or certificate pinning is implemented in this component." The same statement appears in the security sections of the Password Updated Form, Set New Password Form, Forgot Password Form, and Choose Account Form.
| API / Library | Biometric Data Accessed | Data Stored On-Device | Template Stored by App | Privacy Risk |
|---|---|---|---|---|
| None documented in reviewed screens | N/A | N/A | N/A | N/A — no biometric processing identified in these screens |
GDPR Article 9 assessment: No biometric special category data processing is documented in the ten reviewed screens. If biometric authentication is implemented in other screens not covered by this document (e.g., a device unlock screen or profile verification flow), a separate DPIA would be required.
[Not documented — WHO: iOS lead and Android lead; WHAT: Does the application use Face ID, Touch ID, or Android Fingerprint authentication anywhere in the app (including screens not covered in this document)? If so, which library is used (expo-local-authentication, react-native-biometrics, or custom)?; WHERE: Insert in Section 4.2 Biometric Data Handling table, and add a new ROPA entry if applicable]
No direct references to iOS Secure Enclave (SecKeyCreateRandomKey with kSecAttrTokenIDSecureEnclave) or Android Keystore (KeyPairGenerator with AndroidKeyStore provider) are documented in the ten reviewed screens. If the session state layer uses expo-secure-store or react-native-keychain, these libraries automatically use the Secure Enclave/Keystore on supported devices (inferred from expo-secure-store and react-native-keychain library behavior — verify against the session state layer implementation in #/state/session).
[Not documented — WHO: iOS lead; WHAT: Does #/state/session use expo-secure-store, react-native-keychain, or a custom Keychain wrapper for storing accessJwt? If so, what kSecAttrAccessible access class is used (e.g., kSecAttrAccessibleWhenUnlockedThisDeviceOnly vs. kSecAttrAccessibleAlways)?; WHERE: Insert in Section 4.3 and Section 4.4 App Uninstall Data Cleanup table]
| Storage | Behavior on Uninstall | Residual Data Risk | Recommendation |
|---|---|---|---|
| Session state layer (underlying mechanism unknown) | Not documented — if AsyncStorage: cleared automatically; if iOS Keychain: persists by default unless kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly is used |
If Keychain: accessJwt and stored account list may persist after uninstall — potential unauthorized access on device re-use or resale |
Investigate storage mechanism immediately (see DF-001); if Keychain, explicitly delete all items on logout and on first launch after reinstall |
| React component state | Cleared automatically on app close | None | N/A |
| TanStack Query cache | Cleared automatically on app close | None | N/A |
ExpoBackgroundNotificationHandler (UserDefaults / SharedPreferences) |
iOS: behavior depends on whether shared app group is used; Android: cleared on uninstall (inferred from platform behavior — verify against native module source) | iOS: playSoundChat preference may persist — low risk (non-sensitive) |
Low priority; document behavior for completeness |
| Platform | Storage | Backup Behavior | User Consent | Risk |
|---|---|---|---|---|
| iOS | Session state layer (if AsyncStorage) | Included in iCloud backup by default | User controls iCloud backup in iOS Settings | accessJwt and account list may be backed up to iCloud and restored to a different device — session token replay risk |
| iOS | Session state layer (if Keychain) | Excluded from iCloud backup by default (unless kSecAttrAccessible allows it) |
N/A | Low if Keychain is used correctly |
| iOS | ExpoBackgroundNotificationHandler (UserDefaults) |
Included in iCloud backup unless excluded via NSURLIsExcludedFromBackupKey |
User controls iCloud backup | Low — playSoundChat boolean is non-sensitive |
| Android | Session state layer (if AsyncStorage) | Included in Google Drive auto-backup (Android 6+) | User controls Google account backup | accessJwt may be backed up to Google Drive — session token replay risk |
| Android | Session state layer (if Keystore) | Not backed up | N/A | Low |
| Android | ExpoBackgroundNotificationHandler (SharedPreferences) |
May be included in Google Drive auto-backup depending on backup rules configuration | User controls Google account backup | Low — non-sensitive preference |
⚠️ Backup exposure risk: If accessJwt is stored in AsyncStorage, it is included in both iOS iCloud backups and Android Google Drive auto-backups by default. Restoring a device backup to a new device would restore the session token, potentially allowing unauthorized access to the account from the new device. This is a GDPR Article 32 concern (appropriate technical measures) and a MASVS-STOR-2 concern.
| Field | Value |
|---|---|
| Permission | Push Notifications |
| iOS API | expo-notifications / requestNotificationsPermission (from #/lib/notifications/notifications) — UNUserNotificationCenter.requestAuthorization |
| Android API | POST_NOTIFICATIONS (Android 13+); implicit on earlier versions |
| Data Collected | Device push notification token (APNs token on iOS, FCM registration token on Android); notification delivery metadata (notification ID, timestamp, payload) |
| GDPR Classification | Personal Data — device identifier linked to user account |
| Legal Basis | Consent — permission is requested post-login; user can deny |
| Purpose | Delivering push notifications for new direct messages and other social activity |
| Data Minimization | The permission request is triggered post-login (requestNotificationsPermission('Login')) rather than on app launch — this is a privacy-positive practice (contextually appropriate timing). The minimum permission level (notification delivery) is requested. |
| When Requested | After successful login — called in the success path of onPressNext in the Login Form |
| Denied Behavior | Handled within useRequestNotificationsPermission hook — not documented at the Login Form screen level; Login Form treats this as fire-and-forget |
| Permanently Denied Behavior | Not documented at the Login Form screen level — handled within useRequestNotificationsPermission |
| Data Destination | APNs (Apple Push Notification service) on iOS; FCM (Firebase Cloud Messaging) on Android; device token likely transmitted to the Bluesky/ATProto backend for notification routing |
| Screen Source | Login Form (/login/login-form) — Section 8 (post-login side effect) |
Source Evidence: Login Form (/login/login-form) — Section 5 (Post-login side effects) and Section 8 (Business Rules)
[Not documented — WHO: backend lead; WHAT: Is the APNs/FCM device token transmitted to the Bluesky PDS or a separate notification service? What data is included in the notification payload (e.g., does it include message content or only a notification ID)?; WHERE: Insert in PERM-001 Data Destination field and in Third-Party Data Sharing table]
[Not documented — WHO: iOS lead; WHAT: What happens when the user denies or permanently denies the notification permission? Is there a fallback UI or retry mechanism in useRequestNotificationsPermission?; WHERE: Insert in PERM-001 Denied Behavior and Permanently Denied Behavior fields]
Special assessment — Notification payload content: The Chat List screen documentation references decrementBadgeCount(convo.unreadCount) and pushToConversation route param, confirming that push notifications carry at minimum a conversation ID. Whether notification payloads include message content (which would constitute direct message content transmitted through APNs/FCM infrastructure) is not documented and requires investigation.
[Not documented — WHO: backend lead and iOS lead; WHAT: Do push notification payloads for chat messages include message text content, or only a conversation ID / notification type? If message content is included, this constitutes personal data (DC-009) transmitted through Apple/Google infrastructure and requires disclosure in the App Store Privacy Nutrition Label and Google Play Data Safety section.; WHERE: Insert in PERM-001 and in Third-Party Data Sharing table — APNs/FCM row]
| ID | Data Category | Classification | Examples Found | Collection Screens | Storage Location | On-Device Storage | Legal Basis |
|---|---|---|---|---|---|---|---|
| DC-001 | Username / Handle | Personal Data | ATProto handle (e.g., alice.bsky.social), bare username (e.g., alice) |
Login Form, Choose Account Form | Server (ATProto PDS) + On-device (session store) | Session state layer (mechanism not documented — requires investigation) | Performance of Contract (GDPR Art. 6(1)(b)) |
| DC-002 | Email Address | Personal Data | User's registered email address | Forgot Password Form, Set New Password Form (indirectly — reset code sent to email) | Server (ATProto PDS) | In-memory only (React state, discarded on unmount) — not persisted on-device by documented screens | Performance of Contract (GDPR Art. 6(1)(b)) |
| DC-003 | Authentication Token (accessJwt) | Sensitive Data | JSON Web Token for session authentication | Choose Account Form (reads token presence), Login Form (session created) | On-device (session state layer — mechanism not documented) + Server (ATProto PDS validates) | Session state layer (mechanism not documented — Critical: requires investigation) | Performance of Contract (GDPR Art. 6(1)(b)) |
| DC-004 | Password | Sensitive Data | User's account password (current or new) | Login Form, Set New Password Form | Never persisted — in-memory only (React ref / React state) | In-memory only — secureTextEntry={true} on all password fields |
Performance of Contract (GDPR Art. 6(1)(b)) |
| DC-005 | Two-Factor Authentication Code | Sensitive Data | 6-character OTP code sent to user's email | Login Form (2FA input) | Never persisted — in-memory only (React state authFactorToken) |
In-memory only | Performance of Contract (GDPR Art. 6(1)(b)) |
| DC-006 | Password Reset Token | Sensitive Data | XXXXX-XXXXX format one-time reset code |
Set New Password Form | Never persisted — in-memory only (React state resetCode) |
In-memory only | Performance of Contract (GDPR Art. 6(1)(b)) |
| DC-007 | User Profile Data | Personal Data | Display name, handle, DID, profile avatar, associated.chat.allowIncoming |
Conversation, Inbox, Chat List, Settings | Server (ATProto PDS) + TanStack Query in-memory cache | In-memory only (TanStack Query cache, cleared on app close) | Performance of Contract (GDPR Art. 6(1)(b)) |
| DC-008 | Decentralized Identifier (DID) | Personal Data | did:plc:abc123 — unique persistent account identifier |
All messaging screens, Choose Account Form, Settings | Server (ATProto PDS) + On-device (session state layer) | Session state layer (mechanism not documented) | Performance of Contract (GDPR Art. 6(1)(b)) |
| DC-009 | Direct Message Content | Sensitive Data | Text content of DM messages, embedded media references, reactions | Conversation, Inbox, Chat List | Server (ATProto Chat service) + In-memory (ConvoProvider / TanStack Query cache) | In-memory only during session (ConvoProvider) — on-device persistence between sessions not documented | Performance of Contract (GDPR Art. 6(1)(b)) |
| DC-010 | Conversation Metadata | Personal Data | Conversation ID, participant DIDs, unread count, last message timestamp, muted status, conversation status (accepted/request) | Conversation, Inbox, Chat List | Server (ATProto Chat service) + In-memory (TanStack Query cache) | In-memory only (TanStack Query cache) | Performance of Contract (GDPR Art. 6(1)(b)) |
| DC-011 | Messaging Preferences | Personal Data | allowIncoming setting (all/following/none) — controls who can message the user |
Settings | Server (ATProto PDS — actor declaration) + In-memory (React Query cache) | In-memory only (React Query cache) | Performance of Contract (GDPR Art. 6(1)(b)) |
| DC-012 | Notification Preferences | Personal Data | playSoundChat boolean — notification sound preference |
Settings | On-device only (native module — UserDefaults iOS / SharedPreferences Android) |
ExpoBackgroundNotificationHandler native storage |
Legitimate Interest (GDPR Art. 6(1)(f)) — device-local preference with no server transmission documented |
| DC-013 | Analytics / Behavioral Data | Personal Data | Login events (account:loggedIn, withPassword: false/true), password reset outcomes (signin:passwordResetSuccess/Failure), chat open events (chat:open, logContext: 'ChatsList') |
Login Form, Set New Password Form, Choose Account Form, Chat List | Analytics service (destination not documented) | Not documented — depends on analytics SDK | Consent or Legitimate Interest — legal basis not documented; requires investigation |
| DC-014 | Device Push Notification Token | Personal Data | APNs token (iOS), FCM registration token (Android) | Login Form (permission requested post-login) | Server (notification routing service — destination not documented) | Not documented — depends on notification SDK | Consent (GDPR Art. 6(1)(a)) — permission requested at runtime |
| Field | Value |
|---|---|
| Purpose | Authenticate a user's identity and establish an authenticated session on the ATProto PDS |
| Legal Basis | Performance of Contract (GDPR Art. 6(1)(b)) |
| Data Categories | DC-001 (Username/Handle), DC-002 (Email Address — used as identifier), DC-003 (Authentication Token), DC-004 (Password), DC-005 (2FA Code) |
| Data Subjects | Registered users of the Bluesky/ATProto application |
| Recipients | ATProto PDS (user's chosen hosting provider, e.g., bsky.social or self-hosted) |
| On-Device Processing | Handle expansion (createFullHandle) — bare username is expanded to full handle before transmission; password and identifier are lowercased/trimmed; 2FA token is trimmed |
| On-Device Storage | Authentication token (accessJwt) stored in session state layer — storage mechanism not documented (requires investigation); password and 2FA code held in-memory only and discarded after submission |
| Transfers | Cross-border transfer possible — user may be hosted on a PDS in any jurisdiction; serviceUrl is user-configurable. Transfer mechanism (SCCs, adequacy) not documented for third-party PDS instances. |
| Retention — Server | Not documented — WHO: backend lead; WHAT: What is the server-side retention period for session tokens and login audit logs?; WHERE: Insert in this ROPA entry |
| Retention — On-Device | Authentication token: until logout or app uninstall (if AsyncStorage) / potentially indefinitely post-uninstall (if iOS Keychain without explicit deletion) — requires investigation |
| Security Measures | secureTextEntry={true} on password field; autoCorrect={false}, autoCapitalize="none" on password field; credentials transmitted via ATProto SDK (TLS assumed — verify); cleanError() applied to error messages before display |
| Source Screen(s) | Login Form (/login/login-form) |
Source Evidence: Login Form (/login/login-form) — Sections 4, 5, 12, 14
| Field | Value |
|---|---|
| Purpose | Allow a user who has forgotten their password to reset it via an email-based verification flow |
| Legal Basis | Performance of Contract (GDPR Art. 6(1)(b)) |
| Data Categories | DC-002 (Email Address), DC-006 (Password Reset Token), DC-004 (New Password) |
| Data Subjects | Registered users initiating a password reset |
| Recipients | ATProto PDS (user's chosen hosting provider) |
| On-Device Processing | Email format validation (EmailValidator.validate); reset code format validation and normalization (checkAndFormatResetCode); password presence check |
| On-Device Storage | Email address, reset token, and new password held in React component state (in-memory only); discarded on component unmount. No persistence to any storage mechanism. |
| Transfers | Cross-border transfer possible — same as PA-001; serviceUrl is user-configurable |
| Retention — Server | Not documented — WHO: backend lead; WHAT: How long are password reset tokens valid, and are they invalidated immediately after use?; WHERE: Insert in this ROPA entry |
| Retention — On-Device | In-memory only — no on-device retention |
| Security Measures | secureTextEntry={true} on new password field; autoComplete="new-password" hint; cleanError() applied to error messages; Agent instantiated with null session (no existing token used or leaked); reset token transmitted directly to PDS |
| Source Screen(s) | Forgot Password Form (/login/forgot-password-form), Set New Password Form (/login/set-new-password-form), Password Updated Form (/login/password-updated-form) |
Source Evidence: Forgot Password Form — Sections 5, 7, 14; Set New Password Form — Sections 5, 7, 14
| Field | Value |
|---|---|
| Purpose | Allow a user to resume a previously authenticated session without re-entering credentials, using a stored authentication token |
| Legal Basis | Performance of Contract (GDPR Art. 6(1)(b)) |
| Data Categories | DC-001 (Username/Handle), DC-003 (Authentication Token), DC-008 (DID) |
| Data Subjects | Registered users with previously stored sessions |
| Recipients | ATProto PDS (user's chosen hosting provider) |
| On-Device Processing | Presence check on accessJwt; DID comparison with currentAccount.did; resumeSession call to exchange stored token for fresh session |
| On-Device Storage | Authentication token and account list read from session state layer (storage mechanism not documented — requires investigation) |
| Transfers | Cross-border transfer possible — same as PA-001 |
| Retention — Server | Not documented |
| Retention — On-Device | Until logout or app uninstall (mechanism-dependent — see Section 4.4) |
| Security Measures | Race condition guard (pendingDid check prevents concurrent resumption); error logging without credential exposure; graceful fallback to password form on failure |
| Source Screen(s) | Choose Account Form (/login/choose-account-form) |
Source Evidence: Choose Account Form (/login/choose-account-form) — Sections 4, 5, 8, 14
| Field | Value |
|---|---|
| Purpose | Enable authenticated users to send and receive one-on-one direct messages |
| Legal Basis | Performance of Contract (GDPR Art. 6(1)(b)) |
| Data Categories | DC-007 (User Profile Data), DC-008 (DID), DC-009 (Direct Message Content), DC-010 (Conversation Metadata) |
| Data Subjects | Authenticated users and their message recipients |
| Recipients | ATProto Chat service (Bluesky-operated or PDS-hosted); message recipients |
| On-Device Processing | Moderation decisions computed client-side (moderateProfile); display name sanitization (sanitizeDisplayName); last message preview text derivation; unread count tracking |
| On-Device Storage | Message content and conversation metadata held in-memory via ConvoProvider and TanStack Query cache; cleared on app close. On-device persistence between sessions not documented — requires investigation. |
| Transfers | Cross-border transfer possible — ATProto is a federated protocol; message recipients may be on PDS instances in any jurisdiction |
| Retention — Server | Not documented — WHO: backend lead; WHAT: What is the server-side retention period for direct message content and conversation metadata?; WHERE: Insert in this ROPA entry |
| Retention — On-Device | In-memory only during session (documented behavior — verify with data flow analysis); no documented on-device persistence between sessions |
| Security Measures | Email verification required before messaging; age assurance gate; moderation decisions applied client-side; ConvoProvider manages connection lifecycle; no direct auth token handling in screen-level code |
| Source Screen(s) | Conversation (/messages/conversation), Inbox (/messages/inbox), Chat List (/messages/chat-list) |
Source Evidence: Conversation — Sections 4, 5, 14; Inbox — Sections 4, 5, 14; Chat List — Sections 4, 5, 14
| Field | Value |
|---|---|
| Purpose | Allow users to review, accept, reject, block, and report incoming chat requests from other users |
| Legal Basis | Performance of Contract (GDPR Art. 6(1)(b)); Legitimate Interest for blocking/reporting (GDPR Art. 6(1)(f)) |
| Data Categories | DC-007 (User Profile Data of requester), DC-008 (DID), DC-009 (Message Content — preview), DC-010 (Conversation Metadata) |
| Data Subjects | Authenticated users (recipients) and chat requesters (third parties whose profile data is displayed) |
| Recipients | ATProto Chat service; ATProto moderation service (for block and report operations) |
| On-Device Processing | Moderation decisions (moderateProfile); display name sanitization; deleted account detection (handle === 'missing.invalid'); optimistic UI filtering (useLeftConvos) |
| On-Device Storage | In-memory only (TanStack Query cache) |
| Transfers | Cross-border transfer possible — same as PA-004 |
| Retention — Server | Not documented |
| Retention — On-Device | In-memory only during session |
| Security Measures | Email verification gate for accepting chats; age assurance gate; moderation integration; input sanitization on display names |
| Source Screen(s) | Inbox (/messages/inbox) |
Source Evidence: Inbox (/messages/inbox) — Sections 4, 5, 8, 14
| Field | Value |
|---|---|
| Purpose | Allow users to configure who can send them new messages and whether notification sounds play for chat |
| Legal Basis | Performance of Contract (GDPR Art. 6(1)(b)) for allowIncoming; Legitimate Interest (GDPR Art. 6(1)(f)) for playSoundChat |
| Data Categories | DC-007 (User Profile Data — associated.chat.allowIncoming), DC-011 (Messaging Preferences), DC-012 (Notification Preferences) |
| Data Subjects | Authenticated users |
| Recipients | ATProto PDS (for allowIncoming actor declaration update); no server transmission for playSoundChat (device-local only) |
| On-Device Processing | allowIncoming value derived from profile query; playSoundChat read from and written to native module storage |
| On-Device Storage | playSoundChat stored in ExpoBackgroundNotificationHandler native storage (UserDefaults iOS / SharedPreferences Android); allowIncoming in TanStack Query cache (in-memory) |
| Transfers | allowIncoming update transmitted to ATProto PDS — cross-border transfer possible |
| Retention — Server | allowIncoming retained as part of user's actor declaration on the PDS — no documented deletion mechanism |
| Retention — On-Device | playSoundChat: persists until app uninstall (Android) or potentially beyond (iOS UserDefaults) |
| Security Measures | Authenticated session required; closed set of valid values (no free-text input); Toast error feedback on mutation failure |
| Source Screen(s) | Settings (/messages/settings) |
Source Evidence: Settings (/messages/settings) — Sections 4, 5, 8, 14
| Field | Value |
|---|---|
| Purpose | Collect behavioral metrics for product analytics (login success rates, password reset outcomes, chat engagement) |
| Legal Basis | Not documented — WHO: DPO; WHAT: What is the legal basis for analytics event collection? If Consent, is a consent mechanism implemented? If Legitimate Interest, has a Legitimate Interest Assessment (LIA) been conducted?; WHERE: Insert in this ROPA entry |
| Data Categories | DC-013 (Analytics/Behavioral Data), DC-001 (Username/Handle — included in logContext), DC-008 (DID — potentially included in analytics payload) |
| Data Subjects | Authenticated and unauthenticated users |
| Recipients | Analytics service (destination not documented — requires investigation) |
| On-Device Processing | Event construction (event name + properties object) |
| On-Device Storage | Not documented — depends on analytics SDK (may queue events locally before transmission) |
| Transfers | Not documented — analytics service location unknown |
| Retention — Server | Not documented |
| Retention — On-Device | Not documented |
| Security Measures | Not documented |
| Source Screen(s) | Login Form, Set New Password Form, Choose Account Form, Chat List |
Source Evidence: Login Form — Section 5 (post-login side effects); Set New Password Form — Section 10; Choose Account Form — Section 10; Chat List — Section 10
| Field | Value |
|---|---|
| Purpose | Deliver push notifications to users for new direct messages and other social activity |
| Legal Basis | Consent (GDPR Art. 6(1)(a)) — OS-level permission requested post-login |
| Data Categories | DC-014 (Device Push Notification Token), DC-010 (Conversation Metadata — notification routing) |
| Data Subjects | Authenticated users who have granted notification permission |
| Recipients | Apple Push Notification service (APNs) on iOS; Firebase Cloud Messaging (FCM) on Android; Bluesky notification routing service (backend) |
| On-Device Processing | decrementBadgeCount called on conversation open; pushToConversation param processed on Chat List screen |
| On-Device Storage | Not documented — device token storage mechanism not documented |
| Transfers | APNs (Apple infrastructure, US-based); FCM (Google infrastructure, US-based) — cross-border transfer from EU users |
| Retention — Server | Not documented |
| Retention — On-Device | Not documented |
| Security Measures | Permission requested post-login (contextually appropriate); fire-and-forget (Login Form does not handle denial) |
| Source Screen(s) | Login Form (/login/login-form), Chat List (/messages/chat-list) |
Source Evidence: Login Form — Section 5 (post-login side effects), Section 8; Chat List — Section 10, Section 13
/login/form-containerData Profile:
| Aspect | Assessment |
|---|---|
| Data Collection | No |
| Data Display | No |
| Data Modification | No |
| Data Deletion | No |
| External Sharing | No |
| Sensitive Data | No |
| On-Device Storage | None |
| Device Permissions | None |
| Privacy Risk Level | Low |
Data Collection Points: None — this is a pure layout wrapper component.
Data Flows:
titleText (React.ReactNode), children (React.ReactNode), style, testID — all passed as props from parent screens. No personal data is received or processed by this component.useBreakpoints, useGutters). No data transformation.titleText on mobile viewports. titleText is typed as React.ReactNode — consuming screens are responsible for ensuring no PII is passed as the title in a way that could be logged or captured.Privacy Concerns: None identified. This component is a pure layout primitive with no data handling.
/login/login-formData Profile:
| Aspect | Assessment |
|---|---|
| Data Collection | Yes — username/handle, password, 2FA code |
| Data Display | Yes — pre-filled username handle (initialHandle), error messages |
| Data Modification | No |
| Data Deletion | No |
| External Sharing | Yes — credentials transmitted to ATProto PDS; notification permission requested post-login |
| Sensitive Data | Yes — password (DC-004), 2FA code (DC-005), authentication token created (DC-003) |
| On-Device Storage | Session state layer (mechanism not documented) — authentication token stored post-login |
| Device Permissions | Push Notifications (PERM-001) — requested post-login |
| Privacy Risk Level | High |
Data Collection Points:
| Field | Data Category | Required | Validation | Legal Basis | On-Device Storage | Notes |
|---|---|---|---|---|---|---|
| Username / email input | DC-001, DC-002 | Yes | Non-empty after trim; lowercased | Performance of Contract | In-memory only (React ref identifierValueRef) — never persisted by this component |
autoComplete="username" enables OS credential autofill |
| Password input | DC-004 | Yes | Non-empty | Performance of Contract | In-memory only (React ref passwordValueRef) — never persisted by this component |
secureTextEntry={true}, autoCorrect={false}, autoCapitalize="none" |
| 2FA code input | DC-005 | Conditional (server-driven) | Server-side only | Performance of Contract | In-memory only (React state authFactorToken) |
autoComplete="one-time-code" enables SMS/email OTP autofill; displayed uppercase but submitted as-typed |
Data Flows:
initialHandle prop (pre-filled username from parent — may be from previous session or deep link); serviceUrl and serviceDescription from parent; error string from parent.createFullHandle) — bare username expanded to full ATProto handle before transmission. Identifier lowercased and trimmed. 2FA token trimmed. No data is stored or logged during processing.accessJwt) created on successful login and stored by useSessionApi() in the session state layer. Storage mechanism not documented at this screen level — Critical: requires investigation (see DF-001).{ service: serviceUrl, identifier: fullIdent, password, authFactorToken } transmitted to ATProto PDS via com.atproto.server.createSession. Password is never logged.requestNotificationsPermission('Login') — device push notification token transmitted to APNs/FCM and Bluesky notification service (see PERM-001). Analytics events not documented as being fired directly from this component (may be fired within useSessionApi).initialHandle) displayed in username field. Error messages displayed via FormError after cleanError() sanitization.Privacy Concerns:
DF-001: Authentication Token Storage Mechanism Unknown
| Field | Value |
|---|---|
| Risk Level | Critical |
| Data Affected | DC-003 (Authentication Token), DC-001 (Username/Handle), DC-008 (DID) |
| Regulation | GDPR Art. 32 (security of processing), MASVS-STOR-1, MASVS-STOR-2 |
| Description | The Login Form creates an authenticated session via useSessionApi().login(). The resulting accessJwt is stored by the session state layer (#/state/session). The underlying storage mechanism (AsyncStorage, MMKV, Keychain, or SecureStore) is not documented at the screen level. If AsyncStorage is used, the token is stored in plain-text SQLite on the device, included in iOS iCloud and Android Google Drive backups, and accessible on rooted/jailbroken devices. |
| Evidence | Login Form Section 12: "No client-side persistence (AsyncStorage, MMKV, SecureStore) is used directly in this component." — This confirms the component itself does not persist, but the session layer does (confirmed by Choose Account Form reading account.accessJwt). |
| Recommendation | Investigate #/state/session storage implementation immediately. If AsyncStorage is used, migrate to expo-secure-store (wraps iOS Keychain / Android Keystore). See MASVS-STOR-1. Ensure kSecAttrAccessibleWhenUnlockedThisDeviceOnly is used on iOS to prevent Keychain persistence post-uninstall. |
DF-002: TODO Comment — Double Login
| Field | Value |
|---|---|
| Risk Level | Medium |
| Data Affected | DC-003 (Authentication Token), DC-004 (Password) |
| Regulation | GDPR Art. 5(1)(c) (data minimization), GDPR Art. 32 |
| Description | The Login Form source contains a // TODO remove double login comment on the login() call. If the login function is called twice under certain conditions, credentials may be transmitted to the PDS twice, potentially creating duplicate session tokens or audit log entries. |
| Evidence | Login Form Section 17: "A comment in the code explicitly marks the login() call with // TODO remove double login." |
| Recommendation | Investigate and resolve the double-login issue. Ensure the login() function is called exactly once per user submission. Credit: development team has documented awareness of this issue. |
DF-003: 2FA Token Case Handling Gap
| Field | Value |
|---|---|
| Risk Level | Low |
| Data Affected | DC-005 (2FA Code) |
| Regulation | GDPR Art. 5(1)(f) (integrity and confidentiality) |
| Description | The 2FA input displays text in uppercase via CSS textTransform: 'uppercase' (display-only), but the value submitted is whatever the user typed. If the server requires uppercase tokens, the submitted value may fail silently or be rejected, causing user confusion. The login() implementation should uppercase the value — this is not done in LoginForm. |
| Evidence | Login Form Section 17: "authFactorToken case handling: The 2FA input displays in uppercase via textTransform: 'uppercase' (CSS display only), but the value submitted is whatever the user typed." |
| Recommendation | Apply .toUpperCase() to authFactorToken.trim() before submission, or verify that the ATProto server accepts case-insensitive 2FA tokens. Credit: development team has documented awareness. |
DF-004: Notification Permission — Fire-and-Forget
| Field | Value |
|---|---|
| Risk Level | Medium |
| Data Affected | DC-014 (Device Push Notification Token) |
| Regulation | GDPR Art. 6(1)(a) (consent), GDPR Art. 13 (transparency) |
| Description | requestNotificationsPermission('Login') is called post-login as a fire-and-forget operation. The Login Form does not handle the permission result. There is no documented user-facing explanation of why notifications are needed before the permission dialog appears, which may not satisfy GDPR transparency requirements for consent. |
| Evidence | Login Form Section 10: "The handling of granted/denied/permanently-denied states is implemented within useRequestNotificationsPermission hook, not in LoginForm itself. LoginForm does not handle the permission result — it is fire-and-forget." |
| Recommendation | Ensure useRequestNotificationsPermission presents a pre-permission rationale screen before the OS dialog (iOS best practice). Document the denied/permanently-denied handling. Verify that the permission request meets GDPR transparency requirements (Art. 13). |
/login/password-updated-formData Profile:
| Aspect | Assessment |
|---|---|
| Data Collection | No |
| Data Display | No — static success message only |
| Data Modification | No |
| Data Deletion | No |
| External Sharing | No |
| Sensitive Data | No |
| On-Device Storage | None |
| Device Permissions | None |
| Privacy Risk Level | Low |
Data Collection Points: None — this is a static confirmation screen.
Data Flows:
Privacy Concerns: None identified. This screen is a pure confirmation view with no data handling.
/login/set-new-password-formData Profile:
| Aspect | Assessment |
|---|---|
| Data Collection | Yes — password reset token, new password |
| Data Display | No — no personal data displayed |
| Data Modification | Yes — user's password is changed on the PDS |
| Data Deletion | No |
| External Sharing | Yes — reset token and new password transmitted to ATProto PDS |
| Sensitive Data | Yes — DC-004 (new password), DC-006 (reset token) |
| On-Device Storage | None — all data in-memory only |
| Device Permissions | None |
| Privacy Risk Level | Medium |
Data Collection Points:
| Field | Data Category | Required | Validation | Legal Basis | On-Device Storage | Notes |
|---|---|---|---|---|---|---|
| Reset code input | DC-006 | Yes | checkAndFormatResetCode — must match XXXXX-XXXXX pattern |
Performance of Contract | In-memory only (React state resetCode) |
Auto-formatted on blur; autoFocus={true} |
| New password input | DC-004 | Yes | Non-empty only (no complexity rules — see DF-005) | Performance of Contract | In-memory only (React state password) |
secureTextEntry={true}, clearButtonMode="while-editing", autoComplete="new-password", passwordRules="minlength: 8;" |
Data Flows:
serviceUrl prop from parent; error string from parent. No personal data loaded from storage.checkAndFormatResetCode); password presence check. Agent instantiated with null session (no existing token used).{ token: formattedCode, password } transmitted to ATProto PDS via com.atproto.server.resetPassword (unauthenticated XRPC POST).ax.metric('signin:passwordResetSuccess', {}) and ax.metric('signin:passwordResetFailure', {}) — destination service not documented (see DF-007).FormError after cleanError() sanitization.Privacy Concerns:
DF-005: Weak Password Strength Validation
| Field | Value |
|---|---|
| Risk Level | Medium |
| Data Affected | DC-004 (Password) |
| Regulation | GDPR Art. 32 (appropriate technical measures), GDPR Art. 5(1)(f) (integrity and confidentiality) |
| Description | The Set New Password Form only validates that the password field is non-empty. No minimum length, complexity rules, or common-password checks are enforced client-side. The passwordRules="minlength: 8;" attribute is a hint to iOS password managers only and is not enforced by the app's validation logic. A user could set a single-character password that would only be rejected if the PDS enforces a policy. |
| Evidence | Set New Password Form Section 7: "There is a // TODO Better password strength check comment in the code — the current password validation only checks for non-empty." Section 17: "No minimum length enforcement, complexity rules, or common-password checks are implemented client-side." |
| Recommendation | Implement client-side password strength validation: minimum 8 characters, at least one uppercase letter, one number, and one special character. Consider integrating a common-password blocklist. Credit: development team has documented awareness via // TODO. |
DF-006: isProcessing Not Reset on Success — Potential Form Lock
| Field | Value |
|---|---|
| Risk Level | Low |
| Data Affected | DC-004 (Password), DC-006 (Reset Token) |
| Regulation | GDPR Art. 5(1)(e) (storage limitation — user should be able to correct errors) |
| Description | isProcessing is intentionally left as true after a successful API call, relying on the parent to navigate away. If onPasswordSet() does not navigate away (e.g., due to a parent bug), the form is permanently locked with no way for the user to retry. |
| Evidence | Set New Password Form Section 17: "If onPasswordSet() does not navigate away (e.g., due to a bug in the parent), the form will be permanently locked with no way for the user to retry." |
| Recommendation | Add a timeout-based reset of isProcessing as a safety net, or verify that onPasswordSet() always navigates away. Credit: development team has documented awareness. |
DF-007: Analytics Events — Destination and Legal Basis Undocumented
| Field | Value |
|---|---|
| Risk Level | High |
| Data Affected | DC-013 (Analytics/Behavioral Data) |
| Regulation | GDPR Art. 6 (lawfulness), GDPR Art. 13 (transparency), GDPR Art. 30 (ROPA) |
| Description | ax.metric('signin:passwordResetSuccess', {}) and ax.metric('signin:passwordResetFailure', {}) are fired from this screen. The analytics abstraction (useAnalytics from #/analytics) destination service, data transmitted, retention period, and legal basis are not documented in the screen documentation. This applies to all analytics events across the reviewed screens. |
| Evidence | Set New Password Form Section 13: "ax.metric('signin:passwordResetSuccess', {}) — on successful password reset. ax.metric('signin:passwordResetFailure', {}) — on client-side validation failure or API error." |
| Recommendation | Document the analytics service used (PostHog, Mixpanel, Amplitude, etc.), the data transmitted with each event (including whether user DID or handle is included), the retention period, and the legal basis. If Consent is the legal basis, implement a consent mechanism. Add the analytics service to the Third-Party Data Sharing table and the App Store Privacy Nutrition Label. |
/login/forgot-password-formData Profile:
| Aspect | Assessment |
|---|---|
| Data Collection | Yes — email address |
| Data Display | No — no personal data displayed |
| Data Modification | No — initiates server-side password reset request |
| Data Deletion | No |
| External Sharing | Yes — email address transmitted to ATProto PDS |
| Sensitive Data | No — email address is personal data but not special category |
| On-Device Storage | None — email address in-memory only |
| Device Permissions | None |
| Privacy Risk Level | Low |
Data Collection Points:
| Field | Data Category | Required | Validation | Legal Basis | On-Device Storage | Notes |
|---|---|---|---|---|---|---|
| Email address input | DC-002 | Yes | EmailValidator.validate(email) — RFC-compliant format check |
Performance of Contract | In-memory only (React state email) — discarded on unmount |
autoFocus={true}, autoCapitalize="none", autoCorrect={false}, autoComplete="email" |
Data Flows:
serviceUrl and serviceDescription from parent. No personal data loaded from storage.EmailValidator.validate). Agent instantiated with null session.{ email } transmitted to ATProto PDS via com.atproto.server.requestPasswordReset.cleanError().Privacy Concerns:
DF-008: Email Address Transmitted to User-Configurable PDS
| Field | Value |
|---|---|
| Risk Level | Medium |
| Data Affected | DC-002 (Email Address) |
| Regulation | GDPR Art. 46 (transfers to third countries), GDPR Art. 13 (transparency) |
| Description | The email address is transmitted to the serviceUrl — a user-configurable ATProto PDS URL. Users may be hosted on self-hosted or third-party PDS instances in any jurisdiction. The application does not document what transfer mechanism (SCCs, adequacy decision) applies when the PDS is outside the EEA. |
| Evidence | Forgot Password Form Section 5: "Service target: Dynamically set to serviceUrl prop via new Agent(null, { service: serviceUrl })." Section 8: "Dynamic service targeting: Rather than using a global or session-level agent, a new Agent instance is created with new Agent(null, { service: serviceUrl }) at submission time." |
| Recommendation | Document the data transfer mechanism for third-party PDS instances. Consider displaying a notice to users when their PDS is not operated by Bluesky, informing them that their data will be processed by a third-party operator. |
/login/choose-account-formData Profile:
| Aspect | Assessment |
|---|---|
| Data Collection | No — reads existing stored accounts |
| Data Display | Yes — stored account list (handles, display names, avatars) |
| Data Modification | No |
| Data Deletion | No |
| External Sharing | Yes — resumeSession transmits stored token to ATProto PDS |
| Sensitive Data | Yes — DC-003 (Authentication Token) read from session store |
| On-Device Storage | Session state layer (reads accessJwt and account list — mechanism not documented) |
| Device Permissions | None |
| Privacy Risk Level | High |
Data Collection Points: None — this screen reads existing stored data; it does not collect new data from the user.
Data Flows:
useSession). Each account includes did, handle, and accessJwt presence indicator.accessJwt presence check (determines whether to attempt session resumption or redirect to password form); DID comparison with currentAccount.did; resumeSession call.pendingDid is in-memory only.accessJwt transmitted to ATProto PDS via resumeSession for session validation/refresh.ax.metric('account:loggedIn', { logContext: 'ChooseAccountForm', withPassword: false }) — analytics event on successful session resumption (see DF-007).AccountList component — shows handles and potentially display names/avatars for all stored accounts.Privacy Concerns:
DF-009: Multiple Stored Accounts Displayed — Privacy Exposure on Shared Devices
| Field | Value |
|---|---|
| Risk Level | Medium |
| Data Affected | DC-001 (Username/Handle), DC-007 (User Profile Data), DC-008 (DID) |
| Regulation | GDPR Art. 5(1)(f) (integrity and confidentiality), GDPR Art. 25 (privacy by design) |
| Description | The Choose Account Form displays all stored accounts (handles, display names, potentially avatars) to anyone who opens the login screen. On a shared or lost device, this exposes the list of accounts associated with the device to unauthorized viewers. |
| Evidence | Choose Account Form Section 3: "AccountList — The primary interactive element. Renders the list of stored accounts." Section 14: "The component reads account.accessJwt from the session store to determine whether a stored session token exists." |
| Recommendation | Consider requiring device authentication (PIN, biometric) before displaying the account list, or masking handles partially (e.g., a***e.bsky.social). Assess whether this is acceptable under the app's threat model. |
DF-010: Session Resumption — Race Condition in Session API
| Field | Value |
|---|---|
| Risk Level | Low |
| Data Affected | DC-003 (Authentication Token) |
| Regulation | GDPR Art. 32 (security of processing) |
| Description | The code contains an inline comment: "The session API isn't resilient to race conditions so let's just ignore this." The pendingDid guard is a workaround, not a fix. Concurrent resumeSession calls could cause session state corruption. |
| Evidence | Choose Account Form Section 17: "Race condition comment: The code contains an inline comment: 'The session API isn't resilient to race conditions so let's just ignore this.'" |
| Recommendation | Harden the session API to be resilient to concurrent calls, or implement a proper mutex/lock mechanism. Credit: development team has documented awareness. |
/messages/conversationData Profile:
| Aspect | Assessment |
|---|---|
| Data Collection | Yes — message content composed and sent (within MessagesList component) |
| Data Display | Yes — message history, recipient profile, moderation state |
| Data Modification | Yes — messages can be sent |
| Data Deletion | No — deletion not documented at this screen level |
| External Sharing | Yes — message content transmitted to ATProto Chat service; email verification state checked |
| Sensitive Data | Yes — DC-009 (Direct Message Content), DC-007 (User Profile Data) |
| On-Device Storage | In-memory only (ConvoProvider, TanStack Query cache) — on-device persistence between sessions not documented |
| Device Permissions | None documented at this screen level (media attachments handled within MessagesList) |
| Privacy Risk Level | High |
Data Collection Points: Message composition is encapsulated within the MessagesList component — not directly visible at this screen level.
Data Flows:
ConvoProvider; recipient profile from useProfileQuery; moderation decisions from moderateProfile; email verification state from useEmail.moderateProfile computes blocking/moderation state client-side; useMaybeProfileShadow applies local profile mutations; readyToShow derived from scroll state and conversation status.ConvoProvider. No documented on-device persistence between sessions. setCurrentConvoId registers active conversation globally (in-memory).MessagesList (not documented at this screen level). setCurrentConvoId used for notification suppression.Privacy Concerns:
DF-011: ConvoProvider Transport Mechanism Not Documented
| Field | Value |
|---|---|
| Risk Level | High |
| Data Affected | DC-009 (Direct Message Content), DC-010 (Conversation Metadata) |
| Regulation | GDPR Art. 32 (security of processing — encryption in transit) |
| Description | ConvoProvider manages the full lifecycle of a DM conversation including connection, message history, and real-time updates. The specific transport mechanism (WebSocket, polling, Bluesky firehose) is described as "encapsulated in ConvoProvider" and is not documented. It is not confirmed whether message content is transmitted over TLS, whether end-to-end encryption is used, or whether message content is stored on-device between sessions. |
| Evidence | Conversation Section 11: "Real-time message delivery is managed entirely by ConvoProvider. The specific transport mechanism (WebSocket, polling, Bluesky firehose) is encapsulated in ConvoProvider." |
| Recommendation | Document the ConvoProvider transport mechanism. Confirm TLS is used for all message transmission. Assess whether end-to-end encryption is implemented or planned. Determine whether message content is cached on-device between sessions (if so, assess storage mechanism against MASVS-STOR-1). |
DF-012: Email Verification — Client-Side Only Gate
| Field | Value |
|---|---|
| Risk Level | Medium |
| Data Affected | DC-009 (Direct Message Content) |
| Regulation | GDPR Art. 32 (security of processing) |
| Description | Email verification is enforced client-side via needsEmailVerification check. The documentation notes: "the enforcement is purely UI-level — a determined user could potentially bypass the dialog by manipulating navigation state. Server-side enforcement in the API is the authoritative gate." |
| Evidence | Conversation Section 14: "The screen enforces email verification before allowing any messaging interaction. This is a server-side requirement surfaced client-side. However, the enforcement is purely UI-level." |
| Recommendation | Confirm that server-side enforcement of email verification is implemented in the ATProto Chat API. Document the server-side enforcement mechanism. The client-side gate is acceptable as a UX layer but must not be the sole enforcement mechanism. |
DF-013: HACKFIX Load-Bearing setTimeout
| Field | Value |
|---|---|
| Risk Level | Low |
| Data Affected | DC-002 (Email Address — email verification state) |
| Regulation | GDPR Art. 25 (privacy by design — reliability of privacy controls) |
| Description | The email verification dialog open call is wrapped in a setTimeout with no delay as a HACKFIX. This is a fragile timing dependency — if the shell listener's execution time changes, the email verification gate could break, potentially allowing unverified users to access messaging. |
| Evidence | Conversation Section 17: "The comment in InnerReady explicitly marks a setTimeout with no delay as a HACKFIX." |
| Recommendation | Refactor the email verification gate to use a reliable mechanism (e.g., a navigation guard or a proper state machine) rather than a timing-dependent workaround. Credit: development team has documented awareness. |
/messages/inboxData Profile:
| Aspect | Assessment |
|---|---|
| Data Collection | No — reads existing conversation data |
| Data Display | Yes — chat request list with sender profiles, message previews, timestamps |
| Data Modification | Yes — accept, reject, block, delete, mark as read |
| Data Deletion | Yes — leave/delete conversation |
| External Sharing | Yes — block and report operations transmitted to ATProto; accept transmitted to ATProto Chat service |
| Sensitive Data | Yes — DC-009 (Message Content preview), DC-007 (User Profile Data of requesters) |
| On-Device Storage | In-memory only (TanStack Query cache) |
| Device Permissions | None |
| Privacy Risk Level | Medium |
Data Collection Points: None — this screen reads and manages existing data.
Data Flows:
useListConvosQuery({ status: 'request' }); requester profiles from profile queries; moderation decisions from moderateProfile.missing.invalid members); unread detection; last message preview derivation; moderation-aware rendering.useLeftConvos.useAcceptConversation); leave/delete conversation (useLeaveConvo); block profile (useProfileBlockMutationQueue); mark as read (useMarkAsReadMutation); mark all as read (useUpdateAllRead).ReportDialog) — report data transmitted to ATProto moderation service.KnownFollowers (mutual followers displayed for social context).Privacy Concerns:
DF-014: Third-Party Profile Data Displayed Without Explicit Consent
| Field | Value |
|---|---|
| Risk Level | Medium |
| Data Affected | DC-007 (User Profile Data of chat requesters) |
| Regulation | GDPR Art. 6 (lawfulness — third-party data), GDPR Art. 13 (transparency) |
| Description | The Inbox displays profile data (display name, handle, avatar, KnownFollowers) of users who have sent chat requests. These are third parties who have not directly interacted with this screen. Their profile data is fetched and displayed as part of the chat request management flow. |
| Evidence | Inbox Section 3: "Each ChatListItem renders: PreviewableUserAvatar — 52×52 avatar... Display name (Text) + ProfileBadges... @handle... KnownFollowers." |
| Recommendation | This is standard social application behavior and is likely covered by the ATProto protocol's public profile data model. Ensure the privacy policy discloses that other users' public profile data is displayed in the messaging interface. Verify that KnownFollowers data (mutual followers) is sourced from public ATProto data and not from private contact lists. |
DF-015: Deleted Account Handling — Potential Bug with ConvoMenu
| Field | Value |
|---|---|
| Risk Level | Low |
| Data Affected | DC-010 (Conversation Metadata) |
| Regulation | GDPR Art. 17 (right to erasure — deleted account data handling) |
| Description | For deleted account conversations, the onPress handler calls menuControl.open(), but ConvoMenu is not rendered (showMenu={false}). This is a potential bug that could cause unexpected behavior. Additionally, conversations with deleted accounts (handle === 'missing.invalid') are displayed with "Deleted Account" as the display name — the underlying DID may still be stored and transmitted. |
| Evidence | Inbox Section 17: "showMenu={false} in Inbox: ChatListItem is rendered with showMenu={false} in RequestListItem, which means ConvoMenu is not rendered. However... the onPress handler for deleted accounts calls menuControl.open(), but since ConvoMenu is not rendered, this would open a menu that doesn't exist — potential bug." |
| Recommendation | Fix the deleted account press handler bug. Assess whether conversations with deleted accounts should be automatically removed from the inbox or retained for user reference. Ensure deleted account DIDs are not transmitted to analytics or third-party services. Credit: development team has documented awareness. |
/messages/chat-listData Profile:
| Aspect | Assessment |
|---|---|
| Data Collection | No — reads existing conversation data |
| Data Display | Yes — accepted conversation list with participant profiles, message previews, timestamps |
| Data Modification | Yes — mark as read, leave conversation, mute |
| Data Deletion | Yes — leave/delete conversation |
| External Sharing | Yes — mark as read, leave operations transmitted to ATProto; analytics event on chat open |
| Sensitive Data | Yes — DC-009 (Message Content preview), DC-007 (User Profile Data) |
| On-Device Storage | In-memory only (TanStack Query cache) |
| Device Permissions | None directly — push notification token used for pushToConversation routing (handled at app shell level) |
| Privacy Risk Level | Medium |
Data Collection Points: None — this screen reads and manages existing data.
Data Flows:
useListConvosQuery({ status: 'accepted' }); inbox/request conversations from useListConvosQuery({ status: 'request' }); participant profiles from profile queries; moderation decisions; pushToConversation route param from push notification handler.pushToConversation immediate navigation.precacheProfile and precacheConvoQuery write to React Query cache on row press (in-memory).useMarkAsReadMutation); leave conversation (via swipe gesture).ax.metric('chat:open', { logContext: 'ChatsList' }) — analytics event on conversation open (see DF-007).InboxPreview banner with avatar stack.Privacy Concerns:
DF-016: pushToConversation — Push Notification Payload Privacy
| Field | Value |
|---|---|
| Risk Level | Medium |
| Data Affected | DC-009 (Direct Message Content), DC-010 (Conversation Metadata), DC-014 (Device Push Notification Token) |
| Regulation | GDPR Art. 32 (security of processing), GDPR Art. 13 (transparency) |
| Description | The pushToConversation route param is injected by the push notification handler at the app shell level. The notification payload content (whether it includes message text or only a conversation ID) is not documented. If message content is included in the notification payload, it is transmitted through Apple/Google infrastructure (APNs/FCM) and may be stored in notification history on the device and in Apple/Google servers. |
| Evidence | Chat List Section 4: "pushToConversation (`string |
| Recommendation | Ensure push notification payloads contain only a conversation ID (not message content). Document the notification payload structure. Disclose APNs/FCM data transmission in the App Store Privacy Nutrition Label and Google Play Data Safety section. |
DF-017: Inbox Fetch Error Silently Ignored
| Field | Value |
|---|---|
| Risk Level | Low |
| Data Affected | DC-010 (Conversation Metadata) |
| Regulation | GDPR Art. 5(1)(d) (accuracy) |
| Description | Errors from refetchInbox() during pull-to-refresh are caught and logged but not surfaced to the user. If the inbox fetch fails, the InboxPreview banner may disappear or show stale data without any user-facing indication. |
| Evidence | Chat List Section 17: "Inbox Fetch Error Silently Ignored: Errors from refetchInbox() during pull-to-refresh are caught and logged but not surfaced to the user." |
| Recommendation | Surface inbox fetch errors to the user with a non-blocking indicator (e.g., a subtle error state on the InboxPreview banner). Credit: development team has documented awareness. |
/messages/settingsData Profile:
| Aspect | Assessment |
|---|---|
| Data Collection | No — modifies existing preferences |
| Data Display | Yes — current allowIncoming setting, current playSoundChat setting |
| Data Modification | Yes — allowIncoming actor declaration updated on PDS; playSoundChat written to native storage |
| Data Deletion | No |
| External Sharing | Yes — allowIncoming update transmitted to ATProto PDS |
| Sensitive Data | No — preferences are not sensitive data |
| On-Device Storage | ExpoBackgroundNotificationHandler native storage (UserDefaults / SharedPreferences) for playSoundChat |
| Device Permissions | None |
| Privacy Risk Level | Low |
Data Collection Points: None — this screen modifies existing preferences via radio button selection.
Data Flows:
associated.chat.allowIncoming) from useProfileQuery; playSoundChat preference from BackgroundNotificationHandler.getAllPrefsAsync().allowIncoming value derived from profile with 'following' fallback; playSoundChat converted to 'enabled'/'disabled' string for toggle display.playSoundChat written to ExpoBackgroundNotificationHandler native storage via BackgroundNotificationHandler.setBoolAsync('playSoundChat', value).allowIncoming update transmitted to ATProto PDS via useUpdateActorDeclaration.allowIncoming setting displayed as selected radio button; current playSoundChat setting displayed as selected radio button.Privacy Concerns:
DF-018: allowIncoming Setting — Privacy Control Effectiveness
| Field | Value |
|---|---|
| Risk Level | Low |
| Data Affected | DC-011 (Messaging Preferences) |
| Regulation | GDPR Art. 25 (privacy by design), GDPR Art. 17 (right to erasure — no documented deletion mechanism for actor declaration) |
| Description | The allowIncoming setting is a privacy control that determines who can message the user. The Admonition tip informs users that "ongoing conversations are unaffected by this setting." There is no documented mechanism for deleting the actor declaration (only updating it). If a user deletes their account, it is unclear whether the actor declaration is deleted from the PDS. |
| Evidence | Settings Section 3: "An Admonition component of type 'tip' below the radio group, informing the user that ongoing conversations are unaffected by this setting." Settings Section 17: "No optimistic rollback for actor declaration." |
| Recommendation | Document the deletion behavior of the actor declaration when a user deletes their account. Ensure the right to erasure (GDPR Art. 17) is implemented for actor declarations. |
DF-019: Native Module Error Handling Gap
| Field | Value |
|---|---|
| Risk Level | Low |
| Data Affected | DC-012 (Notification Preferences) |
| Regulation | GDPR Art. 5(1)(d) (accuracy — preference may be inaccurate if native write fails silently) |
| Description | BackgroundNotificationHandler.getAllPrefsAsync() and setBoolAsync() calls have no error handling. A native module failure would result in a silent error and potentially a stale or incorrect UI state — the user believes they have changed a preference that was not actually saved. |
| Evidence | Settings Section 17: "Unhandled native module errors: BackgroundNotificationHandler.getAllPrefsAsync() and setBoolAsync() calls in BackgroundNotificationPreferencesProvider have no try/catch or .catch() handlers." |
| Recommendation | Add try/catch error handling to native module calls. Show a Toast error if the preference write fails. Credit: development team has documented awareness. |
DF-020: Non-Null Assertion on currentAccount
| Field | Value |
|---|---|
| Risk Level | Low |
| Data Affected | DC-008 (DID) |
| Regulation | GDPR Art. 32 (security of processing — application stability) |
| Description | currentAccount!.did uses a TypeScript non-null assertion. If the session state is ever null when this screen renders (e.g., during logout race conditions), this will throw a runtime error rather than failing gracefully. |
| Evidence | Settings Section 17: "Non-null assertion on currentAccount: currentAccount!.did uses a TypeScript non-null assertion." |
| Recommendation | Replace currentAccount!.did with a defensive check: if (!currentAccount) return null. Credit: development team has documented awareness. |
| ID | Screen | Data Category | Direction | Destination | Storage Mechanism | Risk Level | Regulation | Recommendation Phase |
|---|---|---|---|---|---|---|---|---|
| DF-001 | Login Form | DC-003 (Auth Token) | On-Device | Session state layer (mechanism unknown) | Unknown — requires investigation | Critical | GDPR Art. 32, MASVS-STOR-1 | Phase 1 (Immediate) |
| DF-007 | Set New Password Form, Login Form, Choose Account Form, Chat List | DC-013 (Analytics) | Out — Third Party | Analytics service (unknown) | Unknown | High | GDPR Art. 6, Art. 13, Art. 30 | Phase 1 (Immediate) |
| DF-011 | Conversation | DC-009 (DM Content), DC-010 (Metadata) | Out — Server | ATProto Chat service | In-memory (ConvoProvider) | High | GDPR Art. 32 | Phase 1 (Immediate) |
| DF-004 | Login Form | DC-014 (Push Token) | Out — Third Party | APNs / FCM / Notification service | Unknown | Medium | GDPR Art. 6(1)(a), Art. 13 | Phase 2 (Short-Term) |
| DF-008 | Forgot Password Form | DC-002 (Email) | Out — Server | User-configurable ATProto PDS | In-memory only | Medium | GDPR Art. 46, Art. 13 | Phase 2 (Short-Term) |
| DF-009 | Choose Account Form | DC-001 (Handle), DC-007 (Profile), DC-008 (DID) | Display | On-screen (account list) | Session state layer | Medium | GDPR Art. 25, Art. 5(1)(f) | Phase 2 (Short-Term) |
| DF-012 | Conversation | DC-009 (DM Content) | Internal | Email verification gate | In-memory | Medium | GDPR Art. 32 | Phase 2 (Short-Term) |
| DF-014 | Inbox | DC-007 (Third-party profiles) | In — Server | ATProto API | In-memory (TanStack Query) | Medium | GDPR Art. 6, Art. 13 | Phase 3 (Medium-Term) |
| DF-016 | Chat List | DC-009 (DM Content), DC-014 (Push Token) | In — Push | APNs / FCM | Unknown | Medium | GDPR Art. 32, Art. 13 | Phase 2 (Short-Term) |
| DF-002 | Login Form | DC-003 (Auth Token), DC-004 (Password) | Out — Server | ATProto PDS | In-memory | Medium | GDPR Art. 5(1)(c), Art. 32 | Phase 2 (Short-Term) |
| DF-005 | Set New Password Form | DC-004 (Password) | Internal | Validation | In-memory | Medium | GDPR Art. 32, Art. 5(1)(f) | Phase 2 (Short-Term) |
| DF-013 | Conversation | DC-002 (Email verification) | Internal | Email dialog | In-memory | Low | GDPR Art. 25 | Phase 3 (Medium-Term) |
| DF-015 | Inbox | DC-010 (Conversation Metadata) | Internal | ConvoMenu (bug) | In-memory | Low | GDPR Art. 17 | Phase 3 (Medium-Term) |
| DF-017 | Chat List | DC-010 (Conversation Metadata) | In — Server | TanStack Query cache | In-memory | Low | GDPR Art. 5(1)(d) | Phase 4 (Backlog) |
| DF-018 | Settings | DC-011 (Messaging Preferences) | Out — Server | ATProto PDS | In-memory (React Query) | Low | GDPR Art. 25, Art. 17 | Phase 3 (Medium-Term) |
| DF-019 | Settings | DC-012 (Notification Preferences) | On-Device | Native module storage | UserDefaults / SharedPreferences | Low | GDPR Art. 5(1)(d) | Phase 3 (Medium-Term) |
| DF-020 | Settings | DC-008 (DID) | Internal | Session state | Session state layer | Low | GDPR Art. 32 | Phase 3 (Medium-Term) |
| DF-003 | Login Form | DC-005 (2FA Code) | Out — Server | ATProto PDS | In-memory | Low | GDPR Art. 5(1)(f) | Phase 3 (Medium-Term) |
| DF-006 | Set New Password Form | DC-004 (Password), DC-006 (Reset Token) | Internal | Form state | In-memory | Low | GDPR Art. 5(1)(e) | Phase 4 (Backlog) |
| DF-010 | Choose Account Form | DC-003 (Auth Token) | Internal | Session API | Session state layer | Low | GDPR Art. 32 | Phase 3 (Medium-Term) |
| Service | Integration Method | Data Shared | Purpose | Legal Basis | DPA Required | Transfer Mechanism | Risk Level |
|---|---|---|---|---|---|---|---|
ATProto PDS (user's chosen hosting provider — e.g., bsky.social or self-hosted) |
@atproto/api SDK — XRPC over HTTPS |
DC-001 (Handle), DC-002 (Email), DC-003 (Auth Token), DC-004 (Password — during login/reset only), DC-005 (2FA Code), DC-006 (Reset Token), DC-007 (Profile), DC-008 (DID), DC-009 (DM Content), DC-010 (Conversation Metadata), DC-011 (Messaging Preferences) | Core application functionality — authentication, messaging, profile management | Performance of Contract (GDPR Art. 6(1)(b)) | Yes — for third-party PDS operators; Bluesky-operated PDS covered by Bluesky's own privacy policy | SCCs or adequacy decision required for non-EEA PDS instances — not documented | High |
Analytics service (#/analytics abstraction — ax.metric) |
Internal SDK abstraction — underlying service not documented | DC-013 (Behavioral events: login, password reset, chat open) — whether DC-001/DC-008 are included in payloads is not documented | Product analytics and usage measurement | Not documented — requires investigation | Yes — if personal data is transmitted | Not documented | High |
| Apple Push Notification service (APNs) | iOS native — expo-notifications |
DC-014 (APNs device token); potentially DC-009/DC-010 if notification payload includes message content | Push notification delivery | Consent (GDPR Art. 6(1)(a)) | Standard Contractual Clauses (Apple DPA) | SCCs — Apple is a US-based processor | Medium |
| Firebase Cloud Messaging (FCM) | Android native — expo-notifications |
DC-014 (FCM registration token); potentially DC-009/DC-010 if notification payload includes message content | Push notification delivery | Consent (GDPR Art. 6(1)(a)) | Standard Contractual Clauses (Google DPA) | SCCs — Google is a US-based processor | Medium |
| ATProto Moderation Service | @atproto/api SDK |
DC-007 (Profile data of reported users), DC-009 (Reported message content), DC-010 (Conversation metadata) | Content moderation and abuse reporting | Legitimate Interest (GDPR Art. 6(1)(f)) | Covered by Bluesky's data processing terms | Same as ATProto PDS | Medium |
@bsky.app/expo-scroll-edge-effect |
Expo package (Bluesky-authored) | No personal data — visual effects only | Scroll edge visual effects | N/A | No | N/A | Low |
react-remove-scroll-bar |
npm package (web only) | No personal data — DOM manipulation only | Web scroll bar suppression | N/A | No | N/A | Low |
email-validator |
npm package | No data transmitted — client-side validation only | Email format validation | N/A | No | N/A | Low |
[Not documented — WHO: engineering lead; WHAT: What analytics service does #/analytics (ax.metric) transmit data to? Is it PostHog, Mixpanel, Amplitude, Firebase Analytics, or a custom service? What data fields are included in each event payload (specifically, are user DID or handle included)?; WHERE: Insert in Third-Party Data Sharing table — Analytics service row, and in PA-007 ROPA entry]
[Not documented — WHO: engineering lead; WHAT: Are there any crash reporting SDKs (Sentry, Bugsnag, Crashlytics) integrated into the application? If so, do crash payloads include app state that might contain PII (e.g., the current user's handle, message content from ConvoProvider state)?; WHERE: Insert as a new row in Third-Party Data Sharing table, and add a new DF entry if PII is included in crash payloads]
⚠️ This workflow continues beyond the documented screens. The ConvoProvider component manages real-time message delivery and its integration with the ATProto Chat service is not covered in this document. Verify the complete data flow for message transmission (including transport protocol, encryption, and server-side storage) with the development team before treating this as a comprehensive process description.
| Data Category | Retention — Server | Retention — On-Device Storage | Storage Mechanism | Deletion Mechanism | Right to Erasure | Notes |
|---|---|---|---|---|---|---|
| DC-001 (Username/Handle) | Not documented — WHO: backend lead; WHAT: What is the server-side retention period for user handles?; WHERE: Insert here | Until logout or app uninstall (if AsyncStorage) / potentially indefinitely post-uninstall (if iOS Keychain) | Session state layer (mechanism not documented) | Logout clears session state (mechanism-dependent); app uninstall clears AsyncStorage/MMKV but not iOS Keychain by default | Not documented — requires investigation | Handle is a public ATProto identifier; deletion may require account deletion on PDS |
| DC-002 (Email Address) | Not documented | In-memory only — discarded on component unmount | React state | Automatic on unmount | N/A — not persisted on-device | Email is stored server-side on the PDS; right to erasure requires server-side deletion |
| DC-003 (Authentication Token) | Not documented — WHO: backend lead; WHAT: What is the server-side session token lifetime and invalidation mechanism?; WHERE: Insert here | Until logout or app uninstall (mechanism-dependent — Critical: see DF-001) | Session state layer (mechanism not documented) | Logout should invalidate token server-side and clear on-device; iOS Keychain persistence post-uninstall is a risk if Keychain is used without kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly |
Partial — logout clears active session; residual Keychain items may persist post-uninstall | Most critical retention risk in this document |
| DC-004 (Password) | Not stored in plaintext server-side (assumed — verify) | In-memory only — discarded after submission | React ref / React state | Automatic on component unmount | N/A — not persisted | secureTextEntry={true} on all password fields |
| DC-005 (2FA Code) | Not applicable — one-time use | In-memory only — discarded after submission | React state | Automatic on component unmount | N/A — not persisted | Short-lived OTP |
| DC-006 (Password Reset Token) | Not documented — WHO: backend lead; WHAT: How long are reset tokens valid? Are they invalidated immediately after use?; WHERE: Insert here | In-memory only — discarded after submission | React state | Automatic on component unmount | N/A — not persisted | One-time use token |
| DC-007 (User Profile Data) | Not documented | In-memory only (TanStack Query cache — cleared on app close) | TanStack Query in-memory cache | Automatic on app close | Not documented — requires investigation | Profile data is public on ATProto; right to erasure requires account deletion on PDS |
| DC-008 (DID) | Permanent — DID is a persistent identifier on ATProto | Until logout or app uninstall (same as DC-003) | Session state layer | Same as DC-003 | DID deletion requires account deletion on PDS — ATProto protocol implications | DID is a permanent identifier; "deletion" on ATProto is complex |
| DC-009 (Direct Message Content) | Not documented — WHO: backend lead; WHAT: What is the server-side retention period for DM content? Is there a deletion mechanism for individual messages or entire conversations?; WHERE: Insert here | In-memory only during session (ConvoProvider) — on-device persistence between sessions not documented | ConvoProvider (in-memory) | App close clears in-memory cache; server-side deletion mechanism not documented | Not documented — requires investigation | Most sensitive data category in messaging flow |
| DC-010 (Conversation Metadata) | Not documented | In-memory only (TanStack Query cache) | TanStack Query in-memory cache | Automatic on app close; useLeaveConvo deletes conversation server-side |
Partial — useLeaveConvo provides user-initiated deletion |
Leave conversation deletes from user's view; server-side retention unclear |
| DC-011 (Messaging Preferences) | Retained as actor declaration on PDS — no documented deletion mechanism | In-memory only (React Query cache) | TanStack Query in-memory cache | No documented deletion mechanism — only update | Not documented — requires investigation | Actor declaration may persist after account deletion |
| DC-012 (Notification Preferences) | Not transmitted to server | Persists until app uninstall (Android) / potentially beyond (iOS UserDefaults) |
ExpoBackgroundNotificationHandler native storage |
App uninstall (Android); unclear on iOS | N/A — device-local only, non-sensitive | Low risk — boolean preference |
| DC-013 (Analytics/Behavioral Data) | Not documented — WHO: DPO and engineering lead; WHAT: What is the analytics service's data retention period? Is there a mechanism for users to request deletion of their analytics data?; WHERE: Insert here | Not documented — depends on analytics SDK | Analytics SDK (unknown) | Not documented | Not documented — requires investigation | Legal basis not documented |
| DC-014 (Device Push Notification Token) | Retained by notification routing service — not documented | Not documented | Unknown | Not documented | Not documented — requires investigation | APNs/FCM tokens are rotated by the OS; server-side cleanup on token rotation not documented |
Mobile-specific retention notes:
iOS Keychain post-uninstall persistence: If the session state layer uses iOS Keychain to store accessJwt, Keychain items with kSecAttrAccessibleWhenUnlocked or kSecAttrAccessibleAlways access classes survive app uninstall by default on iOS. This means a user who uninstalls the app may have their authentication token remain on the device, accessible to a reinstalled version of the app. This is a GDPR right-to-erasure concern — a user who uninstalls the app expecting all data to be removed would be surprised to find their session token persists. Recommendation: use kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly and explicitly delete all Keychain items on logout.
AsyncStorage backup exposure: If accessJwt is stored in AsyncStorage, it is included in iOS iCloud backups and Android Google Drive auto-backups. Restoring a backup to a new device would restore the session token, potentially allowing session replay from the new device.
| Risk ID | Risk Description | Likelihood | Impact | Risk Level | Data Categories | NIST Function | Recommendation |
|---|---|---|---|---|---|---|---|
| PR-001 | Authentication token stored in unencrypted on-device storage (AsyncStorage) — accessible on rooted/jailbroken devices, included in device backups, exposed to session replay attacks | Medium (common pattern in RN apps) | High (full account compromise) | Critical | DC-003, DC-001, DC-008 | Protect | Investigate session state layer storage immediately; migrate to expo-secure-store if AsyncStorage is used (see DF-001) |
| PR-002 | Analytics service destination, data transmitted, and legal basis undocumented — potential unlawful processing of behavioral data | High (analytics is actively used) | High (GDPR Art. 6 violation, regulatory fine risk) | Critical | DC-013 | Govern | Document analytics service, data transmitted, and legal basis; implement consent mechanism if required (see DF-007) |
| PR-003 | ConvoProvider transport mechanism undocumented — direct message content may not be encrypted in transit | Medium | High (DM content exposure) | High | DC-009, DC-010 | Protect | Document and verify TLS for all message transmission; assess end-to-end encryption (see DF-011) |
| PR-004 | iOS Keychain items may persist after app uninstall — right-to-erasure gap | Medium (iOS default behavior) | Medium (residual auth token on device) | High | DC-003, DC-008 | Govern | Use kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly; explicitly delete Keychain items on logout and on first launch after reinstall |
| PR-005 | Push notification payload content undocumented — DM content may be transmitted through APNs/FCM infrastructure | Medium | High (DM content exposure through third-party infrastructure) | High | DC-009, DC-014 | Protect | Ensure notification payloads contain only conversation ID, not message content (see DF-016) |
| PR-006 | Authentication token included in iOS iCloud / Android Google Drive backups (if AsyncStorage) — session replay risk on device restore | Medium (if AsyncStorage is used) | High (account takeover via backup restore) | High | DC-003 | Protect | Exclude sensitive data from device backups; use Keychain/Keystore which are excluded from backups by default |
| PR-007 | Weak password strength validation in Set New Password Form — single-character passwords accepted client-side | High (no complexity enforcement) | Medium (weak account security) | High | DC-004 | Protect | Implement client-side password strength validation (see DF-005) |
| PR-008 | No documented right-to-erasure mechanism for direct message content, authentication tokens, or analytics data | High (no deletion flow documented) | High (GDPR Art. 17 non-compliance) | High | DC-003, DC-009, DC-013 | Control | Document and implement right-to-erasure flows for all data categories |
| PR-009 | Cross-border data transfers to user-configurable ATProto PDS instances — transfer mechanism not documented | Medium (federated protocol by design) | Medium (GDPR Art. 46 non-compliance) | Medium | DC-001, DC-002, DC-009, DC-010, DC-011 | Govern | Document transfer mechanisms for third-party PDS instances; consider user notice when PDS is non-EEA (see DF-008) |
| PR-010 | Stored account list displayed on login screen — privacy exposure on shared or lost devices | Medium | Medium (account enumeration) | Medium | DC-001, DC-007, DC-008 | Protect | Consider device authentication before displaying account list (see DF-009) |
| PR-011 | Notification permission requested post-login without documented pre-permission rationale — may not meet GDPR transparency requirements | Medium | Medium (consent validity) | Medium | DC-014 | Communicate | Implement pre-permission rationale screen before OS notification dialog (see DF-004) |
| PR-012 | Native module error handling gap — playSoundChat preference write failures are silent |
Low | Low (preference inaccuracy) | Low | DC-012 | Protect | Add error handling to native module calls (see DF-019) |
| PR-013 | // TODO remove double login — potential duplicate credential transmission |
Low (workaround in place) | Medium (duplicate session creation) | Medium | DC-003, DC-004 | Protect | Resolve double-login issue (see DF-002) |
| PR-014 | HACKFIX setTimeout in email verification gate — fragile timing dependency could break privacy control | Low (timing-dependent) | Medium (unverified users accessing messaging) | Medium | DC-009 | Protect | Refactor email verification gate to use reliable mechanism (see DF-013) |
[DF-001 / PR-001]: Investigate #/state/session storage implementation — determine whether accessJwt is stored in AsyncStorage, MMKV, Keychain, or SecureStore. If AsyncStorage or unencrypted MMKV is used, migrate to expo-secure-store immediately. — Effort: 1–3 days (investigation + migration if needed)
[DF-007 / PR-002]: Document the analytics service used by #/analytics (ax.metric). Identify all data fields transmitted with each event. Determine legal basis. If Consent is required, implement a consent mechanism before analytics events are fired. Add the analytics service to the App Store Privacy Nutrition Label and Google Play Data Safety section. — Effort: 2–5 days (documentation + potential consent implementation)
[DF-011 / PR-003]: Document the ConvoProvider transport mechanism. Confirm TLS is used for all message transmission. Determine whether message content is cached on-device between sessions. — Effort: 1–2 days (documentation + verification)
[PR-004]: If iOS Keychain is used for session storage, audit the kSecAttrAccessible access class. Implement explicit Keychain item deletion on logout. Consider using kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly to prevent post-uninstall persistence. — Effort: 2–3 days
[PR-006]: If AsyncStorage is used for session storage, configure android:allowBackup="false" for sensitive data files, or migrate to Keychain/Keystore (which are excluded from backups by default). — Effort: 1–2 days (after PR-001 resolution)
[DF-005 / PR-007]: Implement client-side password strength validation in Set New Password Form: minimum 8 characters, complexity rules. Resolve the // TODO Better password strength check comment. — Effort: 1–2 days
[DF-004 / PR-011]: Implement a pre-permission rationale screen before the OS notification permission dialog. Document the denied/permanently-denied handling in useRequestNotificationsPermission. — Effort: 1–2 days
[DF-016 / PR-005]: Audit push notification payload content. Ensure payloads contain only conversation ID, not message content. Document the notification payload structure. — Effort: 1 day (audit) + implementation if needed
[DF-002]: Investigate and resolve the // TODO remove double login issue in the Login Form. — Effort: 1–3 days
[DF-008 / PR-009]: Document data transfer mechanisms for third-party ATProto PDS instances. Consider displaying a notice to users when their PDS is not operated by Bluesky. — Effort: 2–3 days (legal review + implementation)
[PR-008]: Document and implement right-to-erasure flows for all data categories: direct message content (server-side deletion API), authentication tokens (logout + Keychain cleanup), analytics data (analytics service deletion API). — Effort: 2–4 weeks
[DF-009 / PR-010]: Assess whether device authentication should be required before displaying the stored account list. Implement if appropriate for the app's threat model. — Effort: 1–2 weeks
[DF-012]: Confirm server-side enforcement of email verification for messaging. Document the server-side enforcement mechanism. — Effort: 1 day (verification + documentation)
[DF-013]: Refactor the HACKFIX setTimeout in the email verification gate to use a reliable mechanism. — Effort: 2–3 days
[DF-018]: Document the deletion behavior of the actor declaration (allowIncoming) when a user deletes their account. Implement deletion if not already handled. — Effort: 1–2 days
[DF-019]: Add try/catch error handling to BackgroundNotificationHandler.getAllPrefsAsync() and setBoolAsync() calls. Show a Toast error if the preference write fails. — Effort: 0.5 days
[DF-020]: Replace currentAccount!.did with a defensive null check in the Settings screen. — Effort: 0.5 days
[DF-003]: Apply .toUpperCase() to authFactorToken.trim() before submission in the Login Form, or verify server accepts case-insensitive tokens. — Effort: 0.5 days
[DF-010]: Harden the session API to be resilient to concurrent resumeSession calls. — Effort: 2–5 days
[DF-006]: Add a timeout-based reset of isProcessing in Set New Password Form as a safety net for the case where onPasswordSet() does not navigate away. — Effort: 0.5 days
[DF-015]: Fix the deleted account press handler bug in the Inbox screen (menuControl.open() called when ConvoMenu is not rendered). — Effort: 0.5 days
[DF-017]: Surface inbox fetch errors to the user with a non-blocking indicator on the InboxPreview banner. — Effort: 0.5 days
App Store Privacy Nutrition Label / Google Play Data Safety: After resolving Phase 1 and Phase 2 items, update the App Store Privacy Nutrition Label and Google Play Data Safety section to accurately reflect all data collected, data linked to user, and data used to track. — Effort: 1–2 days
DPIA trigger assessment: Given the processing of direct message content (DC-009) and the federated nature of the ATProto protocol (cross-border transfers to user-configurable PDS instances), assess whether a full DPIA is required under GDPR Art. 35. — Effort: 1–2 weeks (DPO-led)
| Term | Definition |
|---|---|
| GDPR | General Data Protection Regulation (EU) 2016/679 — the primary EU data protection law governing the processing of personal data of EU residents. |
| CCPA/CPRA | California Consumer Privacy Act / California Privacy Rights Act — California state laws granting consumers rights over their personal information. |
| Personal Data | Any information relating to an identified or identifiable natural person (GDPR Art. 4(1)). In this document: usernames, email addresses, DIDs, message content, device tokens. |
| Sensitive Data | Data requiring additional protection beyond standard personal data. In this document: passwords, authentication tokens, 2FA codes, reset tokens, direct message content. |
| Special Category Data | GDPR Art. 9 data: racial/ethnic origin, political opinions, religious beliefs, genetic/biometric data, health data, sex life/orientation. No special category data was identified in the reviewed screens. |
| ROPA | Records of Processing Activities — required under GDPR Art. 30 for organizations processing personal data. Documents the purposes, legal bases, data categories, recipients, and retention periods for each processing activity. |
| DPIA | Data Protection Impact Assessment — required under GDPR Art. 35 for high-risk processing activities. Assesses privacy risks and mitigation measures. |
| DPA | Data Processing Agreement — a contract required under GDPR Art. 28 between a data controller and a data processor. Required for all third-party services that process personal data on behalf of the controller. |
| Legal Basis | The lawful ground for processing personal data under GDPR Art. 6: Consent, Performance of Contract, Legal Obligation, Vital Interest, Public Interest, or Legitimate Interest. |
| Right to Erasure | GDPR Art. 17 — the right of data subjects to request deletion of their personal data. Also known as the "right to be forgotten." |
| Privacy by Design | GDPR Art. 25 — the principle that privacy protections should be built into systems from the outset, not added as an afterthought. |
| Data Minimization | GDPR Art. 5(1)(c) — the principle that only the minimum necessary personal data should be collected and processed. |
| SCC | Standard Contractual Clauses — a legal mechanism for transferring personal data from the EEA to third countries without an adequacy decision. |
| BCR | Binding Corporate Rules — a legal mechanism for intra-group transfers of personal data to third countries. |
| LIA | Legitimate Interest Assessment — a three-part test (purpose, necessity, balancing) required before relying on Legitimate Interest as a legal basis under GDPR Art. 6(1)(f). |
| Article | Subject |
|---|---|
| Art. 5 | Principles relating to processing of personal data (lawfulness, fairness, transparency, purpose limitation, data minimization, accuracy, storage limitation, integrity and confidentiality) |
| Art. 6 | Lawfulness of processing — legal bases |
| Art. 9 | Processing of special categories of personal data |
| Art. 13 | Information to be provided where personal data are collected from the data subject (transparency) |
| Art. 17 | Right to erasure ('right to be forgotten') |
| Art. 25 | Data protection by design and by default |
| Art. 28 | Processor obligations and Data Processing Agreements |
| Art. 30 | Records of processing activities (ROPA) |
| Art. 32 | Security of processing — appropriate technical and organisational measures |
| Art. 35 | Data protection impact assessment (DPIA) |
| Art. 46 | Transfers subject to appropriate safeguards (SCCs, BCRs) |
| Control | Subject |
|---|---|
| ISO 27701:2019 §7.2 | Conditions for collection and processing |
| ISO 27701:2019 §7.3 | Obligations to PII principals (data subjects) |
| ISO 27701:2019 §7.4 | Privacy by design and privacy by default |
| ISO 27701:2019 §8.2 | Conditions for collection and processing (processor obligations) |
| Control | Subject |
|---|---|
| MASVS-STOR-1 | The app only stores sensitive data in protected storage (Keychain on iOS, Keystore on Android). Sensitive data must not be stored in AsyncStorage, MMKV (without encryption), or other unprotected locations. |
| MASVS-STOR-2 | Sensitive data is not stored in backup-eligible locations without justification. AsyncStorage and unencrypted MMKV are included in iOS iCloud and Android Google Drive backups by default. |
| Term | Definition |
|---|---|
| AsyncStorage | A React Native key-value storage system backed by plain-text SQLite on the device. Not encrypted. Included in iOS iCloud backups and Android Google Drive auto-backups by default. Cleared on app uninstall. |
| MMKV | A high-performance key-value storage library for React Native (from WeChat). Not encrypted by default — requires explicit encryption key configuration. Included in device backups by default. Cleared on app uninstall. |
| Keychain (iOS) | Apple's secure credential storage system. Encrypted using the device's hardware security. Excluded from iCloud backup by default (depending on kSecAttrAccessible class). Persists after app uninstall by default unless kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly is used. |
| Keystore (Android) | Android's hardware-backed key storage system. Encrypted. Not included in Google Drive backups. Cleared on app uninstall (Android 9+). |
| SecureStore (Expo) | Expo's wrapper around iOS Keychain and Android Keystore. Provides encrypted storage with the same persistence characteristics as the underlying platform APIs. |
| expo-secure-store | The Expo SDK package implementing SecureStore. Uses iOS Keychain and Android Keystore automatically. |
| Secure Enclave | Apple's dedicated security chip (A-series and M-series processors) that performs cryptographic operations without exposing private keys to the main processor. Used by iOS Keychain for hardware-backed key protection. |
| kSecAttrAccessible | An iOS Keychain attribute that controls when a Keychain item can be accessed (e.g., kSecAttrAccessibleWhenUnlocked, kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly). The ThisDeviceOnly variants prevent Keychain items from surviving app uninstall. |
| UserDefaults | Apple's key-value storage system for app preferences on iOS/macOS. Not encrypted. Included in iCloud backups unless excluded via NSURLIsExcludedFromBackupKey. |
| SharedPreferences | Android's key-value storage system for app preferences. Not encrypted by default. May be included in Google Drive auto-backups depending on backup rules configuration. |
| iCloud backup | Apple's device backup service. Includes AsyncStorage, MMKV, and UserDefaults data by default. Excludes Keychain items (unless kSecAttrAccessible allows it). |
| Google Drive auto-backup | Android's automatic backup service (Android 6+). Includes AsyncStorage, MMKV, and SharedPreferences data by default. Excludes Keystore data. |
| IDFA | Identifier for Advertisers — Apple's advertising identifier for iOS devices. Used by ad networks for cross-app tracking. Not referenced in the reviewed screens. |
| GAID | Google Advertising ID — Android's advertising identifier. Used by ad networks for cross-app tracking. Not referenced in the reviewed screens. |
| APNs | Apple Push Notification service — Apple's infrastructure for delivering push notifications to iOS devices. Receives device tokens and notification payloads from app servers. |
| FCM | Firebase Cloud Messaging — Google's infrastructure for delivering push notifications to Android devices (and iOS via Firebase SDK). |
| Term | Definition |
|---|---|
| POST_NOTIFICATIONS | Android 13+ permission required to post push notifications. Implicit on earlier Android versions. |
| NSLocationWhenInUseUsageDescription | iOS Info.plist key required to request location access while the app is in use. Not referenced in the reviewed screens. |
| NSCameraUsageDescription | iOS Info.plist key required to request camera access. Not referenced in the reviewed screens. |
| UNUserNotificationCenter | iOS framework for managing notification permissions and delivery. Used by expo-notifications. |
| Term | Definition |
|---|---|
| ATProto / AT Protocol | The decentralized social networking protocol developed by Bluesky. Authentication, messaging, and profile management in this app are performed against ATProto-compatible servers. |
| PDS (Personal Data Server) | The server that hosts a user's ATProto account data. Users may be on Bluesky's default PDS (bsky.social) or a self-hosted/third-party PDS. |
| DID (Decentralized Identifier) | A globally unique, persistent identifier for a user on the AT Protocol (e.g., did:plc:abc123). Used as the primary key for accounts. |
| Handle | An ATProto user identifier in the format username.domain (e.g., alice.bsky.social). Human-readable identity in the ATProto ecosystem. |
| accessJwt | A JSON Web Token used to authenticate API requests for an ATProto session. Its presence in the session store indicates an active or recently active session. |
| XRPC | Cross-service Remote Procedure Call — the HTTP-based RPC protocol used by ATProto. Method names like com.atproto.server.createSession are XRPC lexicon identifiers. |
| ConvoProvider | A React context provider that manages the full state machine for a single DM conversation, including connection, message history, and real-time updates. |
| AllowIncoming | A union type (`'all' |
| Actor Declaration | An ATProto concept where a user declares preferences about themselves on the network. In this context, controls chat message permissions (allowIncoming). |
| Age Assurance | A feature requiring users to confirm their age before accessing certain content (specifically: chats). Enforced by AgeRestrictedScreen. |
| Minimal Shell Mode | An app-wide UI state where the tab bar and navigation chrome are hidden, giving the active screen full-screen real estate. Activated during conversation viewing. |
| Acronym | Expansion |
|---|---|
| PII | Personally Identifiable Information |
| DPA | Data Processing Agreement (also: Data Protection Authority) |
| DPIA | Data Protection Impact Assessment |
| ROPA | Records of Processing Activities |
| SCC | Standard Contractual Clauses |
| BCR | Binding Corporate Rules |
| MASVS | Mobile Application Security Verification Standard |
| MSTG | Mobile Security Testing Guide |
| GDPR | General Data Protection Regulation |
| CCPA | California Consumer Privacy Act |
| CPRA | California Privacy Rights Act |
| DPO | Data Protection Officer |
| EEA | European Economic Area |
| TLS | Transport Layer Security |
| OTP | One-Time Password |
| JWT | JSON Web Token |
| APNs | Apple Push Notification service |
| FCM | Firebase Cloud Messaging |
| IDFA | Identifier for Advertisers (iOS) |
| GAID | Google Advertising ID (Android) |
| RN | React Native |
| PDS | Personal Data Server (ATProto) |
| DID | Decentralized Identifier |
| XRPC | Cross-service Remote Procedure Call |
| LIA | Legitimate Interest Assessment |
| ISO | International Organization for Standardization |
| NIST | National Institute of Standards and Technology |
| OWASP | Open Web Application Security Project |
End of Data Flow & Privacy Map (Mobile) — social-app — April 2026
Generated by DocAgent — automated codebase documentation analysis. Subject matter expert review is recommended before distribution.