bluesky-social/social-app
May 5, 2026
This Data Flow & Privacy Map covers the Bluesky social application — a React Native / Expo mobile application built on the AT Protocol (atproto), a decentralized social networking protocol. The assessment covers 131 screens across iOS and Android platforms, examining data collection, on-device storage, server-side transmission, third-party sharing, and deletion mechanisms.
Overall Privacy Posture Rating: Moderate
The application demonstrates strong privacy-by-design principles in several areas — notably its use of expo-secure-store / Keychain for authentication tokens, its moderation-aware content rendering, and its explicit user controls for notification preferences, content language, and activity privacy. However, several medium-to-high risk gaps were identified, primarily around on-device storage practices, analytics data collection, and the scope of device permissions.
Data Categories Identified:
Top 3 Highest-Risk Privacy Concerns:
Device Contacts Upload (High Risk): The Find Contacts flow (/find-contacts-flow) uploads device phone contacts to Bluesky's servers for matching against registered accounts. This involves third-party PII (contacts who have not consented to the app's data processing) and requires explicit GDPR Article 6 legal basis analysis and potentially Article 9 assessment if contacts include health-related data.
Analytics SDK Data Collection (High Risk): The application uses a custom analytics abstraction (useAnalytics / ax.metric) that collects user behavior data including profile DIDs, post URIs, search queries, feed interactions, and navigation patterns. The underlying analytics provider is not identifiable from the codebase alone, preventing assessment of data transfer mechanisms, retention periods, and DPA status.
Persisted Storage Backup Exposure (Medium Risk): The application uses a persisted storage layer (likely AsyncStorage-backed) for session account data, search history, and preferences. This data is included in iOS iCloud backups and Android Google Drive auto-backups by default, potentially exposing session tokens and browsing history if a device backup is restored to a different device or accessed by a third party.
Assessment Limitations: This assessment is based on documentation-based analysis of 131 screens. It does not include live data flow tracing, device forensics, network traffic inspection, or server-side database inspection. All findings should be verified against the live application before regulatory submission.
All 131 screens provided in the documentation were assessed, covering:
react-native-mmkv in SharedPrefs native module), SecureStore/Keychain (via expo-secure-store)Based on technical documentation review, not live data flow tracing, database inspection, or device forensics. The specific storage keys, encryption configurations, and retention periods for the persisted storage layer are not fully visible from screen documentation alone.
| Storage Mechanism | Encrypted | Data Stored | Survives Uninstall | Backup Eligible | Privacy Risk | MASVS-STOR |
|---|---|---|---|---|---|---|
Persisted storage (AsyncStorage-backed, via #/state/persisted) |
No (plain-text) | Session account list, reminders, NUX state, policy update state, search history, profile history | No (cleared on uninstall) | Yes — included in iOS iCloud backup and Android Google Drive auto-backup by default | High — session account data and browsing history backed up to cloud | MASVS-STOR-1 concern |
Device storage (MMKV-backed, via #/storage) |
No by default (MMKV supports encryption but configuration not documented) | Dev mode flag, activity subscription nudge state, policy update debug override | No | Yes | Medium — non-sensitive flags, but MMKV encryption status unverified | MASVS-STOR-1 concern if sensitive data added |
expo-secure-store / Keychain (iOS) / Keystore (Android) |
Yes — platform secure enclave | Authentication tokens (access JWT, refresh JWT), session credentials | iOS: Persists by default after uninstall unless kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly is used; Android: Cleared on uninstall |
Excluded from iCloud backup by default; excluded from Android backup | Low if used correctly — but iOS Keychain persistence post-uninstall is a right-to-erasure gap | MASVS-STOR-1 compliant |
SharedPrefs native module (MMKV-backed, expo-bluesky-swiss-army) |
No by default | Notification sound preferences (playSoundChat), background notification handler preferences |
No | Yes | Low — non-sensitive notification preferences | MASVS-STOR-1 concern if sensitive data added |
| In-memory only (React state / TanStack Query cache) | N/A | Feed data, post content, profile views, conversation messages, search results | No (lost on app close) | No | Low — ephemeral, not persisted | N/A |
(inferred from expo-secure-store and #/state/persisted usage patterns — verify against actual storage key enumeration and encryption configuration)
The application does not process biometric data at the application layer. Based on the screen documentation:
| API / Library | Biometric Data Accessed | Data Stored On-Device | Template Stored by App | Privacy Risk |
|---|---|---|---|---|
| OS-level biometric (Face ID / Touch ID / Fingerprint) | Used only for device unlock / app authentication gate | No — OS-only | No — App only receives boolean authentication result | Low — OS handles biometric, app never accesses template |
Key finding: GDPR Article 9 special category classification does not apply to this application's biometric handling, as the app uses OS-level APIs that return only a boolean authentication result. The app does not capture, store, or process biometric templates.
(inferred from expo-secure-store usage — verify against library version changelog)
expo-secure-store wraps iOS Keychain and Android Keystore automatically on supported devices:
kSecAttrAccessible access class used is not documented in the screen documentation.[Not documented — WHO: iOS lead; WHAT: What kSecAttrAccessible access class is used for expo-secure-store items? Specifically, is kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly used to prevent Keychain items from surviving app uninstall?; WHERE: Insert in Section 3 — App Uninstall Data Cleanup table, iOS Keychain row]
| Storage | Behavior on Uninstall | Residual Data Risk | Recommendation |
|---|---|---|---|
| Persisted storage (AsyncStorage-backed) | Cleared automatically | None | N/A |
| Device storage (MMKV-backed) | Cleared automatically | None | N/A |
iOS Keychain (via expo-secure-store) |
Persists by default unless kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly access class is used |
High — authentication tokens (access JWT, refresh JWT) may persist after uninstall, allowing re-authentication without user credentials on reinstall | Use kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly or explicitly delete all Keychain items on logout |
Android Keystore (via expo-secure-store) |
Cleared on uninstall (Android 9+) | Low on modern Android | Document minimum Android API level requirement |
SharedPrefs (MMKV-backed native module) |
Cleared automatically | None | N/A |
| Platform | Storage | Backup Behavior | User Consent | Risk |
|---|---|---|---|---|
| iOS | Persisted storage (AsyncStorage-backed) | Included in iCloud backup by default | User controls iCloud backup in iOS Settings | Session account list, search history, and profile history may be backed up to iCloud and restored to a different device |
| iOS | Keychain (via expo-secure-store) |
Excluded from iCloud backup by default (unless kSecAttrAccessible allows it) |
N/A | Low |
| Android | Persisted storage (AsyncStorage-backed) | Included in Google Drive backup (Android 6+, auto backup) | User controls Google account backup | Session account data and preferences may back up to Google Drive |
| Android | Keystore (via expo-secure-store) |
Not backed up | N/A | Low |
Critical finding: The persisted storage layer contains session account data (including stored account credentials for multi-account switching) and search/profile history. This data is included in device backups by default. If a user restores a backup to a new device, their session tokens and browsing history are restored — this may be appropriate for session continuity but represents a privacy risk if the backup is accessed by a third party or restored to an unauthorized device.
Based on the screen documentation, the following device permissions are requested by the application:
PERM-001: Camera
| Field | Value |
|---|---|
| Permission | Camera |
| iOS API | AVCaptureDevice (via useCameraPermission hook, openCamera utility) |
| Android API | CAMERA |
| Data Collected | Photos and videos captured by the user for use as profile avatar, post attachments, or message attachments |
| GDPR Classification | Personal Data (photos may contain biometric data of third parties) |
| Legal Basis | Consent (user explicitly initiates camera action) |
| Purpose | Allow users to capture photos/videos for profile setup (onboarding avatar), post composition, and message composition |
| Data Minimization | Camera access is triggered only when user explicitly presses camera button; no background camera access |
| When Requested | When feature is triggered (user presses camera button in composer or profile setup) |
| Denied Behavior | Camera button action does not proceed; no error message documented in screen docs |
| Permanently Denied Behavior | (not documented — requires investigation) |
| Data Destination | Captured image is processed on-device, then uploaded to AT Protocol blob storage on user's PDS (server-side) |
| Screen Source | Feed (/src/view/com/feeds/feed) — ComposerPrompt camera button; Profile setup during onboarding |
Source Evidence: Feed screen (/src/view/com/feeds/feed) — Section 6 (User Interactions), Section 13 (Integration Points)
[Not documented — WHO: iOS lead; WHAT: What happens when camera permission is permanently denied — is the user shown a settings redirect prompt?; WHERE: Insert in PERM-001 table, Permanently Denied Behavior row]
PERM-002: Photo Library
| Field | Value |
|---|---|
| Permission | Photo Library |
| iOS API | PHPhotoLibrary (via usePhotoLibraryPermission, useVideoLibraryPermission, openUnifiedPicker utility) |
| Android API | READ_EXTERNAL_STORAGE / READ_MEDIA_IMAGES / READ_MEDIA_VIDEO (Android 13+) |
| Data Collected | Photos and videos selected by the user from their device library |
| GDPR Classification | Personal Data (photos may contain biometric data, location metadata via EXIF) |
| Legal Basis | Consent (user explicitly initiates photo selection) |
| Purpose | Allow users to select photos/videos for profile avatar, banner, post attachments, and message attachments |
| Data Minimization | Access is triggered only when user explicitly presses gallery button; only selected items are accessed (not full library scan) |
| When Requested | When feature is triggered (user presses gallery button) |
| Denied Behavior | Gallery button action does not proceed |
| Permanently Denied Behavior | (not documented — requires investigation) |
| Data Destination | Selected image/video is processed on-device (resized, compressed), then uploaded to AT Protocol blob storage on user's PDS (server-side). EXIF metadata including GPS location may be included in uploaded files — this is not documented as being stripped. |
| Screen Source | Feed (/src/view/com/feeds/feed) — ComposerPrompt gallery button; Profile header edit dialog; Onboarding profile setup |
Source Evidence: Feed screen (/src/view/com/feeds/feed) — Section 6, Section 13
Special assessment — EXIF metadata: Photos selected from the device library may contain EXIF metadata including GPS coordinates, device model, and timestamp. The documentation does not indicate whether EXIF data is stripped before upload. If GPS coordinates are included in uploaded photos, this constitutes precise location data processing under GDPR Article 4(1) and CCPA.
[Not documented — WHO: Backend lead; WHAT: Does the application strip EXIF metadata (including GPS coordinates) from photos before uploading to the AT Protocol blob storage?; WHERE: Insert in PERM-002 table, Data Destination row, and in DC-007 (Photo/Video Data) in the Data Category Inventory]
PERM-003: Contacts
| Field | Value |
|---|---|
| Permission | Contacts |
| iOS API | CNContactStore (via expo-contacts, Contacts.isAvailableAsync()) |
| Android API | READ_CONTACTS |
| Data Collected | Device phone contacts (names, phone numbers, email addresses) |
| GDPR Classification | Sensitive Personal Data — contacts include third-party PII of individuals who have not consented to this app's data processing |
| Legal Basis | Consent (user explicitly initiates Find Contacts flow) — however, the legal basis for processing third-party contacts data (the contacts themselves) requires separate analysis |
| Purpose | Match device contacts against registered Bluesky accounts to help users find people they know |
| Data Minimization | (not documented — requires investigation: are all contacts uploaded, or only hashed phone numbers?) |
| When Requested | When user initiates Find Contacts flow (/find-contacts-flow) |
| Denied Behavior | Admonition error block shown in Intro component; import button not shown |
| Permanently Denied Behavior | Same as denied — Admonition shown |
| Data Destination | Uploaded to Bluesky servers for matching against registered accounts. The getAllListMembers and contact upload APIs are called via the AT Protocol agent. |
| Screen Source | Find Contacts Flow (/find-contacts-flow), Find Contacts Settings (/settings/find-contacts-settings) |
Source Evidence: Find Contacts Flow (/find-contacts-flow) — Section 1, Section 5; Find Contacts Settings (/settings/find-contacts-settings) — Section 5
Special assessment — Third-party PII: Contacts data includes PII of third parties (the contacts themselves) who have not consented to the Bluesky application's data processing. Under GDPR, this requires a separate legal basis for processing third-party data. The legitimate interest basis may apply (helping users find friends), but a Legitimate Interest Assessment (LIA) should be conducted. Under CCPA, contacts data may constitute "sensitive personal information" if it includes precise geolocation or other sensitive categories.
[Not documented — WHO: Backend lead; WHAT: (1) Are device contacts uploaded in plaintext or as hashed values (e.g., SHA-256 of phone numbers)? (2) How long are uploaded contacts retained on Bluesky servers? (3) Are contacts deleted from Bluesky servers when the user removes their contact data via the "Remove all contacts" button?; WHERE: Insert in PERM-003 table, Data Destination row, and in PA-009 (Contact Import Processing) in the ROPA section]
| ID | Data Category | Classification | Examples Found | Collection Screens | Storage Location | On-Device Storage | Legal Basis |
|---|---|---|---|---|---|---|---|
| DC-001 | Authentication Credentials | Sensitive | Access JWT, refresh JWT, session tokens | Login Form, Choose Account Form, Signup flow | Server DB + On-device | Keychain (iOS) / Keystore (Android) via expo-secure-store |
Contract (account access) |
| DC-002 | Account Identity | Personal | DID, handle, display name, email address | Account Settings, Login Form, Signup flow | Server DB + On-device | Persisted storage (AsyncStorage-backed) — account list | Contract |
| DC-003 | Profile Information | Personal | Display name, bio/description, avatar image, banner image, date of birth | Profile header, Edit Profile Dialog, Account Settings, Onboarding | Server DB + On-device (avatar/banner cached) | In-memory (TanStack Query cache) | Contract |
| DC-004 | Social Graph | Personal | Follower list, following list, block list, mute list | Profile Followers, Profile Follows, Known Followers | Server DB | None (fetched on demand) | Contract |
| DC-005 | Post Content | Personal | Post text, embedded media, rich text facets, reply context | Feed, Post Thread, Conversation | Server DB | In-memory (TanStack Query cache) | Contract |
| DC-006 | Direct Messages | Personal | Message text, message metadata, conversation members | Conversation, Chat List, Inbox | Server DB (chat service) | In-memory only | Contract |
| DC-007 | Photo/Video Data | Personal | Photos and videos selected from device library or captured by camera; may include EXIF metadata with GPS coordinates | Feed (ComposerPrompt), Profile header (avatar/banner), Onboarding | Server DB (blob storage) | In-memory during selection; uploaded to server | Consent |
| DC-008 | Search History | Personal | Recent search terms (up to 6), recently viewed profile DIDs (up to 10) | Search Shell | On-device only | Persisted storage (AsyncStorage-backed), keyed by account DID | Legitimate Interest (personalization) |
| DC-009 | Notification Preferences | Personal | Push/in-app notification settings per notification type, filter settings | Notification Settings screens | Server DB + On-device | Persisted storage (AsyncStorage-backed) | Contract |
| DC-010 | Language Preferences | Personal | App UI language, primary language, content languages | Language Settings | On-device | Persisted storage (AsyncStorage-backed) | Contract |
| DC-011 | Appearance Preferences | Personal | Color mode, dark theme, font family, font scale, app icon selection | Appearance Settings | On-device | Persisted storage (AsyncStorage-backed) | Contract |
| DC-012 | Feed Preferences | Personal | Saved feeds, pinned feeds, following feed preferences, thread preferences | Saved Feeds, Following Feed Preferences, Thread Preferences | Server DB + On-device | Persisted storage (AsyncStorage-backed) | Contract |
| DC-013 | Interest Tags | Personal | Selected interest categories (e.g., "sports", "tech") | Interests Settings, Onboarding | Server DB | None (fetched from preferences API) | Consent |
| DC-014 | Device Contacts | Sensitive | Phone contacts (names, phone numbers, email addresses) — third-party PII | Find Contacts Flow | Server DB (uploaded) | None (not stored on-device after upload) | Consent (with LIA required for third-party data) |
| DC-015 | Age/Date of Birth | Sensitive | Date of birth (used for age gating — 13+ and 18+ checks) | Signup flow, Account Settings (Birthday) | Server DB | None (not stored on-device) | Legal Obligation (COPPA compliance) |
| DC-016 | Email Address | Personal | Email address for account verification, 2FA, password reset | Account Settings, Login Form, Signup flow | Server DB | On-device (session account object in persisted storage) | Contract |
| DC-017 | App Passwords | Personal | App password names, creation dates, privileged status | App Passwords Settings | Server DB | None (plaintext password shown once, not stored) | Contract |
| DC-018 | Activity Subscriptions | Personal | List of accounts the user has subscribed to for activity notifications | Activity Notification Settings | Server DB | None (fetched on demand) | Contract |
| DC-019 | Analytics/Behavioral Data | Personal | User interactions (post views, feed refreshes, search queries, profile visits, follow actions, navigation patterns), device identifiers | All screens (via ax.metric() calls) |
Third-party analytics service (provider unknown) | None (transmitted to analytics service) | Consent (if analytics provider requires it) / Legitimate Interest |
| DC-020 | Notification Sound Preferences | Personal | playSoundChat boolean |
Messages Settings | On-device | Native SharedPrefs module (MMKV-backed) |
Contract |
| DC-021 | Verification Status | Personal | Account verification status, verification role | Profile header, Account Settings | Server DB | None (fetched from profile API) | Contract |
| DC-022 | Starter Pack Data | Personal | Starter pack name, description, member list, feed list | Starter Pack screens | Server DB | None (fetched on demand) | Contract |
| DC-023 | List Data | Personal | List name, description, member list, list type (mod/curated) | List screens | Server DB | None (fetched on demand) | Contract |
| DC-024 | Labeler Subscription Data | Personal | Subscribed labelers, per-labeler label preferences | Verification Settings, Labels section | Server DB | None (fetched from preferences API) | Consent |
PA-001: User Authentication and Session Management
| Field | Value |
|---|---|
| Purpose | Authenticate users and maintain session state across app launches |
| Legal Basis | Performance of Contract (GDPR Art. 6(1)(b)) |
| Data Categories | DC-001 (Authentication Credentials), DC-002 (Account Identity), DC-016 (Email Address) |
| Data Subjects | Registered Bluesky users |
| Recipients | Bluesky PDS (Personal Data Server) — AT Protocol backend |
| On-Device Processing | Session tokens validated locally; account list maintained for multi-account switching |
| On-Device Storage | Authentication tokens: Keychain (iOS) / Keystore (Android) via expo-secure-store. Account list (DID, handle, email): Persisted storage (AsyncStorage-backed). |
| Transfers | AT Protocol API calls to user's PDS (may be cross-border depending on PDS location) |
| Retention — Server | [Not documented — WHO: Backend lead; WHAT: How long are session tokens and account records retained on the PDS?; WHERE: Insert in PA-001 table, Retention — Server row] |
| Retention — On-Device | Authentication tokens: Until explicit logout or app uninstall (iOS Keychain may persist post-uninstall). Account list: Until account removed or app uninstalled. |
| Security Measures | Keychain/Keystore encryption for tokens; TLS for API transmission; email verification for sensitive operations |
| Source Screen(s) | Login Form, Choose Account Form, Deactivated, Signup flow |
Source Evidence: Login Form (/login/login-form), Choose Account Form (/login/choose-account-form), Account Settings (/settings/account-settings)
PA-002: Profile Data Processing
| Field | Value |
|---|---|
| Purpose | Display user profiles, enable profile editing, support social graph features |
| Legal Basis | Performance of Contract (GDPR Art. 6(1)(b)) |
| Data Categories | DC-003 (Profile Information), DC-004 (Social Graph), DC-021 (Verification Status) |
| Data Subjects | Registered Bluesky users (both profile owners and viewers) |
| Recipients | Bluesky AppView (read); User's PDS (write) |
| On-Device Processing | Profile data cached in TanStack Query in-memory cache; moderation decisions computed client-side via moderateProfile() |
| On-Device Storage | In-memory only (TanStack Query cache, 5-minute GC time by default) |
| Transfers | AT Protocol API calls; cross-border if PDS is in different jurisdiction |
| Retention — Server | [Not documented — WHO: Backend lead; WHAT: How long are profile records retained after account deletion?; WHERE: Insert in PA-002 table, Retention — Server row] |
| Retention — On-Device | In-memory only; cleared when app is closed or cache expires |
| Security Measures | TLS for API transmission; moderation-aware rendering (blur/hide based on labels) |
| Source Screen(s) | Profile Header Standard, Profile Header Labeler, Edit Profile Dialog, Account Settings |
PA-003: Direct Messaging
| Field | Value |
|---|---|
| Purpose | Enable private one-on-one messaging between users |
| Legal Basis | Performance of Contract (GDPR Art. 6(1)(b)) |
| Data Categories | DC-006 (Direct Messages), DC-002 (Account Identity) |
| Data Subjects | Registered Bluesky users who send or receive messages |
| Recipients | Bluesky Chat Service (separate from main PDS) |
| On-Device Processing | Messages held in-memory during active conversation; polling every 10 seconds when screen is focused |
| On-Device Storage | In-memory only; no local message persistence documented |
| Transfers | AT Protocol chat API calls to Bluesky chat service; cross-border transfer possible |
| Retention — Server | [Not documented — WHO: Backend lead; WHAT: How long are direct messages retained on the chat service? Are deleted messages purged immediately?; WHERE: Insert in PA-003 table, Retention — Server row] |
| Retention — On-Device | In-memory only; cleared when app is closed |
| Security Measures | TLS for API transmission; age assurance gate (AgeRestrictedScreen); email verification required to send messages |
| Source Screen(s) | Conversation, Chat List, Inbox, Messages Settings |
PA-004: Content Feed Processing
| Field | Value |
|---|---|
| Purpose | Display personalized and algorithmic content feeds; track post engagement for feed feedback |
| Legal Basis | Performance of Contract (GDPR Art. 6(1)(b)); Legitimate Interest for feed personalization (GDPR Art. 6(1)(f)) |
| Data Categories | DC-005 (Post Content), DC-019 (Analytics/Behavioral Data), DC-012 (Feed Preferences) |
| Data Subjects | Registered Bluesky users |
| Recipients | Bluesky AppView; Feed Generator servers (third-party); Analytics service (provider unknown) |
| On-Device Processing | Feed items cached in TanStack Query; post view tracking (40% visible for 500ms); feed feedback signals (like/dislike) sent to feed generators |
| On-Device Storage | In-memory only (TanStack Query cache) |
| Transfers | AT Protocol API calls; feed generator API calls (third-party servers); analytics events to unknown provider |
| Retention — Server | [Not documented — WHO: Backend lead; WHAT: How long are feed interaction signals retained by feed generators?; WHERE: Insert in PA-004 table, Retention — Server row] |
| Retention — On-Device | In-memory only; 5-minute GC time by default |
| Security Measures | TLS for API transmission; moderation-aware rendering |
| Source Screen(s) | Feed, Search Shell (Explore), Topic, Hashtag |
PA-005: Search and Discovery
| Field | Value |
|---|---|
| Purpose | Enable content and user discovery; personalize search results |
| Legal Basis | Performance of Contract (GDPR Art. 6(1)(b)); Legitimate Interest for search history (GDPR Art. 6(1)(f)) |
| Data Categories | DC-008 (Search History), DC-019 (Analytics/Behavioral Data) |
| Data Subjects | Registered Bluesky users |
| Recipients | Bluesky AppView (search API); Analytics service |
| On-Device Processing | Search terms stored locally (up to 6); profile history stored locally (up to 10 DIDs); search queries augmented with user DID for personalization |
| On-Device Storage | Persisted storage (AsyncStorage-backed), keyed by account DID: [did, 'searchTermHistory'] and [did, 'searchAccountHistory'] |
| Transfers | AT Protocol search API calls; analytics events |
| Retention — Server | N/A (search history is on-device only) |
| Retention — On-Device | Until user clears history (individual items removable via X button) or account is removed |
| Security Measures | History scoped to account DID (prevents cross-account leakage); unauthenticated users use 'pwi' key |
| Source Screen(s) | Search Shell (/search/shell) |
PA-006: Notification Preference Management
| Field | Value |
|---|---|
| Purpose | Allow users to configure notification delivery preferences per notification type |
| Legal Basis | Performance of Contract (GDPR Art. 6(1)(b)) |
| Data Categories | DC-009 (Notification Preferences), DC-020 (Notification Sound Preferences) |
| Data Subjects | Registered Bluesky users |
| Recipients | Bluesky PDS (preferences API); Native OS notification service (APNs/FCM) |
| On-Device Processing | Notification sound preference stored in native SharedPrefs module; push notification token managed by OS |
| On-Device Storage | Notification sound: Native SharedPrefs (MMKV-backed). Push notification token: Managed by OS (APNs/FCM), not directly by app. |
| Transfers | Preferences written to AT Protocol PDS; push notification tokens transmitted to APNs (iOS) or FCM (Android) |
| Retention — Server | [Not documented — WHO: Backend lead; WHAT: How long are notification preferences retained after account deletion?; WHERE: Insert in PA-006 table, Retention — Server row] |
| Retention — On-Device | Until app uninstall |
| Security Measures | TLS for API transmission |
| Source Screen(s) | All notification settings screens under /settings/notification-settings/ |
PA-007: Account Creation (Signup)
| Field | Value |
|---|---|
| Purpose | Create new AT Protocol accounts on the Bluesky network |
| Legal Basis | Performance of Contract (GDPR Art. 6(1)(b)); Legal Obligation for age verification (GDPR Art. 6(1)(c) — COPPA) |
| Data Categories | DC-002 (Account Identity), DC-015 (Age/Date of Birth), DC-016 (Email Address), DC-001 (Authentication Credentials) |
| Data Subjects | New users registering for Bluesky |
| Recipients | Bluesky PDS; hCaptcha (CAPTCHA verification) |
| On-Device Processing | Age validation (is13(), is18()) performed client-side; CAPTCHA challenge rendered in WebView/iframe |
| On-Device Storage | None during signup; credentials stored in Keychain after successful account creation |
| Transfers | Account creation API call to PDS; CAPTCHA verification to hCaptcha servers |
| Retention — Server | [Not documented — WHO: Backend lead; WHAT: How long is date of birth retained after account creation? Is it stored or only used for age verification?; WHERE: Insert in PA-007 table, Retention — Server row] |
| Retention — On-Device | Date of birth: Not stored on-device after signup. Email: Stored in session account object in persisted storage. |
| Security Measures | CAPTCHA verification; email verification; age gating (13+ required, 18+ for certain features) |
| Source Screen(s) | Signup flow screens (/signup/*) |
PA-008: Analytics and Behavioral Tracking
| Field | Value |
|---|---|
| Purpose | Track user interactions for product analytics, feature usage measurement, and recommendation improvement |
| Legal Basis | [Not documented — WHO: DPO; WHAT: What is the legal basis for analytics data collection? Is user consent obtained? Is legitimate interest relied upon, and if so, has an LIA been conducted?; WHERE: Insert in PA-008 table, Legal Basis row] |
| Data Categories | DC-019 (Analytics/Behavioral Data) |
| Data Subjects | All app users (authenticated and unauthenticated) |
| Recipients | Unknown analytics provider (abstracted behind useAnalytics() / ax.metric()) |
| On-Device Processing | Events collected synchronously at interaction points; transmitted to analytics service |
| On-Device Storage | None documented (events appear to be transmitted immediately) |
| Transfers | Analytics events transmitted to unknown provider; cross-border transfer possible |
| Retention — Server | [Not documented — WHO: DPO; WHAT: What is the analytics provider's data retention period? Where is analytics data stored geographically?; WHERE: Insert in PA-008 table, Retention — Server row] |
| Retention — On-Device | None (events transmitted immediately) |
| Security Measures | [Not documented — WHO: Engineering lead; WHAT: Is analytics data anonymized or pseudonymized before transmission? Are user DIDs transmitted in analytics events?; WHERE: Insert in PA-008 table, Security Measures row] |
| Source Screen(s) | All screens (analytics calls present throughout the application) |
PA-009: Contact Import and Matching
| Field | Value |
|---|---|
| Purpose | Help users find Bluesky accounts of people they know by matching device contacts against registered users |
| Legal Basis | Consent (GDPR Art. 6(1)(a)) for the user's own data; separate legal basis required for third-party contacts data — Legitimate Interest (GDPR Art. 6(1)(f)) with LIA required |
| Data Categories | DC-014 (Device Contacts) |
| Data Subjects | The authenticated user; third-party individuals whose contact information is uploaded |
| Recipients | Bluesky servers (contact matching service) |
| On-Device Processing | Contacts read from device; availability checked via expo-contacts; uploaded to server for matching |
| On-Device Storage | None (contacts not stored on-device after upload) |
| Transfers | Contact data uploaded to Bluesky servers; cross-border transfer possible |
| Retention — Server | [Not documented — WHO: Backend lead; WHAT: (1) How long are uploaded contacts retained? (2) Are contacts deleted when user removes data via "Remove all contacts"? (3) Are contacts stored as plaintext or hashed?; WHERE: Insert in PA-009 table, Retention — Server row] |
| Retention — On-Device | None |
| Security Measures | User must explicitly initiate the flow; "Remove all contacts" option available in settings |
| Source Screen(s) | Find Contacts Flow (/find-contacts-flow), Find Contacts Settings (/settings/find-contacts-settings) |
PA-010: Account Deletion
| Field | Value |
|---|---|
| Purpose | Enable users to permanently delete their Bluesky account |
| Legal Basis | Legal Obligation (GDPR Art. 17 — Right to Erasure) |
| Data Categories | DC-001 through DC-024 (all data categories) |
| Data Subjects | Registered Bluesky users requesting deletion |
| Recipients | Bluesky PDS; Bluesky Chat Service |
| On-Device Processing | Chat service notified before PDS deletion; session cleared after deletion |
| On-Device Storage | All on-device data cleared on app uninstall (except iOS Keychain items if not explicitly deleted) |
| Transfers | Deletion API calls to PDS and chat service |
| Retention — Server | [Not documented — WHO: Backend lead; WHAT: How long does it take for account data to be fully purged from all Bluesky servers after deletion? Are backups purged?; WHERE: Insert in PA-010 table, Retention — Server row] |
| Retention — On-Device | AsyncStorage/MMKV: Cleared on uninstall. iOS Keychain: May persist post-uninstall — explicit deletion required. |
| Security Measures | Two-factor deletion flow (email code + password); chat service notified; session invalidated |
| Source Screen(s) | Account Settings (/settings/account-settings) — Delete Account dialog |
Due to the large number of screens (131) in scope, this section presents representative high-risk and high-complexity screens in detail. Lower-risk screens with similar patterns are grouped by category.
Data Profile:
| Aspect | Assessment |
|---|---|
| Data Collection | No — displays existing conversations |
| Data Display | Yes — conversation member profiles (avatar, display name, handle), last message preview, unread count |
| Data Modification | Yes — mark as read, accept/reject/delete conversations, block users |
| Data Deletion | Yes — leave/delete conversations |
| External Sharing | No direct third-party sharing; analytics events fired on conversation open |
| Sensitive Data | Yes — direct message content previews; age-assurance gated |
| On-Device Storage | In-memory only (TanStack Query cache) |
| Device Permissions | None |
| Privacy Risk Level | Medium |
Data Collection Points:
| Field | Data Category | Required | Validation | Legal Basis | On-Device Storage | Notes |
|---|---|---|---|---|---|---|
| N/A | N/A | N/A | N/A | N/A | N/A | No user input collected on this screen |
Data Flows:
chat.bsky.convo.listConvos API call fetches pending chat requests with status: 'request'; conversation member profiles fetched from AT ProtocolleftConvos list; hasUnreadConvos computed; moderation applied via moderateProfile()chat.bsky.convo.acceptConversation), reject/leave (chat.bsky.convo.leaveConvo), block (app.bsky.graph.block), mark as read (chat.bsky.convo.updateRead) mutationsax.metric('chat:open', {logContext: 'ChatsList'}) analytics event on conversation openPrivacy Concerns:
DF-001: Analytics Event on Conversation Open
| Field | Value |
|---|---|
| Risk Level | Low |
| Data Affected | DC-019 (Analytics/Behavioral Data) |
| Regulation | GDPR Art. 6 (legal basis for analytics); CCPA §1798.100 |
| Description | ax.metric('chat:open', {logContext: 'ChatsList'}) is fired when a user opens a conversation. This transmits behavioral data to an unknown analytics provider. The legal basis for this analytics collection is not documented. |
| Evidence | Inbox screen documentation Section 10: "ax.metric('chat:open', {logContext: 'ChatsList'}) — analytics event fired when a non-deleted-account conversation row is tapped." |
| Recommendation | Identify the analytics provider; ensure a DPA is in place; document the legal basis for analytics collection; consider whether messaging behavior analytics requires explicit consent given the sensitive nature of messaging data. |
Data Profile:
| Aspect | Assessment |
|---|---|
| Data Collection | Yes — device contacts (names, phone numbers, email addresses) |
| Data Display | Yes — matched Bluesky profiles |
| Data Modification | Yes — follow matched users |
| Data Deletion | Yes — remove all contact data |
| External Sharing | Yes — contacts uploaded to Bluesky servers |
| Sensitive Data | Yes — third-party PII (device contacts) |
| On-Device Storage | None (contacts not stored on-device) |
| Device Permissions | Contacts (READ_CONTACTS / CNContactStore) |
| Privacy Risk Level | High |
Data Collection Points:
| Field | Data Category | Required | Validation | Legal Basis | On-Device Storage | Notes |
|---|---|---|---|---|---|---|
| Device contacts | DC-014 | Yes (for feature) | Contacts.isAvailableAsync() check |
Consent | None | Third-party PII — contacts have not consented to Bluesky processing |
Data Flows:
expo-contacts; contact availability checkedbulkWriteFollowsPrivacy Concerns:
DF-002: Third-Party PII in Contact Upload
| Field | Value |
|---|---|
| Risk Level | High |
| Data Affected | DC-014 (Device Contacts) |
| Regulation | GDPR Art. 6 (legal basis for third-party data), Art. 13/14 (transparency), Art. 35 (DPIA may be required); CCPA §1798.140 (sensitive personal information) |
| Description | Device contacts include PII of third parties (the contacts themselves) who have not consented to Bluesky's data processing. The legal basis for processing this third-party data is not documented. A Legitimate Interest Assessment (LIA) is required if legitimate interest is relied upon. |
| Evidence | Find Contacts Settings documentation Section 5: "Contact data uploaded to Bluesky servers for matching against registered accounts." |
| Recommendation | (1) Conduct a Legitimate Interest Assessment (LIA) for contact data processing. (2) Determine whether contacts are uploaded as plaintext or hashed — hashing would significantly reduce privacy risk. (3) Ensure the privacy notice discloses contact data processing. (4) Consider whether a DPIA is required under GDPR Art. 35 (large-scale processing of personal data). |
DF-003: Contact Data Retention Unknown
| Field | Value |
|---|---|
| Risk Level | High |
| Data Affected | DC-014 (Device Contacts) |
| Regulation | GDPR Art. 5(1)(e) (storage limitation), Art. 17 (right to erasure) |
| Description | The retention period for uploaded contact data on Bluesky servers is not documented. It is unclear whether the "Remove all contacts" button in settings triggers immediate deletion from all server systems including backups. |
| Evidence | Find Contacts Settings documentation Section 5: "agent.app.bsky.contact.removeData({}) — notifies chat service of deletion." |
| Recommendation | Document the contact data retention period. Verify that "Remove all contacts" triggers complete deletion from all server systems. Communicate the retention period to users in the privacy notice. |
Data Profile:
| Aspect | Assessment |
|---|---|
| Data Collection | Yes — email, password, handle, date of birth |
| Data Display | Yes — current email, email verification status, handle, automation label status |
| Data Modification | Yes — all account credentials and identity |
| Data Deletion | Yes — account deactivation and deletion |
| External Sharing | No direct third-party sharing |
| Sensitive Data | Yes — email address, password, date of birth |
| On-Device Storage | None (all data server-side) |
| Device Permissions | Clipboard (for DNS record copying) |
| Privacy Risk Level | Medium |
Privacy Concerns:
DF-004: Chat Data Export Contains Sent Messages Only
| Field | Value |
|---|---|
| Risk Level | Low |
| Data Affected | DC-006 (Direct Messages) |
| Regulation | GDPR Art. 20 (data portability) |
| Description | The chat data export (chat.bsky.actor.exportAccountData) exports only sent messages, not received messages. This may not satisfy GDPR Art. 20 data portability requirements, which require all personal data provided by the data subject. |
| Evidence | Account Settings documentation Section 5: "The JSONL export contains only sent messages, not received messages (noted in UI copy)." |
| Recommendation | Assess whether the current export scope satisfies GDPR Art. 20 data portability requirements. Consider whether received messages should also be exportable. |
DF-005: iOS Keychain Persistence Post-Uninstall
| Field | Value |
|---|---|
| Risk Level | High |
| Data Affected | DC-001 (Authentication Credentials) |
| Regulation | GDPR Art. 17 (right to erasure), Art. 32 (security of processing); MASVS-STOR-1 |
| Description | Authentication tokens stored in iOS Keychain via expo-secure-store may persist after app uninstall by default. If a user uninstalls the app expecting all data to be deleted, their authentication tokens may remain accessible on the device and could be used to re-authenticate on reinstall without entering credentials. |
| Evidence | On-Device Storage Privacy Assessment (Section 3): iOS Keychain persists by default unless kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly is used. |
| Recommendation | (1) Verify the kSecAttrAccessible access class used by expo-secure-store. (2) If not already using kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly, implement explicit Keychain item deletion on logout. (3) Document this behavior in the privacy notice. |
Data Profile:
| Aspect | Assessment |
|---|---|
| Data Collection | Yes — search queries, profile interaction history |
| Data Display | Yes — search results (posts, profiles, feeds), trending topics, suggested accounts |
| Data Modification | No |
| Data Deletion | Yes — individual search history items removable |
| External Sharing | Yes — analytics events for search interactions |
| Sensitive Data | No |
| On-Device Storage | Persisted storage (AsyncStorage-backed) — search history and profile history |
| Device Permissions | None |
| Privacy Risk Level | Medium |
Data Collection Points:
| Field | Data Category | Required | Validation | Legal Basis | On-Device Storage | Notes |
|---|---|---|---|---|---|---|
| Search query text | DC-008 (Search History) | No | None | Legitimate Interest | Persisted storage (AsyncStorage-backed), keyed by [did, 'searchTermHistory'] |
Up to 6 terms stored; user can delete individual items |
| Viewed profile DIDs | DC-008 (Search History) | No | None | Legitimate Interest | Persisted storage (AsyncStorage-backed), keyed by [did, 'searchAccountHistory'] |
Up to 10 DIDs stored; 400ms delay before storage |
Privacy Concerns:
DF-006: Search History in Device Backup
| Field | Value |
|---|---|
| Risk Level | Medium |
| Data Affected | DC-008 (Search History) |
| Regulation | GDPR Art. 32 (appropriate technical measures); MASVS-STOR-1 |
| Description | Search history (recent search terms and recently viewed profile DIDs) is stored in persisted storage (AsyncStorage-backed) which is included in iOS iCloud backups and Android Google Drive auto-backups by default. This means a user's search history could be restored to a different device or accessed via a backup. |
| Evidence | Search Shell documentation Section 12: "useStorage(account, [did, 'searchTermHistory']) — Stores up to 6 recent search term strings, keyed by account DID." |
| Recommendation | Assess whether search history needs to be included in device backups. If not, implement backup exclusion for these storage keys. Alternatively, document this behavior in the privacy notice. |
DF-007: Analytics Events Include Profile DIDs and Search Queries
| Field | Value |
|---|---|
| Risk Level | Medium |
| Data Affected | DC-019 (Analytics/Behavioral Data) |
| Regulation | GDPR Art. 6 (legal basis), Art. 25 (data minimization) |
| Description | Multiple analytics events in the Search screen transmit profile DIDs and search query content to an unknown analytics provider: ax.metric('search:autocomplete:press', {profileDid, position}), ax.metric('search:recent:press', {profileDid, position}), ax.metric('suggestedUser:seen', {suggestedDid, ...}). |
| Evidence | Search Shell documentation Section 10: Multiple ax.metric calls with profileDid and suggestedDid parameters. |
| Recommendation | Identify the analytics provider. Assess whether profile DIDs constitute personal data in the analytics context. Consider pseudonymizing DIDs before transmission. Ensure the legal basis for analytics collection is documented. |
Data Profile:
| Aspect | Assessment |
|---|---|
| Data Collection | No (messages composed in child component) |
| Data Display | Yes — message content, sender profiles |
| Data Modification | No |
| Data Deletion | No |
| External Sharing | No |
| Sensitive Data | Yes — direct message content; age-assurance gated |
| On-Device Storage | In-memory only |
| Device Permissions | None |
| Privacy Risk Level | Medium |
Privacy Concerns:
DF-008: Age Assurance Gate for Messaging
| Field | Value |
|---|---|
| Risk Level | Low |
| Data Affected | DC-006 (Direct Messages) |
| Regulation | GDPR Art. 8 (children's data); applicable national age verification laws |
| Description | The AgeRestrictedScreen wrapper enforces age assurance before rendering the conversation screen. This is a positive privacy control. However, the specific age threshold and verification mechanism are not documented in the screen documentation. |
| Evidence | Conversation documentation Section 2: "AgeRestrictedScreen wrapper enforces age-related access control." |
| Recommendation | Document the age threshold for messaging access and the verification mechanism used by AgeRestrictedScreen. Ensure this is disclosed in the privacy notice. |
Data Profile:
| Aspect | Assessment |
|---|---|
| Data Collection | Yes — email, password, handle, date of birth, CAPTCHA verification |
| Data Display | No |
| Data Modification | No |
| Data Deletion | No |
| External Sharing | Yes — CAPTCHA data to hCaptcha |
| Sensitive Data | Yes — date of birth, email, password |
| On-Device Storage | None during signup |
| Device Permissions | None |
| Privacy Risk Level | Medium |
Privacy Concerns:
DF-009: hCaptcha Third-Party Data Processing
| Field | Value |
|---|---|
| Risk Level | Medium |
| Data Affected | DC-019 (Analytics/Behavioral Data) — device fingerprinting by hCaptcha |
| Regulation | GDPR Art. 28 (processor requirements), Art. 13 (transparency) |
| Description | The CAPTCHA step loads hCaptcha in a WebView/iframe, which may collect device information (IP address, browser fingerprint, behavioral data) for bot detection. This is a third-party data processor relationship that requires a Data Processing Agreement (DPA). |
| Evidence | Captcha Web documentation Section 13: "hCaptcha domains allowlisted: js.hcaptcha.com, newassets.hcaptcha.com, api2.hcaptcha.com." |
| Recommendation | Verify that a DPA is in place with hCaptcha. Disclose hCaptcha data processing in the privacy notice. Assess whether hCaptcha's data collection is proportionate to the bot-detection purpose. |
Master table of all identified data flows, sorted by risk level:
| ID | Screen | Data Category | Direction | Destination | Storage Mechanism | Risk Level | Regulation | Recommendation Phase |
|---|---|---|---|---|---|---|---|---|
| DF-002 | Find Contacts Flow | DC-014 (Device Contacts) | Out | Bluesky servers | None (server-side) | High | GDPR Art. 6, 13, 35 | Phase 1 (Immediate) |
| DF-003 | Find Contacts Flow | DC-014 (Device Contacts) | Out | Bluesky servers | None | High | GDPR Art. 5(1)(e), 17 | Phase 1 (Immediate) |
| DF-005 | Account Settings | DC-001 (Auth Credentials) | On-Device | iOS Keychain | Keychain | High | GDPR Art. 17, 32; MASVS-STOR-1 | Phase 1 (Immediate) |
| DF-001 | Inbox | DC-019 (Analytics) | Out | Unknown analytics provider | None | Low | GDPR Art. 6; CCPA | Phase 2 (Short-Term) |
| DF-006 | Search Shell | DC-008 (Search History) | On-Device | Persisted storage (backup-eligible) | AsyncStorage-backed | Medium | GDPR Art. 32; MASVS-STOR-1 | Phase 2 (Short-Term) |
| DF-007 | Search Shell | DC-019 (Analytics) | Out | Unknown analytics provider | None | Medium | GDPR Art. 6, 25 | Phase 2 (Short-Term) |
| DF-009 | Signup (Captcha) | DC-019 (Analytics) | Out | hCaptcha | None | Medium | GDPR Art. 28, 13 | Phase 2 (Short-Term) |
| DF-004 | Account Settings | DC-006 (Direct Messages) | Out | User device (export) | None | Low | GDPR Art. 20 | Phase 3 (Medium-Term) |
| DF-008 | Conversation | DC-006 (Direct Messages) | In | In-memory | In-memory | Low | GDPR Art. 8 | Phase 4 (Backlog) |
| Service | Integration Method | Data Shared | Purpose | Legal Basis | DPA Required | Transfer Mechanism | Risk Level |
|---|---|---|---|---|---|---|---|
| Bluesky PDS (Personal Data Server) | AT Protocol XRPC API | DC-001 through DC-023 (all user data) | Core social platform functionality | Contract | N/A (first-party service) | TLS; cross-border if PDS in different jurisdiction | Low (first-party) |
| Bluesky Chat Service | AT Protocol XRPC API (with DM_SERVICE_HEADERS) |
DC-006 (Direct Messages) | Direct messaging | Contract | N/A (first-party service) | TLS | Low (first-party) |
| Bluesky AppView | AT Protocol XRPC API | DC-003, DC-004, DC-005 (profile, social graph, posts) | Content aggregation and search | Contract | N/A (first-party service) | TLS | Low (first-party) |
| Feed Generator Servers (third-party) | AT Protocol XRPC API | DC-005 (Post content context), DC-019 (feed feedback signals) | Custom algorithmic feeds | Legitimate Interest | Yes — DPA required | TLS; cross-border possible | Medium |
| hCaptcha | WebView/iframe (web), WebView (native) | Device fingerprint, behavioral data, IP address | Bot detection during signup | Legitimate Interest / Consent | Yes — DPA required | TLS; cross-border (hCaptcha servers) | Medium |
| Unknown Analytics Provider | Custom useAnalytics() abstraction |
DC-019 (user interactions, profile DIDs, post URIs, search queries, navigation patterns) | Product analytics | Unknown — requires investigation | Yes — DPA required | Unknown | High |
| APNs (Apple Push Notification Service) | iOS OS-level | Device push token, notification metadata | Push notifications | Contract | N/A (Apple standard service) | TLS | Low |
| FCM (Firebase Cloud Messaging) | Android OS-level | Device push token, notification metadata | Push notifications | Contract | N/A (Google standard service) | TLS | Low |
Critical finding — Unknown Analytics Provider: The application uses a custom analytics abstraction (useAnalytics() / ax.metric()) throughout all 131 screens. The underlying analytics provider is not identifiable from the codebase documentation. This is a significant gap:
[Not documented — WHO: DPO and Engineering lead; WHAT: (1) What is the underlying analytics provider behind useAnalytics()? (2) Is a DPA in place with this provider? (3) What data transfer mechanism is used for cross-border transfers? (4) What is the data retention period? (5) Is user consent obtained for analytics collection?; WHERE: Insert in Section 9 (Third-Party Data Sharing) — Unknown Analytics Provider row, and in PA-008 (Analytics and Behavioral Tracking) in the ROPA section]
| Data Category | Retention — Server | Retention — On-Device Storage | Storage Mechanism | Deletion Mechanism | Right to Erasure | Notes |
|---|---|---|---|---|---|---|
| DC-001 (Auth Credentials) | [Not documented] | Until logout or uninstall (iOS Keychain may persist post-uninstall) | Keychain (iOS) / Keystore (Android) | Explicit logout; app uninstall (Android); explicit Keychain deletion required on iOS | Supported (logout clears tokens) | iOS Keychain persistence post-uninstall is a right-to-erasure gap |
| DC-002 (Account Identity) | [Not documented] | Until account removed from device or app uninstalled | Persisted storage (AsyncStorage-backed) | Account removal; app uninstall | Supported (account deletion API) | Account list in persisted storage includes email and handle |
| DC-003 (Profile Information) | [Not documented] | In-memory only (5-min GC) | TanStack Query cache | App close; cache expiry | Supported (account deletion API) | |
| DC-006 (Direct Messages) | [Not documented] | In-memory only | TanStack Query cache | App close | Supported (account deletion API notifies chat service) | Export includes sent messages only |
| DC-008 (Search History) | N/A (on-device only) | Until user clears or account removed | Persisted storage (AsyncStorage-backed) | Individual item deletion (X button); account removal | N/A (on-device only) | Included in device backups |
| DC-009 (Notification Preferences) | [Not documented] | Until app uninstall | Persisted storage + Native SharedPrefs | App uninstall | Supported (account deletion) | |
| DC-014 (Device Contacts) | [Not documented] | None (not stored on-device) | N/A | "Remove all contacts" button in settings | Supported (remove data API) | Retention period on server unknown |
| DC-015 (Age/Date of Birth) | [Not documented] | None | N/A | Account deletion | Supported | |
| DC-019 (Analytics/Behavioral Data) | [Not documented — analytics provider unknown] | None (transmitted immediately) | N/A | [Unknown — depends on analytics provider] | [Unknown] | Critical gap — analytics provider and retention unknown |
Mobile-specific retention considerations:
iOS Keychain persistence post-uninstall: Authentication tokens stored via expo-secure-store may persist after app uninstall on iOS. This is a GDPR Art. 17 right-to-erasure concern. Users who uninstall the app expecting all data to be deleted may have authentication tokens remaining on their device.
AsyncStorage backup exposure: Search history, profile history, and session account data stored in persisted storage (AsyncStorage-backed) are included in iOS iCloud backups and Android Google Drive auto-backups by default. If a user restores a backup to a new device, this data is restored — assess whether this is appropriate for each data type.
App uninstall as deletion trigger: App uninstall clears AsyncStorage/MMKV data on both platforms. However, server-side data (posts, profile, messages, followers) requires an API-level deletion flow via the account deletion feature in Account Settings.
| Risk ID | Risk Description | Likelihood | Impact | Risk Level | Data Categories | NIST Function | Recommendation |
|---|---|---|---|---|---|---|---|
| PR-001 | Unknown analytics provider collecting behavioral data including profile DIDs and search queries without documented legal basis or DPA | High | High | Critical | DC-019 | Govern | Identify analytics provider; establish DPA; document legal basis; assess consent requirements |
| PR-002 | Device contacts (third-party PII) uploaded to Bluesky servers without documented legal basis for third-party data processing | High | High | Critical | DC-014 | Govern | Conduct LIA; document legal basis; assess DPIA requirement; verify hashing of contact data |
| PR-003 | iOS Keychain authentication tokens persisting after app uninstall, creating right-to-erasure gap | Medium | High | High | DC-001 | Protect | Use kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly or implement explicit Keychain deletion on logout |
| PR-004 | Search history and session account data in device backups (iCloud/Google Drive) without user awareness | Medium | Medium | High | DC-008, DC-002 | Communicate | Assess backup exclusion for sensitive storage keys; document in privacy notice |
| PR-005 | EXIF metadata (including GPS coordinates) potentially included in uploaded photos | Medium | High | High | DC-007 | Protect | Implement EXIF stripping before photo upload; document in privacy notice |
| PR-006 | Contact data retention period on Bluesky servers unknown; unclear if "Remove all contacts" triggers complete deletion | Medium | High | High | DC-014 | Govern | Document retention period; verify deletion completeness; communicate to users |
| PR-007 | hCaptcha third-party data processing during signup without documented DPA or user disclosure | Low | Medium | Medium | DC-019 | Govern | Verify DPA with hCaptcha; disclose in privacy notice |
| PR-008 | Feed generator servers (third-party) receiving feed feedback signals without documented DPA | Low | Medium | Medium | DC-005, DC-019 | Govern | Identify feed generator operators; establish DPAs where required |
| PR-009 | Analytics events transmitting profile DIDs without pseudonymization | Medium | Medium | Medium | DC-019 | Protect | Pseudonymize DIDs before analytics transmission; assess data minimization |
| PR-010 | Chat data export (GDPR Art. 20 portability) includes only sent messages, not received messages | Low | Low | Low | DC-006 | Govern | Assess whether current export scope satisfies Art. 20 requirements |
[PR-001] Identify analytics provider and establish legal basis: The unknown analytics provider behind useAnalytics() is the highest-priority gap. Identify the provider, verify DPA status, document legal basis, and assess whether user consent is required. — Effort: Medium (1-3 days for identification; legal review may take longer)
[PR-002] Document legal basis for contact data processing: Conduct a Legitimate Interest Assessment (LIA) for the Find Contacts feature. Determine whether contacts are uploaded as plaintext or hashed. If plaintext, implement hashing immediately. — Effort: High (requires legal review and potentially engineering changes)
[DF-005 / PR-003] Implement explicit Keychain deletion on logout: Verify the kSecAttrAccessible access class used by expo-secure-store. If not using kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly, implement explicit deletion of all Keychain items on logout to prevent post-uninstall token persistence. — Effort: Low (1-2 days engineering)
[PR-004] Assess backup exclusion for sensitive storage keys: Evaluate whether search history (searchTermHistory, searchAccountHistory) and session account data should be excluded from device backups. Implement backup exclusion for sensitive keys if appropriate. — Effort: Medium (2-3 days engineering)
[PR-005] Implement EXIF stripping before photo upload: Verify whether EXIF metadata (including GPS coordinates) is stripped from photos before upload to AT Protocol blob storage. If not, implement EXIF stripping. — Effort: Medium (2-3 days engineering)
[PR-006] Document and verify contact data retention: Obtain documentation of contact data retention period from the backend team. Verify that "Remove all contacts" triggers complete deletion from all server systems including backups. Communicate retention period to users. — Effort: Low (documentation); Medium (verification)
[DF-009] Establish DPA with hCaptcha: Verify that a Data Processing Agreement is in place with hCaptcha. Disclose hCaptcha data processing in the privacy notice. — Effort: Low (legal review)
[PR-007 / PR-008] Establish DPAs with feed generator operators: Identify operators of third-party feed generators that receive feed feedback signals. Establish DPAs where required under GDPR Art. 28. — Effort: High (requires identification of all feed generator operators)
[PR-009] Pseudonymize analytics data: Assess whether profile DIDs transmitted in analytics events can be pseudonymized (e.g., hashed) before transmission to reduce privacy risk. — Effort: Medium (engineering change to analytics abstraction layer)
[DF-007] Review analytics data minimization: Review all ax.metric() calls across 131 screens to assess whether all transmitted data fields are necessary for the stated analytics purpose. Remove unnecessary fields. — Effort: High (requires review of all 131 screens)
[PR-010] Assess GDPR Art. 20 data portability scope: Assess whether the current chat data export (sent messages only) satisfies GDPR Art. 20 data portability requirements. Consider including received messages in the export. — Effort: Medium
[DF-008] Document age assurance mechanism: Document the specific age threshold and verification mechanism used by AgeRestrictedScreen for messaging access. Ensure this is disclosed in the privacy notice. — Effort: Low
General: Complete ROPA entries: Fill in all [Not documented] fields in the ROPA section (PA-001 through PA-010) with actual retention periods, security measures, and transfer mechanisms obtained from the backend and legal teams. — Effort: Medium
AT Protocol (atproto): The open, decentralized social networking protocol developed by Bluesky. Defines the API lexicons, data models, and federation mechanisms used by the Bluesky application. All API calls use AT Protocol XRPC methods.
APNs (Apple Push Notification Service): Apple's push notification delivery service for iOS devices. Device push tokens are transmitted to APNs for notification delivery.
AsyncStorage: A React Native key-value storage system that stores data as plain-text JSON on the device. Data is included in iOS iCloud backups and Android Google Drive auto-backups by default. Not encrypted.
BCR (Binding Corporate Rules): A data transfer mechanism under GDPR that allows multinational companies to transfer personal data within their corporate group across borders.
CCPA/CPRA: California Consumer Privacy Act / California Privacy Rights Act. California state privacy laws granting consumers rights over their personal data.
DID (Decentralized Identifier): A globally unique, persistent identifier for an AT Protocol user account (e.g., did:plc:abc123). Used as the canonical identity key throughout the application.
DPA (Data Processing Agreement): A contract required under GDPR Art. 28 between a data controller and a data processor, governing how the processor handles personal data on behalf of the controller.
DPIA (Data Protection Impact Assessment): A process required under GDPR Art. 35 for processing activities that are likely to result in high risk to individuals' rights and freedoms.
expo-secure-store: An Expo library that wraps iOS Keychain and Android Keystore for encrypted key-value storage. Used for storing authentication tokens.
FCM (Firebase Cloud Messaging): Google's push notification delivery service for Android devices.
GDPR (General Data Protection Regulation): EU regulation governing the processing of personal data of EU residents.
GAID (Google Advertising ID): A unique, user-resettable identifier for advertising on Android devices. Not documented as used in this application.
Handle: A human-readable username in the AT Protocol (e.g., alice.bsky.social). Handles can change; DIDs are permanent.
hCaptcha: A third-party CAPTCHA service used during signup to verify that users are human. Collects device fingerprint and behavioral data.
IDFA (Identifier for Advertisers): A unique, user-resettable identifier for advertising on iOS devices. Not documented as used in this application.
iCloud backup: Apple's cloud backup service for iOS devices. AsyncStorage data is included in iCloud backups by default.
ISO/IEC 27701:2019: International standard for Privacy Information Management Systems (PIMS), extending ISO/IEC 27001 with privacy-specific controls.
Keychain (iOS): Apple's secure credential storage system on iOS. Used by expo-secure-store to store authentication tokens. Keychain items may persist after app uninstall depending on the kSecAttrAccessible access class.
Keystore (Android): Android's hardware-backed key storage system. Used by expo-secure-store to store authentication tokens. Keystore items are cleared on app uninstall (Android 9+).
kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly: An iOS Keychain access class that restricts items to the current device and requires a device passcode. Items with this class are cleared when the app is uninstalled.
LIA (Legitimate Interest Assessment): A three-part test required under GDPR when relying on legitimate interest as the legal basis for data processing: purpose test, necessity test, and balancing test.
MASVS-STOR: OWASP Mobile Application Security Verification Standard — Storage controls. Defines requirements for secure on-device data storage in mobile applications.
MMKV: A high-performance key-value storage library for mobile applications. Used in this application via the expo-bluesky-swiss-army native module (SharedPrefs) and potentially as the backing store for the persisted storage layer.
NIST Privacy Framework v1.0: A voluntary framework for managing privacy risk, organized around five functions: Identify, Govern, Control, Communicate, and Protect.
PDS (Personal Data Server): In the AT Protocol, the server that hosts a user's data repository. Users may self-host or use a third-party PDS.
PII (Personally Identifiable Information): Information that can be used to identify an individual, either directly or in combination with other data.
ROPA (Records of Processing Activities): A documentation requirement under GDPR Art. 30 for organizations to maintain records of their data processing activities.
SCC (Standard Contractual Clauses): Pre-approved contract clauses issued by the European Commission for transferring personal data from the EU to third countries.
SecureStore (Expo): Expo's wrapper around iOS Keychain and Android Keystore, providing encrypted key-value storage. See expo-secure-store.
Secure Enclave: Apple's dedicated security chip in iPhone 5s and later, used for hardware-backed cryptographic operations including Keychain key storage.
Special Category Data (GDPR Art. 9): Data requiring heightened protection: racial/ethnic origin, political opinions, religious beliefs, genetic data, biometric data (used for identification), health data, sex life/sexual orientation. Biometric data processed by OS-level APIs (Face ID, Touch ID) does not fall under this category for this application.
TanStack Query (React Query): A data-fetching and caching library used throughout the application. Data is stored in-memory with a default 5-minute garbage collection time. Not persisted to disk.
XRPC: The remote procedure call protocol used by AT Protocol for client-server communication. Queries (reads) use GET; procedures (writes) use POST.