myowjaYOY/social-app
April 19, 2026
| Stakeholder | Recommended Sections |
|---|---|
| Executives and product managers | §4 Executive Summary, §5 Stakeholders & Concerns, §13 Quality Attributes |
| React Native developers joining the team | §4, §7 Solution Strategy, §8 Building Block View, §9 Runtime View, §10 Data Flow, §11 Cross-Cutting Concerns |
| Mobile platform engineers (iOS/Android) | §7.2 Platform-Branching Strategy, §8, §11.3 Gesture Handling, §11.4 Platform-Branching Consistency, §12 Architecture Decisions |
| Backend / AT Protocol engineers | §6 System Context, §9 Runtime View, §10.1 Data Sources, §12.4 AD-4 |
| Security reviewers | §6, §10.4 Persistence Layer, §11.6 Authentication, §14 Risks & Technical Debt |
| Architects evaluating the system | All sections |
| QA and test engineers | §9 Runtime View, §11 Cross-Cutting Concerns, §14 Risks & Technical Debt |
Bluesky Social (social-app) is a decentralized social networking application built on the AT Protocol (Authenticated Transfer Protocol), an open standard for federated social data. The application allows users to create accounts, compose posts, follow other users, discover content through algorithmic and custom feeds, exchange direct messages, and manage moderation preferences. The primary users are individuals seeking an open, user-controlled social media experience across iOS, Android, and web platforms.
The application is built with React Native and Expo (bare workflow), sharing a single TypeScript codebase across iOS, Android, and Expo Web. Navigation is implemented with React Navigation using native stack navigators. Server state is managed exclusively with TanStack Query (React Query), which handles caching, pagination, and background refetching for all AT Protocol API calls. The application communicates with the AT Protocol network — specifically Bluesky's AppView and Personal Data Servers — using the @atproto/api SDK. All user-facing strings are internationalized using Lingui, with support for over 40 locales.
The most significant architectural characteristics of this system are: (1) the AT Protocol as the sole backend integration point, meaning all data — posts, profiles, feeds, notifications, direct messages, and preferences — flows through AT Protocol XRPC endpoints; (2) a multi-platform codebase with a consistent platform-branching strategy using IS_IOS, IS_ANDROID, IS_NATIVE, and IS_WEB constants derived at build time; (3) a rich set of custom Expo native modules for capabilities not covered by the standard Expo SDK, including background notification handling, GIF rendering, emoji picking, and shared preferences; and (4) OTA (over-the-air) update delivery via EAS Updates with RSA code-signing, enabling production updates without App Store review cycles.
This Architecture Overview covers 130 screens of the Bluesky Social React Native application. The screens assessed include: Value Proposition Pager.shared, Reply Notification Settings, Index.web (onboarding/step-find-contacts-intro), Settings (messages), Index.web (onboarding/step-find-contacts), Profile Follows, Profile Header Standard, Choose Account Form, Avatar Circle, Status Bar Shadow.web, Handle, Miscellaneous Notification Settings, Mention Notification Settings, Quote Notification Settings, App Passwords, About Settings, Interests Settings, Policies, Types (app-icon-settings), Use App Icon Sets, Content And Media Settings, Automation Label Settings, Appearance Settings, Following Feed Preferences, External Media Preferences, Find Contacts Settings, Profile Header Labeler, Shared Preferences Tester, Shell (profile/header), Status Bar Shadow, Like Notification Settings, Likes On Reposts Notification Settings, Index.web (settings/app-icon-settings), App Icon Image, Settings List Item (app-icon-settings), Activity Privacy Settings, Account Settings, Settings List Item.web, Signup Queued, Legacy Notification Settings, Utils (search), State (onboarding), Metrics, Use Suggested Onboarding Users, Accessibility Settings, Deactivated, Language Settings, Gesture Action, Activity Notification Settings, Settings, Splash, Inbox, Expo Scroll Forwarder, Gif, List Hidden, Find Contacts Flow, No Saved Feeds Of Any Type, Set New Password Form, Password Updated Form, No Following Feed, Log, Activity List, Form Container, Chat List, Emoji Picker, Forgot Password Form, Layout (onboarding), Verification Settings, Login Form, No Feeds Pinned, Conversation, Hashtag, Growable Banner, Edit Profile Dialog, Avatar Creator Items, Const (post-thread), Types (onboarding/step-profile), Interest Button, Avatar Creator Circle, Util (onboarding), Placeholder Canvas, Post Reposted By, Post Liked By, Error State, Display Name, Post Quotes, Starter Pack Card, Growable Avatar, Feed Section, About Section, Types (profile/sections), Profile Labeler Liked By, Profile Followers, Profile Search, Known Followers, Explore Suggested Accounts, Labels, Suggested Follows, Feed (profile/sections), Explore Interests Card, Explore Recommendations, Search Results, Saved Feeds, Explore Trending Videos, Explore Trending Topics, Explore, Shell (search), Back Next Buttons, New Follower Notification Settings, Handle Suggestions, Repost Notification Settings, Thread Preferences, Reposts On Reposts Notification Settings, State (signup), Captcha Web View.web, Captcha Web, Privacy And Security Settings, Draggable Scroll, State (starter-pack/wizard), Error, Step Profiles, Index.web (video-feed), Step Details, Starter Pack Landing, Starter Pack, Step Feeds, Takendown, Types (video-feed), Feed (src/view/com/feeds/feed), and Topic. Components, patterns, and features not included in the assessed documentation are outside the scope of this document.
This document reflects a partial view of the application, as 130 screens represent a significant but not exhaustive portion of the full codebase.
Generated by DocAgent — automated codebase documentation analysis. Subject matter expert review is recommended before distribution.
| Stakeholder | Role | Key Concerns |
|---|---|---|
| iOS Users | Primary native platform audience | Performance, Face ID/Touch ID support, iOS-specific UX conventions (swipe-back, safe area, Dynamic Island), App Clip support |
| Android Users | Native platform audience | Back button behavior, adaptive icons, FCM push notifications, material design conventions, edge-to-edge display |
| Web Users (Expo Web) | Browser-based audience | Responsive layout, keyboard navigation, sticky headers, no native module dependencies |
| Bluesky Social (Product Team) | Application owner | Feature velocity, AT Protocol compliance, moderation tooling, subscription monetization (Bluesky+) |
| React Native Developers | Feature implementers | Consistent patterns, type safety, clear module boundaries, testability |
| Technical Leads / Architects | System design owners | Scalability of the navigator hierarchy, state management consistency, cross-platform parity |
| App Store Reviewers (Apple / Google) | Distribution gatekeepers | Privacy manifest compliance, permission usage descriptions, age assurance, content moderation |
| AT Protocol / Bluesky Backend Team | API providers | Correct lexicon usage, session management, PDS compatibility |
| Security Reviewers | Risk assessors | Token storage security, input sanitization, OTA update integrity, sensitive data handling |
| Localization Contributors | i18n maintainers | Lingui catalog completeness, RTL support, locale-specific formatting |
Based on patterns observed across the 130 assessed screens, the following quality goals are demonstrably prioritized in the codebase:
IS_IOS, IS_NATIVE, and IS_WEB constants.@atproto/api SDK; no alternative backend integrations are present.msg, <Trans>, <Plural>), supporting 40+ locales.FlatList/FlashList virtualization, React.memo, useMemo, and useCallback are applied consistently across list-heavy screens.moderateProfile and related AT Protocol moderation functions are applied before rendering user-generated content throughout the profile and feed surfaces.The Bluesky Social mobile application is the system under description. It runs as a React Native JavaScript bundle executing within the iOS and Android runtimes (and as a web application in browsers via Expo Web). The application boundary encompasses all JavaScript/TypeScript code, bundled assets, and custom native modules.
Outside the application boundary are: the AT Protocol network (Bluesky AppView, Personal Data Servers), the native iOS and Android operating system layers, push notification delivery infrastructure (APNs and FCM), the EAS Updates OTA service, the Sentry crash reporting service (when configured), the hCaptcha verification service (used during signup), and the Bitdrift network instrumentation service.
The mobile client communicates with the AT Protocol backend exclusively over HTTPS using the XRPC protocol (a JSON-over-HTTP RPC convention defined by the AT Protocol lexicon). Authentication uses JWT-based session tokens managed by the @atproto/api agent.
[Diagram: System Context — show the React Native application as a box in the center with the iOS runtime, Android runtime, AT Protocol AppView/PDS, EAS Updates service, APNs, FCM, Sentry, hCaptcha, and Bitdrift connected as external systems]
| External System | Direction | Protocol | Purpose |
|---|---|---|---|
| Native iOS Runtime | Internal-platform | Native modules / JSI | Keychain, Face ID, camera, microphone, photo library, APNs, contacts, location, UserDefaults, App Clip |
| Native Android Runtime | Internal-platform | Native modules / JNI | Android Keystore, biometrics, FCM, contacts, SharedPreferences, adaptive icons, edge-to-edge display |
| AT Protocol AppView (bsky.app) | Outbound | HTTPS / XRPC | Feed data, profile data, search, notifications, moderation labels |
| AT Protocol PDS (bsky.social and others) | Outbound | HTTPS / XRPC | Account creation, session management, record writes (posts, follows, likes, preferences) |
| Bluesky Moderation Service (Ozone) | Outbound | HTTPS / XRPC | Moderation reports, appeals |
| Bluesky Chat Service | Outbound | HTTPS / XRPC | Direct messages, conversation management |
| EAS Updates (updates.bsky.app) | Outbound | HTTPS | OTA JavaScript bundle delivery with RSA code-signing |
| APNs (Apple Push Notification service) | Inbound | Native / APNs | iOS push notification delivery |
| FCM (Firebase Cloud Messaging) | Inbound | Native / FCM | Android push notification delivery |
| Sentry (sentry.io) | Outbound | HTTPS | Crash reporting and error monitoring (conditional on SENTRY_AUTH_TOKEN) |
| hCaptcha | Outbound (WebView) | HTTPS / iframe redirect | Bot-prevention CAPTCHA during account signup |
| Bitdrift | Outbound | HTTPS | Network instrumentation and observability |
| Bluesky Link Shortener | Outbound | HTTPS | Generating short share URLs for starter packs and content |
| Layer | Technology | Notes |
|---|---|---|
| Framework | React Native (Expo bare workflow) | app.config.js confirms Expo configuration; newArchEnabled: false in config |
| Language | TypeScript | tsconfig.json extends @react-native/typescript-config; strict mode via ESLint |
| Navigation | React Navigation (native stack) | NativeStackScreenProps used throughout; no Expo Router file-based routing |
| Server State | TanStack Query (React Query) | useQuery, useMutation, useInfiniteQuery used across all data-fetching screens |
| Animations | react-native-reanimated | Used in profile header, gesture actions, feed, onboarding, and settings screens |
| Gesture Handling | react-native-gesture-handler | Used in GestureActionView, BlockDrawerGesture, and swipe-to-action patterns |
| Internationalization | Lingui (@lingui/core, @lingui/react) |
40+ locales; lingui.config.ts defines catalog paths |
| Build System | EAS Build (Expo Application Services) | app.config.js references eas.build configuration |
| OTA Updates | EAS Updates | updates.url: 'https://updates.bsky.app/manifest'; RSA code-signing enabled |
| Crash Reporting | Sentry (@sentry/react-native) |
Conditional on SENTRY_AUTH_TOKEN; Metro config uses getSentryExpoConfig |
| Bundler | Metro | metro.config.js extends Sentry's Expo config; custom resolver for multiformats |
| AT Protocol SDK | @atproto/api |
All API calls, type definitions, and moderation functions |
| UI Component Library | Internal (#/alf, #/components) |
No third-party UI library (no NativeBase, Tamagui, etc.) |
The codebase uses a constants-based branching strategy as its primary mechanism. Platform constants are defined in #/env and derived from Platform.OS at build time:
IS_IOS — true when Platform.OS === 'ios'IS_ANDROID — true when Platform.OS === 'android'IS_NATIVE — true on iOS or Android (not web)IS_WEB — true on Expo WebThese constants are used throughout the codebase in conditional expressions (e.g., IS_NATIVE ? 2 : 0.25 for onEndReachedThreshold), style utilities (e.g., native(a.absolute), web(a.sticky)), and full component exclusions (e.g., if (IS_ANDROID) return null in ProfileHeaderSuggestedFollows).
File-extension platform splits (.web.tsx) are used for screens that have no viable web implementation. Examples observed:
StepFindContactsIntro/index.web.tsx — throws Error('StepFindContactsIntro is not available on web')StepFindContacts/index.web.tsx — throws Error('StepFindContacts is not available on web')AppIconSettings/index.web.tsx — throws Error('Not supported on web')AppIconSettings/settings-list-item.web.tsx — returns undefined (stub)StatusBarShadow.web.tsx — returns nullCaptchaWebView.web.tsx — web-specific iframe implementationVideoFeed/index.web.tsx — returns null (stub)Platform.select() is not observed as a primary branching mechanism; the constants approach is dominant.
The application uses React Navigation with native stack navigators. The root navigator structure is not fully visible from the assessed screens alone, but the following navigator types are evidenced:
NativeStackScreenProps is used as the screen props type across all navigated screens, confirming React Navigation native stack usage.CommonNavigatorParams and AllNavigatorParams are the typed param lists for the two primary navigator scopes.MessagesTabNavigatorParams is a separate param list for the messages tab navigator.Pager component (used in Profile, Search/Explore, Hashtag, and Topic screens) provides horizontal swipe-between-tabs behavior within screens, distinct from the navigator hierarchy.[Not documented — WHO: Mobile platform lead; WHAT: What is the complete root navigator structure (root navigator type, auth stack vs. main stack split, tab navigator configuration, drawer navigator if any)?; WHERE: §7.3 Navigation System Choice and §8.2 Navigator Hierarchy]
useQuery, useMutation, or useInfiniteQuery hook. No raw fetch calls appear in screen components (except in ExportCarDialog for a specific JSONL endpoint that the XRPC client cannot handle).#/state/queries/ and expose typed data, loading, and error states to screens.useProfileShadow wraps profile data with a local optimistic overlay, allowing follow/block state changes to reflect immediately without a full refetch.useReducer-backed context provider (useWizardState, useOnboardingInternalState, SignupContext) to share state across steps.Layout.Screen, Layout.Header.*, and Layout.Content are shared primitives used consistently across all settings and profile screens.SettingsList.Container, SettingsList.Item, SettingsList.LinkItem, SettingsList.Group, and related components provide a consistent settings list UI across all settings screens.createPortalGroup() is used in SettingsList.Group and the onboarding Layout to teleport icon/header content into a parent row without prop drilling.cleanError for user-facing error messages — All API error strings are passed through cleanError() before display to strip internal details.| Quality Goal | Architectural Approach |
|---|---|
| Cross-Platform Consistency | IS_IOS/IS_NATIVE/IS_WEB constants; .web.tsx file splits for incompatible screens; platform() and web() style utilities from #/alf |
| AT Protocol Compliance | All API calls via @atproto/api SDK; lexicon types used for all data shapes; moderateProfile applied before rendering |
| Internationalization | Lingui macros on all user-facing strings; lingui.config.ts defines 40+ locales; babel-plugin-lingui-macro in build pipeline |
| Performance at Scale | TanStack Query caching; FlatList/FlashList virtualization; React.memo, useMemo, useCallback on list-heavy screens; react-native-reanimated for UI-thread animations |
| Moderation & Safety | moderateProfile and moderatePost from @atproto/api applied before rendering; sanitizeDisplayName, sanitizeHandle utilities; cleanError for error messages |
| Container | Technology | Responsibility |
|---|---|---|
| React Native JavaScript Bundle | Metro-bundled TypeScript/JS | All screen components, hooks, business logic, API calls, navigation |
| iOS Native Layer | CocoaPods-linked native modules | UIKit, AVFoundation, CoreLocation, Keychain, APNs, App Clip, custom Expo modules |
| Android Native Layer | Gradle-linked native modules | Android Keystore, FCM, contacts, SharedPreferences, adaptive icons, custom Expo modules |
| AT Protocol Backend | Bluesky AppView + PDS | Feed data, profile data, preferences, moderation, direct messages |
| EAS Updates Service | Expo Application Services | OTA JavaScript bundle delivery |
The following hierarchy is derived from NativeStackScreenProps type annotations, CommonNavigatorParams, AllNavigatorParams, and MessagesTabNavigatorParams observed across the 130 assessed screens. The root navigator structure is inferred from the presence of auth-gated screens and the logged-out shell.
[Not documented — WHO: Mobile platform lead; WHAT: The complete root navigator definition file(s) — specifically the root navigator type (Stack/Tab/Drawer), the auth stack vs. main stack split condition, and the tab navigator configuration for the main app; WHERE: §8.2 Navigator Hierarchy tree below]
Root Navigator (type: Stack — inferred from NativeStackScreenProps usage — verify against root navigator definition)
├── Logged-Out Shell
│ ├── Splash (/src/view/com/auth/splash)
│ ├── Login Stack
│ │ ├── LoginForm (/login/login-form)
│ │ ├── ChooseAccountForm (/login/choose-account-form)
│ │ ├── ForgotPasswordForm (/login/forgot-password-form)
│ │ ├── SetNewPasswordForm (/login/set-new-password-form)
│ │ └── PasswordUpdatedForm (/login/password-updated-form)
│ ├── Signup Stack
│ │ ├── State (/signup/state)
│ │ ├── BackNextButtons (/signup/back-next-buttons)
│ │ ├── Policies (/signup/step-info/policies)
│ │ ├── HandleSuggestions (/signup/step-handle/handle-suggestions)
│ │ ├── CaptchaWeb (/signup/step-captcha/captcha-web)
│ │ └── CaptchaWebView.web (/signup/step-captcha/captcha-web-view.web)
│ ├── SignupQueued (/signup-queued)
│ └── StarterPackLanding (/starter-pack/starter-pack-landing)
│
└── Main App (authenticated)
├── Deactivated (/deactivated)
├── Takendown (/takendown)
├── Onboarding Stack
│ ├── Layout (/onboarding/layout)
│ ├── State (/onboarding/state)
│ ├── AvatarCircle (/onboarding/step-profile/avatar-circle)
│ ├── AvatarCreatorCircle (/onboarding/step-profile/avatar-creator-circle)
│ ├── AvatarCreatorItems (/onboarding/step-profile/avatar-creator-items)
│ ├── PlaceholderCanvas (/onboarding/step-profile/placeholder-canvas)
│ ├── InterestButton (/onboarding/step-interests/interest-button)
│ ├── StarterPackCard (/onboarding/step-suggested-starterpacks/starter-pack-card)
│ ├── ValuePropositionPager.shared (/onboarding/step-finished/value-proposition-pager.shared)
│ ├── Index.web — StepFindContactsIntro (/onboarding/step-find-contacts-intro/index.web)
│ └── Index.web — StepFindContacts (/onboarding/step-find-contacts/index.web)
│
├── Tab Navigator (inferred — verify against root navigator definition)
│ ├── Home Tab
│ │ ├── Feed (/src/view/com/feeds/feed)
│ │ ├── NoFeedsPinned (/home/no-feeds-pinned)
│ │ ├── NoFollowingFeed (/feeds/no-following-feed)
│ │ └── NoSavedFeedsOfAnyType (/feeds/no-saved-feeds-of-any-type)
│ ├── Search Tab
│ │ ├── Shell (/search/shell)
│ │ ├── Explore (/search/explore)
│ │ ├── SearchResults (/search/search-results)
│ │ ├── Utils (/search/utils)
│ │ ├── ExploreSuggestedAccounts (/search/modules/explore-suggested-accounts)
│ │ ├── ExploreInterestsCard (/search/modules/explore-interests-card)
│ │ ├── ExploreRecommendations (/search/modules/explore-recommendations)
│ │ ├── ExploreTrendingVideos (/search/modules/explore-trending-videos)
│ │ └── ExploreTrendingTopics (/search/modules/explore-trending-topics)
│ ├── Notifications Tab
│ │ ├── ActivityList (/notifications/activity-list)
│ │ └── Log (/log)
│ └── Messages Tab (MessagesTabNavigatorParams)
│ ├── Inbox (/messages/inbox)
│ ├── ChatList (/messages/chat-list)
│ ├── Conversation (/messages/conversation)
│ └── Settings (/messages/settings)
│
├── CommonNavigatorParams screens (modal/push stack)
│ ├── Profile Stack
│ │ ├── ProfileHeaderStandard (/profile/header/profile-header-standard)
│ │ ├── ProfileHeaderLabeler (/profile/header/profile-header-labeler)
│ │ ├── Shell (/profile/header/shell)
│ │ ├── Handle (/profile/header/handle)
│ │ ├── DisplayName (/profile/header/display-name)
│ │ ├── Metrics (/profile/header/metrics)
│ │ ├── GrowableBanner (/profile/header/growable-banner)
│ │ ├── GrowableAvatar (/profile/header/growable-avatar)
│ │ ├── StatusBarShadow (/profile/header/status-bar-shadow)
│ │ ├── StatusBarShadow.web (/profile/header/status-bar-shadow.web)
│ │ ├── EditProfileDialog (/profile/header/edit-profile-dialog)
│ │ ├── SuggestedFollows (/profile/header/suggested-follows)
│ │ ├── ErrorState (/profile/error-state)
│ │ ├── ProfileFollows (/profile/profile-follows)
│ │ ├── ProfileFollowers (/profile/profile-followers)
│ │ ├── ProfileSearch (/profile/profile-search)
│ │ ├── KnownFollowers (/profile/known-followers)
│ │ ├── ProfileLabelerLikedBy (/profile/profile-labeler-liked-by)
│ │ ├── Labels (/profile/sections/labels)
│ │ ├── Feed (/profile/sections/feed)
│ │ └── Types (/profile/sections/types)
│ ├── Post Stack
│ │ ├── PostRepostedBy (/post/post-reposted-by)
│ │ ├── PostLikedBy (/post/post-liked-by)
│ │ ├── PostQuotes (/post/post-quotes)
│ │ └── Const (/post-thread/const)
│ ├── Hashtag (/hashtag)
│ ├── Topic (/topic)
│ ├── SavedFeeds (/saved-feeds)
│ ├── ListHidden (/list/list-hidden)
│ ├── FindContactsFlow (/find-contacts-flow)
│ ├── ProfileList Stack
│ │ ├── FeedSection (/profile-list/feed-section)
│ │ └── AboutSection (/profile-list/about-section)
│ ├── StarterPack (/starter-pack)
│ ├── StarterPackLanding (/starter-pack/starter-pack-landing)
│ ├── StarterPack Wizard
│ │ ├── State (/starter-pack/wizard/state)
│ │ ├── StepDetails (/starter-pack/wizard/step-details)
│ │ ├── StepProfiles (/starter-pack/wizard/step-profiles)
│ │ └── StepFeeds (/starter-pack/wizard/step-feeds)
│ ├── VideoFeed
│ │ ├── Index.web (/video-feed/index.web)
│ │ └── Types (/video-feed/types)
│ └── Settings Stack
│ ├── Settings (/settings)
│ ├── AccountSettings (/settings/account-settings)
│ ├── AppPasswords (/settings/app-passwords)
│ ├── AboutSettings (/settings/about-settings)
│ ├── InterestsSettings (/settings/interests-settings)
│ ├── AppearanceSettings (/settings/appearance-settings)
│ ├── AccessibilitySettings (/settings/accessibility-settings)
│ ├── LanguageSettings (/settings/language-settings)
│ ├── ContentAndMediaSettings (/settings/content-and-media-settings)
│ ├── AutomationLabelSettings (/settings/automation-label-settings)
│ ├── FollowingFeedPreferences (/settings/following-feed-preferences)
│ ├── ExternalMediaPreferences (/settings/external-media-preferences)
│ ├── FindContactsSettings (/settings/find-contacts-settings)
│ ├── ThreadPreferences (/settings/thread-preferences)
│ ├── PrivacyAndSecuritySettings (/settings/privacy-and-security-settings)
│ ├── ActivityPrivacySettings (/settings/activity-privacy-settings)
│ ├── VerificationSettings (/moderation/verification-settings)
│ ├── LegacyNotificationSettings (/settings/legacy-notification-settings)
│ ├── AppIconSettings Stack
│ │ ├── Index.web (/settings/app-icon-settings/index.web)
│ │ ├── Types (/settings/app-icon-settings/types)
│ │ ├── UseAppIconSets (/settings/app-icon-settings/use-app-icon-sets)
│ │ ├── AppIconImage (/settings/app-icon-settings/app-icon-image)
│ │ ├── SettingsListItem (/settings/app-icon-settings/settings-list-item)
│ │ └── SettingsListItem.web (/settings/app-icon-settings/settings-list-item.web)
│ └── Notification Settings Stack
│ ├── ReplyNotificationSettings (/settings/notification-settings/reply-notification-settings)
│ ├── MiscellaneousNotificationSettings (/settings/notification-settings/miscellaneous-notification-settings)
│ ├── MentionNotificationSettings (/settings/notification-settings/mention-notification-settings)
│ ├── QuoteNotificationSettings (/settings/notification-settings/quote-notification-settings)
│ ├── LikeNotificationSettings (/settings/notification-settings/like-notification-settings)
│ ├── LikesOnRepostsNotificationSettings (/settings/notification-settings/likes-on-reposts-notification-settings)
│ ├── RepostNotificationSettings (/settings/notification-settings/repost-notification-settings)
│ ├── RepostsOnRepostsNotificationSettings (/settings/notification-settings/reposts-on-reposts-notification-settings)
│ ├── NewFollowerNotificationSettings (/settings/notification-settings/new-follower-notification-settings)
│ └── ActivityNotificationSettings (/settings/notification-settings/activity-notification-settings)
│
└── E2E / Dev Screens
└── SharedPreferencesTester (/e2e/shared-preferences-tester)
[Diagram: Building Block View — show the navigator hierarchy tree, screen components as leaves, and native module integration points (ExpoBackgroundNotificationHandler, ExpoBlueskyGifView, EmojiPicker, ExpoScrollForwarder, SharedPrefs)]
| Building Block | Type | Used By | Responsibility |
|---|---|---|---|
Layout.Screen / Layout.Header.* / Layout.Content |
Shared UI components | All settings, profile, and notification screens | Consistent screen chrome with safe-area handling |
SettingsList.* |
Shared UI components | All settings screens | Consistent settings list rows, groups, and dividers |
useNotificationSettingsQuery |
Custom hook (TanStack Query) | All 10 notification settings screens | Fetches and caches the user's full notification preferences |
useNotificationSettingsUpdateMutation |
Custom hook (TanStack Query) | All 10 notification settings screens | Persists notification preference changes to the AT Protocol backend |
PreferenceControls |
Shared UI component | All notification settings screens | Renders push/in-app toggle group and optional filter radio group |
useProfileQuery |
Custom hook (TanStack Query) | Profile screens, Settings, Account Settings, Automation Label | Fetches a user's full AT Protocol profile by DID |
useSession / useSessionApi |
Custom hooks | Auth screens, Settings, Profile, Messages | Provides current account state and session management functions |
useAnalytics |
Custom hook | Analytics-instrumented screens | Provides ax.metric() for event tracking |
PostFeed |
Shared UI component | Home feed, Profile sections, Activity List | Virtualized, paginated, polled post list |
moderateProfile |
AT Protocol SDK function | Profile header, Search, Explore, Chat List | Computes content moderation decisions for a profile |
cleanError |
Utility function | All screens with error states | Sanitizes raw API error strings for user display |
sanitizeDisplayName / sanitizeHandle |
Utility functions | Profile header, Display Name, Handle, Chat List | Normalizes user-generated strings before rendering |
ExpoBackgroundNotificationHandler |
Custom native module | Messages Settings | Reads/writes background notification preferences to native storage |
ExpoBlueskyGifView |
Custom native module | GIF rendering in posts | Renders and animates GIF content natively |
EmojiPicker |
Custom native module | Emoji picker in composer | Renders platform-native emoji picker UI |
SharedPrefs |
Custom native module | SharedPreferencesTester, app-wide | Cross-platform native key-value storage |
Participants: JS bundle, useSession, SplashScreen, root navigator, LoggedOutShell or main app navigator
Sequence:
useSession reads persisted session tokens from the session store.SplashScreen renders the Bluesky logo and background illustration while session state is being hydrated.LoggedOutShell → SplashScreen with "Create account" and "Sign in" buttons.isSignupQueued(accessJwt)) → SignupQueued screen renders with a 60-second polling loop.Deactivated screen renders.Takendown screen renders.Error path: If session hydration fails (e.g., corrupted token store), the app falls back to the logged-out shell.
[Diagram: Sequence — App Launch & Auth State Check]
Participants: Feed screen, usePostFeedQuery, AT Protocol AppView, PostFeed, List (FlatList)
Sequence:
Feed screen mounts with feed="following" descriptor.usePostFeedQuery("following", ...) fires; TanStack Query checks cache — if stale or absent, sends GET /xrpc/app.bsky.feed.getTimeline to the AT Protocol AppView.feedItems array is computed via useMemo.PostFeed renders List (Animated.FlatList) with initialNumToRender items.onEndReached fires when 2 viewport-heights remain; fetchNextPage() is called.checkForNew() polls for new posts; if new posts exist, hasNew becomes true and LoadLatestBtn appears.LoadLatestBtn; truncateAndInvalidate clears cached pages beyond page 1 and triggers a background refetch; list scrolls to top.Error path: Network failure → isError becomes true → PostFeedErrorMessage renders with detectKnownError() classification and a retry button.
[Diagram: Sequence — Authenticated Feed Load]
Participants: ReplyNotificationSettings (representative of all 10 notification settings screens), useNotificationSettingsQuery, useNotificationSettingsUpdateMutation, AT Protocol PDS, PreferenceControls
Sequence:
useNotificationSettingsQuery() fires on mount; fetches AppBskyNotificationDefs.Preferences from the AT Protocol backend.preferences.reply is passed to PreferenceControls; toggle group renders with current channel state.onChangeChannels(['list']) fires in PreferenceControls.ax.metric('activityPreference:changeChannels', ...) fires for analytics.useNotificationSettingsUpdateMutation().mutate({ reply: { ...preference, push: false } }) is called.Error path: Mutation fails → error is silently swallowed (no error UI shown to user — see §14 Risks & Technical Debt).
[Diagram: Sequence — Notification Settings Update]
Participants: AppState (React Native), PostFeed, usePostFeedQuery, Messages screens, useMessagesEventBus
Sequence:
AppState changes to 'background'.useFocusEffect cleanup functions.AppState changes to 'active'.PostFeed's AppState.addEventListener callback fires; checkForNew() is called for the active feed.hasNew becomes true; LoadLatestBtn appears with a dot indicator.messagesBus.requestPollInterval(MESSAGE_SCREEN_POLL_INTERVAL) is re-registered via useFocusEffect.useRefreshOnFocus triggers a refetch of conversation lists when the Messages screen regains focus.Error path: Network unavailable on foreground → checkForNew() fails silently (network errors are swallowed in the polling path).
[Diagram: Sequence — Background → Foreground Transition]
Participants: SignupContext (reducer), StepInfo, StepHandle, StepCaptcha, useSubmitSignup, AT Protocol PDS, onboardingDispatch
Sequence:
SplashScreen; signup navigator mounts; SignupContext initializes with createInitialState().StepInfo renders; user enters email, password, date of birth, and optionally an invite code.dispatch({ type: 'next' }) advances to StepHandle.useActorAutocompleteQuery provides suggestions via HandleSuggestions.serviceDescription.phoneVerificationRequired, dispatch({ type: 'next' }) advances to StepCaptcha.CaptchaWebView (native) or CaptchaWebView.web (web) loads the hCaptcha challenge URL in a WebView/iframe.bsky.app and extracts the code parameter.dispatch({ type: 'setPendingSubmit', value: { verificationCode: code } }) stores the code.useSubmitSignup calls createAccount(...) on the AT Protocol PDS.onboardingDispatch({ type: 'start' }) initiates the onboarding flow.Error path: createAccount fails with InvalidInviteCodeError → user is returned to StepInfo with an error on the invite code field. Other errors → user is returned to StepInfo with a generic error message.
⚠️ This workflow continues beyond the documented screens. Steps involving the full onboarding wizard (StepProfile, StepInterests, StepSuggestedStarterpacks, StepFindContacts, StepFinished) are partially documented but the complete onboarding navigator structure is not fully covered in this document. Verify the complete workflow with the development team before treating this as a comprehensive process description.
[Diagram: Sequence — Signup Flow]
| Source | Data Type | Access Mechanism |
|---|---|---|
| AT Protocol AppView (bsky.app) | Feed posts, profiles, search results, notifications, trending topics | TanStack Query hooks in #/state/queries/ |
| AT Protocol PDS (bsky.social and others) | Account records, preferences, follow/like/repost records | TanStack Query mutation hooks |
| Bluesky Chat Service | Conversations, messages | ConvoProvider state machine; polling via useMessagesEventBus |
| Native device storage (iOS UserDefaults / Android SharedPreferences) | Background notification preferences (playSoundChat) |
ExpoBackgroundNotificationHandler native module |
| App-level persisted storage | Search history, account history, dev mode flags, demo mode | useStorage hook (abstracted; likely MMKV-backed) |
| React Query in-memory cache | All server state | TanStack Query cache; useQueryClient for manual manipulation |
React Context / useReducer |
Onboarding state, signup state, starter pack wizard state | useOnboardingInternalState, SignupContext, useWizardState |
#/state/preferences hooks |
User preferences (autoplay, in-app browser, trending, language, thread, external embeds) | Custom hooks backed by persisted storage |
The application uses a layered state management strategy:
useState; they read from the query cache.useReducer is used for multi-step wizard flows (onboarding, signup, starter pack wizard) and for shell-level state (minimal shell mode, active starter pack, logged-out view controls).useState is used for ephemeral UI state (loading flags, form inputs, dialog open/close, pull-to-refresh indicators).#/state/preferences/ abstract the underlying storage mechanism (likely MMKV) for user preferences that survive app restarts.useProfileShadow provides a local optimistic overlay on top of TanStack Query-cached profile data, enabling immediate UI updates for follow/block/like actions.Route params are used as the primary mechanism for passing data between screens in the navigator stack:
ProfileFollows, ProfileFollowers, KnownFollowers, ProfileSearch — receive name (handle or DID) as a route param; resolve it to a full profile via useResolveDidQuery + useProfileQuery.PostRepostedBy, PostLikedBy, PostQuotes — receive name and rkey params; construct an AT URI via makeRecordUri.ActivityList — receives posts (URI-encoded list of post URIs) as a route param.Hashtag, Topic — receive topic (URI-encoded string) as a route param.MessagesConversation — receives conversation (convo ID) and optional accept flag.StarterPack — receives name, rkey, and optional new flag.| Data | Storage Mechanism | Encrypted | Notes |
|---|---|---|---|
| AT Protocol session tokens (accessJwt, refreshJwt) | Session store (abstracted) | [Not documented — WHO: Mobile platform lead; WHAT: What storage mechanism backs the session store — is it SecureStore/Keychain, MMKV, or AsyncStorage? Is it encrypted?; WHERE: §10.4 Persistence Layer] | Critical security concern — see §14 |
Background notification preferences (playSoundChat) |
Native module (iOS UserDefaults / Android SharedPreferences) | No | Non-sensitive boolean flag |
| Search term history | useStorage hook (per-account) |
No | Non-sensitive; capped at 6 items |
| Profile DID history | useStorage hook (per-account) |
No | Non-sensitive; capped at 10 items |
| Dev mode flag | useStorage hook |
No | Non-sensitive boolean |
| Demo mode flag | useStorage hook |
No | Non-sensitive boolean; iOS only |
| User preferences (autoplay, language, thread, etc.) | #/state/preferences hooks |
No | Non-sensitive UI preferences |
| App icon selection | useCurrentAppIcon hook (abstracted) |
No | Non-sensitive |
| NUX completion state | useSaveNux / AT Protocol preferences |
No | Stored server-side |
| Shared preferences (E2E test keys) | SharedPrefs native module |
No | Test-only; non-sensitive |
AT Protocol API response (JSON)
→ @atproto/api SDK (typed deserialization)
→ TanStack Query cache (in-memory, keyed by RQKEY)
→ Custom hook (usePostFeedQuery, useProfileQuery, etc.)
→ Screen component (reads data, isLoading, error)
→ Moderation layer (moderateProfile, moderatePost)
→ Sanitization (sanitizeDisplayName, sanitizeHandle, cleanError)
→ UI rendering (PostFeed, ProfileHeader, etc.)
For preferences:
AT Protocol preferences API
→ usePreferencesQuery (TanStack Query)
→ normalizeSort / normalizeView / channels derivation (useMemo)
→ Toggle.Group / Toggle.Item (controlled components)
→ useNotificationSettingsUpdateMutation / useSetFeedViewPreferencesMutation
→ AT Protocol preferences API (write)
| Mechanism | Used For | Examples |
|---|---|---|
| Route params | Screen-to-screen data transfer via navigation | name/rkey for profile/post screens; topic for Hashtag/Topic |
| TanStack Query cache | Shared server state across screens | Profile data shared between Profile header and Profile sections |
| React Context | Wizard step state | useOnboardingInternalState, SignupContext, useWizardState |
| Profile shadow cache | Optimistic follow/block state | useProfileShadow used in Profile header, Chat List, Explore |
| Shell state atoms | Global UI state | useSetMinimalShellMode, useActiveStarterPack, useCurrentConvoId |
useSession |
Current account identity | Used in all authenticated screens |
Safe-area handling is consistently delegated to the Layout component system. Layout.Screen, Layout.Header.Outer, and Layout.Content are shared primitives that internally handle SafeAreaView behavior. Individual screen components do not directly import SafeAreaView or call useSafeAreaInsets in most cases.
Exceptions where useSafeAreaInsets is called directly:
ProfileHeaderShell (Shell.tsx) — for back button and status bar gradient positioning.GrowableBanner — for spinner positioning on iOS.StatusBarShadow — for gradient height.SignupQueued — for bottom padding of the fixed footer.Deactivated — for top and bottom padding of the content area.Takendown — for bottom padding of the fixed button panel.This pattern is consistent and appropriate. No screens were observed to lack safe-area handling where it would be expected.
Keyboard handling is inconsistent across the codebase:
KeyboardAwareScrollView from react-native-keyboard-controller for automatic keyboard avoidance.KeyboardAvoidingView because they contain no text inputs.Dialog.ScrollableInner, DisableEmail2FADialog) handle keyboard avoidance internally.StepDetails (starter pack wizard) has a multiline description field with minHeight: 150 but no KeyboardAvoidingView — the keyboard may obscure this field on smaller devices.LoginForm uses Keyboard.dismiss() imperatively in onPressSelectService and onPressNext.There is no single, app-wide KeyboardAvoidingView strategy. Keyboard handling is addressed per-screen or per-dialog as needed.
react-native-gesture-handler is used in the application, evidenced by:
GestureActionView — uses Gesture and GestureDetector from react-native-gesture-handler for swipe-to-action in feed and chat list rows.BlockDrawerGesture — prevents drawer-open gestures within horizontal scroll areas (used in SearchHistory, SuggestedAccountsTabBar, ExploreTrendingVideos).DraggableScrollView — uses useDraggableScrollView for mouse-drag scrolling on web.[Not documented — WHO: Mobile platform lead; WHAT: Is GestureHandlerRootView wrapping the root of the application? If not, gesture handling may fail on Android; WHERE: §11.3 Gesture Handling System and §14 Risks & Technical Debt]
The platform-branching strategy is consistent across the assessed screens. The primary mechanism is the IS_IOS, IS_ANDROID, IS_NATIVE, and IS_WEB constants from #/env, supplemented by the platform(), native(), ios(), android(), and web() style utilities from #/alf.
The .web.tsx file-extension split is used exclusively for screens that have no viable web implementation (throwing an error or returning null). This is a deliberate, documented pattern.
No screens were observed using Platform.OS === 'ios' or Platform.select() directly in screen-level code; these are abstracted behind the constants. This is a positive consistency finding.
The error handling strategy is layered but partially inconsistent:
isError and error fields. Screens branch on isError to show error UI (typically Admonition, ListMaybePlaceholder, or ErrorScreen).cleanError() is applied before displaying error strings to users across all observed screens.isNetworkError(e) is used in several screens (login, password reset, forgot password) to show a connectivity-specific message.ErrorScreen component is used as a full-screen error state but is not a React Error Boundary.logger.error(...) is used consistently for non-user-facing error logging.Authentication is enforced at the navigator level (not within individual screens). Screens assume a valid session exists and use useSession() to access currentAccount. The useRequireAuth() hook is used in specific action handlers (e.g., follow/unfollow in ProfileHeaderStandard) to redirect unauthenticated users to login before executing mutations.
Auth tokens are managed by the @atproto/api BskyAgent instance, which handles token refresh automatically. Screens do not directly read or write JWT tokens.
The session storage mechanism is abstracted behind useSession / useSessionApi. The underlying storage (SecureStore, Keychain, MMKV, or AsyncStorage) is not visible in the assessed screen documentation and represents a security concern (see §14 Risks & Technical Debt).
Loading and empty states are handled consistently using shared components:
ListMaybePlaceholder — used across list screens (Profile Followers, Known Followers, Activity List, Starter Pack, Find Contacts Settings) for loading, error, and empty states.Loader — a shared spinner component used inline in buttons (during mutations) and as a full-screen loading indicator.Admonition — used in settings screens to show error states when preference fetches fail.FeedFeedLoadingPlaceholder / PostFeedLoadingPlaceholder — skeleton placeholders for feed loading states.ChatListLoadingPlaceholder — skeleton for chat list loading.The pattern of showing a Loader spinner when preference is undefined (loading) and an Admonition error when isError is true is applied consistently across all 10 notification settings screens.
| System | Integration | Scope |
|---|---|---|
| Sentry | @sentry/react-native/expo plugin in app.config.js; getSentryExpoConfig in metro.config.js; conditional on SENTRY_AUTH_TOKEN |
Crash reporting and error monitoring |
| Bitdrift | @bitdrift/react-native plugin in app.config.js with networkInstrumentation: true |
Network request instrumentation |
| Internal logger | #/logger module; logger.error, logger.warn, logger.debug |
Structured application logging |
| Analytics | useAnalytics() hook (#/analytics); ax.metric(...) |
Custom event tracking (provider not identified in assessed documentation) |
[Not documented — WHO: Analytics/data engineering team; WHAT: What is the analytics provider behind useAnalytics() / ax.metric()? Is it PostHog, Mixpanel, a custom system, or another provider?; WHERE: §11.8 Logging & Observability]
| Pattern | Usage |
|---|---|
FlatList virtualization |
PostFeed (home feed, profile sections, activity list), ChatList, Inbox, SearchResults, Explore, SavedFeeds, FindContactsSettings, KnownFollowers, ProfileFollowers, StepProfiles, StepFeeds |
FlashList |
Not observed in assessed documentation |
React.memo |
PostFeed, FeedItemInner, PostContent, ChatListItem, SuggestedProfileCard, SearchResults sub-components, List |
useMemo |
Feed item arrays, moderation decisions, derived preference values, interest display names, navigator sections |
useCallback |
Event handlers in all list-heavy screens; focus effect callbacks |
react-native-reanimated (UI-thread animations) |
Profile header (growable banner/avatar, status bar shadow), gesture actions, appearance settings, app passwords dialog, onboarding layout |
useInitialNumToRender |
PostFeed, KnownFollowers, SearchResults, Topic — dynamically calculates initial render count based on screen height |
truncateAndInvalidate |
Feed refresh — preserves first page of cache to avoid loading flash |
enabled: active pattern |
SearchResults tabs, TopicScreenTab, StepProfiles, StepFeeds — prevents inactive tabs from fetching |
staleTime: Infinity |
StepDetails profile query — prevents refetch during wizard flow |
GestureHandlerRootView at app root — Not confirmed in the assessed documentation. If absent, react-native-gesture-handler gestures may fail on Android. This is a critical risk (see §14 Risks & Technical Debt).ErrorBoundary component usage is observed in the assessed screen documentation. Unhandled render errors (e.g., from the .web.tsx stubs that throw) would propagate to the nearest error boundary or crash the app.onError default) is observed.FlashList from @shopify/flash-list — Not observed in the assessed screens; FlatList is used throughout. For very long lists, FlashList would offer better performance.NetInfo-based offline detection or queued-action mechanism is observed in any assessed screen. Network failures surface as error states requiring manual retry.@react-navigation/native-stack is used as the navigation library. All screens are typed via NativeStackScreenProps<ParamList, RouteName>.ParamList pattern (CommonNavigatorParams, AllNavigatorParams, MessagesTabNavigatorParams) provides compile-time safety for route params.app.config.js) for managing Info.plist, AndroidManifest.xml, and native build settings. EAS Build provides reproducible cloud builds without requiring local Xcode/Android Studio setup for most developers.ios/ and android/ directories are generated and managed by Expo's config plugin system. buildReactNativeFromSource: true is set for iOS (always) and Android (production only), increasing build times.useQuery, useMutation, or useInfiniteQuery hooks in #/state/queries/.#/state/queries/ creates a clear service layer boundary.RQKEY, FEED_RQKEY, etc.) must be maintained carefully to avoid stale data. The useQueryClient hook is used directly in some screens for manual cache manipulation (truncateAndInvalidate, resetQueries, setQueryData).@atproto/api SDK. No alternative REST APIs, GraphQL endpoints, or direct database connections are used.app.bsky.unspecced namespace (used in trending topics and recommendations) is explicitly unstable and subject to breaking changes.IS_IOS, IS_ANDROID, IS_NATIVE, and IS_WEB boolean constants from #/env, supplemented by platform(), native(), ios(), android(), and web() style utilities from #/alf. File-extension splits (.web.tsx) are used only for screens with no viable web implementation.Platform.OS string comparisons scattered throughout the codebase. The #/alf utilities enable platform-conditional styles without StyleSheet.create duplication.#/env module must be kept in sync with actual platform detection logic. The constants are build-time values, meaning they cannot be changed at runtime.@lingui/core, @lingui/react) is used for all internationalization. All user-facing strings are wrapped in msg, <Trans>, <Plural>, or t\...`macros. Thebabel-plugin-lingui-macro` transforms these at build time.compileNamespace: 'ts' setting in lingui.config.ts generates TypeScript-typed message catalogs.eslint-plugin-lingui and bsky-internal/avoid-unwrapped-text ESLint rules enforce this at the linting level. Translation catalogs must be maintained for 40+ locales.babel-plugin-react-compiler (targeting React 19) is included in the Babel configuration. The eslint-plugin-react-compiler enforces compiler compatibility rules.useMemo, useCallback, and React.memo annotations. The target: '19' setting enables React 19 compatibility mode.'react-compiler/react-compiler': 'warn'), indicating the compiler is not yet fully enforced across the codebase.rsa-v1_5-sha256) is applied to all production and TestFlight updates. The checkAutomatically: 'NEVER' setting means updates are checked programmatically (via Updates.checkForUpdateAsync() in AboutSettings).appVersion runtime version policy ties OTA updates to the native binary version, preventing incompatible JS bundles from being applied.IS_TESTFLIGHT || IS_PRODUCTION). Development builds do not receive OTA updates. The About Settings screen exposes an OTA update UI for internal builds.modules/ using the Expo Modules API:
expo-background-notification-handler — background notification sound preferencesexpo-bluesky-gif-view — native GIF renderingexpo-emoji-picker — native emoji pickerexpo-scroll-forwarder — scroll position forwarding between native viewsexpo-bluesky-swiss-army (includes SharedPrefs) — cross-platform native key-value storageNativeModules API.@sentry/react-native/expo but is conditionally enabled — only when SENTRY_AUTH_TOKEN is present in the build environment. The Metro config uses getSentryExpoConfig unconditionally, but the Expo plugin is only added when USE_SENTRY is true.SENTRY_AUTH_TOKEN environment variable gates production-only instrumentation.SENTRY_AUTH_TOKEN is not configured. The ax.logger.error calls observed in screen documentation may route to Sentry in production but to the internal logger in development.newArchEnabled: false is set in app.config.js, keeping the application on the legacy bridge architecture.metro.config.js explicitly disables unstable_enablePackageExports due to compatibility issues with Lingui and other packages.| Quality Attribute | Current State | Evidence | Risk Level |
|---|---|---|---|
| Performance | Good — virtualized lists, memoization, UI-thread animations | FlatList with windowSize, maxToRenderPerBatch, removeClippedSubviews in PostFeed; React.memo on ChatListItem, SuggestedProfileCard; react-native-reanimated worklets in profile header |
Low |
| Maintainability | Good — consistent patterns, typed navigator params, shared component system | Layout.*, SettingsList.*, PreferenceControls shared across screens; CommonNavigatorParams / AllNavigatorParams typed param lists; ESLint rules enforce import ordering and text wrapping |
Low–Medium |
| Security | Partial — session token storage mechanism not confirmed; mutation errors silently swallowed | cleanError() applied before display; sanitizeDisplayName/sanitizeHandle applied before rendering; BLUESKY_MOD_SERVICE_HEADERS for moderation routing; hCaptcha host allowlist in CaptchaWebView; session token storage mechanism not visible in assessed documentation |
Medium |
| Reliability | Partial — no centralized error boundary; mutation errors silently swallowed in notification settings | ListMaybePlaceholder for list error states; cleanError for error display; logger.error for observability; no React Error Boundary observed; notification settings mutation errors not surfaced to users |
Medium |
| Portability | Good — single codebase for iOS, Android, and web; .web.tsx stubs for incompatible screens |
IS_NATIVE/IS_WEB constants; platform()/web()/native() style utilities; .web.tsx file splits for contacts, app icon, video feed screens |
Low |
| Usability | Good — consistent loading/empty states, i18n for 40+ locales, accessibility rules enforced | ListMaybePlaceholder, Loader, Admonition used consistently; Lingui macros on all strings; eslint-plugin-react-native-a11y in ESLint config; useReducedMotion() respected in animations |
Low |
| Internationalization | Excellent — 40+ locales, compile-time extraction, pluralization support | lingui.config.ts defines 40+ locales; babel-plugin-lingui-macro; <Plural> used for follower/following counts; i18n.number() for locale-aware number formatting |
Low |
| Testability | Partial — testID props on key interactive elements; E2E screen present |
testID on buttons, inputs, and screens throughout; SharedPreferencesTester E2E screen; no unit test patterns visible in assessed documentation |
Medium |
| ID | Category | Description | Severity | Recommendation |
|---|---|---|---|---|
| R-1 | Security Risk | Session token storage mechanism not confirmed. The useSession/useSessionApi hooks abstract the session store, but the underlying storage (SecureStore/Keychain vs. AsyncStorage vs. MMKV) is not visible in the assessed documentation. If tokens are stored in unencrypted AsyncStorage, they are accessible to other apps on rooted/jailbroken devices. |
High | Audit #/state/session implementation to confirm tokens are stored in SecureStore (iOS) / Android Keystore-backed storage. Document the storage mechanism in this section. |
| R-2 | Risk | GestureHandlerRootView wrapper not confirmed. react-native-gesture-handler is used in GestureActionView and BlockDrawerGesture, but the presence of GestureHandlerRootView at the app root is not confirmed in the assessed documentation. Without it, gestures may fail silently on Android. |
High | Verify that the root component wraps the navigator in <GestureHandlerRootView style={{ flex: 1 }}>. |
| R-3 | Tech Debt | Notification settings mutation errors silently swallowed. All 10 notification settings screens use useNotificationSettingsUpdateMutation without onError callbacks. A failed preference save gives the user no feedback and may leave them believing their change was saved. |
Medium | Add onError handling to PreferenceControls to show a toast or inline error when the mutation fails. |
| R-4 | Tech Debt | SettingsListItem.web.tsx is an empty stub. The web implementation of the App Icon Settings list item returns undefined with no UI. This is a known incomplete implementation. |
Medium | Implement the web variant or explicitly document that app icon customization is not supported on web. |
| R-5 | Tech Debt | VideoFeed/index.web.tsx returns null. The web video feed is a stub with no implementation. |
Medium | Implement the web video feed or document the intentional exclusion. |
| R-6 | Tech Debt | StarterPackCard.onFollowAll does not return early on mutation error. After the bulkWriteFollows catch block, execution continues to setIsFollowingAll(true) and setIsProcessing(false), marking the operation as successful even when it failed. |
Medium | Add return after the error handling in the bulkWriteFollows catch block. |
| R-7 | Tech Debt | STARTER_PACK_MAX_SIZE off-by-one in wizard state. The profile cap check uses > (strictly greater than), meaning the cap is STARTER_PACK_MAX_SIZE + 1 profiles before blocking. |
Low | Change the check to >= to enforce the cap at exactly STARTER_PACK_MAX_SIZE. |
| R-8 | Risk | decodeURIComponent without try/catch in Hashtag, Topic, and ActivityList screens. A malformed URI-encoded route param would throw an unhandled URIError, crashing the screen. |
Medium | Wrap decodeURIComponent calls in try/catch with a fallback value. |
| R-9 | Tech Debt | TODO remove double login in LoginForm. A code comment documents a known issue where login() may be called twice in some flow. |
Low | Investigate and resolve the double-login condition. |
| R-10 | Tech Debt | Unpinned feeds list in SavedFeeds uses plain map(). The unpinned feeds section renders with Array.prototype.map() rather than a virtualized list. For users with many saved feeds, this could cause performance issues. |
Low | Migrate the unpinned feeds list to FlatList or FlashList if user research indicates large saved feed lists are common. |
| R-11 | Risk | app.bsky.unspecced endpoints used in production. ExploreTrendingTopics and ExploreRecommendations use AppBskyUnspeccedDefs types, indicating reliance on unstable, unspecced AT Protocol endpoints that may change or be removed without notice. |
Medium | Monitor AT Protocol changelog for stabilization of these endpoints; implement graceful degradation if they become unavailable. |
| R-12 | Tech Debt | SharedPreferencesTester E2E screen has no production build guard. The screen is accessible in production builds (no __DEV__ or IS_INTERNAL guard) and exposes the ability to write to shared preferences storage. |
Medium | Add an IS_INTERNAL guard to prevent this screen from being accessible in production builds. |
| R-13 | Tech Debt | stickyHeaderIndices computation in Explore is O(n²). items.indexOf(curr) inside a reduce over the items array produces quadratic complexity. For current data volumes this is not a bottleneck, but it should be addressed if the items array grows. |
Low | Track indices during the items array construction rather than searching for them afterward. |
| R-14 | Risk | No React Error Boundary observed. Render errors in screen components (e.g., from .web.tsx stubs that throw) would propagate to the nearest error boundary or crash the app. No ErrorBoundary component usage is observed in the assessed documentation. |
Medium | Implement React Error Boundaries at the navigator level and around major feature areas. |
| R-15 | Tech Debt | newArchEnabled: false — New Architecture not adopted. The application runs on the legacy bridge architecture, missing JSI performance benefits and Turbo Module lazy loading. |
Low | Plan a migration to the New Architecture; audit all native modules for Fabric/Turbo Module compatibility. |
| Term | Definition |
|---|---|
| AT Protocol | Authenticated Transfer Protocol — an open standard for federated social networking developed by Bluesky. Defines lexicons (schemas), XRPC (HTTP-based RPC), and DIDs for identity. |
| AppView | The Bluesky application-layer service that aggregates and indexes AT Protocol data for efficient querying (e.g., bsky.app). |
| PDS (Personal Data Server) | An AT Protocol server that hosts a user's repository of records (posts, follows, likes, preferences). Users may self-host or use a hosted PDS (e.g., bsky.social). |
| DID (Decentralized Identifier) | A globally unique, persistent identifier for an AT Protocol account (e.g., did:plc:abc123). Used as the canonical identity key throughout the app. |
| AT URI | An AT Protocol URI identifying a specific record (e.g., at://did:plc:abc123/app.bsky.feed.post/rkey123). |
| Lexicon | An AT Protocol schema definition for a record type or XRPC method (e.g., app.bsky.feed.post, com.atproto.server.createAccount). |
| XRPC | A JSON-over-HTTP RPC convention used by the AT Protocol. Queries are GET requests; procedures are POST requests. |
| Feed Generator | An AT Protocol service that provides a custom algorithmic feed. Identified by a feedgen AT URI. |
| Starter Pack | A Bluesky feature allowing users to bundle a list of accounts and feeds for easy onboarding of new users. |
| Labeler | An AT Protocol moderation service that applies content labels to posts and accounts. |
| NUX (New User Experience) | A one-time onboarding prompt or card shown to new users. Tracked via the Nux enum and useSaveNux hook. |
| Bluesky+ | A Bluesky subscription tier that unlocks premium app icons and other features. |
| App Clip | An iOS feature allowing users to experience a lightweight version of the app without installing it. Used for starter pack onboarding. |
| Germ DM | A third-party direct messaging integration (com.germnetwork.declaration) visible in the profile header. |
| TID (Timestamp ID) | A lexicographically sortable, collision-resistant identifier used as record keys in AT Protocol repositories. Generated via TID.nextStr() from @atproto/common-web. |
| Profile Shadow | A local optimistic overlay on top of a cached AT Protocol profile, allowing follow/block state changes to reflect immediately without a server round-trip. |
| Ozone | Bluesky's moderation service infrastructure. Reports and appeals are routed to Ozone via BLUESKY_MOD_SERVICE_HEADERS. |
| Term | Definition |
|---|---|
| Navigator | A React Navigation component that manages a set of screens and their transitions (e.g., NativeStackNavigator, BottomTabNavigator). |
| ParamList | A TypeScript type mapping route names to their parameter shapes (e.g., CommonNavigatorParams, AllNavigatorParams). Used to type-check navigation.navigate() calls and route.params. |
| Route key | The string name of a screen within a navigator (e.g., 'ProfileFollows', 'MessagesConversation'). |
| Stack navigator | A React Navigation navigator that manages screens in a push/pop stack with slide transitions. Used throughout the app via @react-navigation/native-stack. |
| Tab navigator | A React Navigation navigator that manages screens accessible via a tab bar. Used for the main app tabs (Home, Search, Notifications, Messages). |
| Drawer navigator | A React Navigation navigator that manages screens accessible via a side drawer. Presence in this app is inferred from BlockDrawerGesture usage — verify against root navigator definition. |
| Deep link | A URL that navigates directly to a specific screen within the app (e.g., bsky.app/profile/alice.bsky.social). Configured via associatedDomains in app.config.js. |
| Native module | A module that bridges JavaScript to platform-native code (Swift/Kotlin). Custom native modules in this app use the Expo Modules API. |
| Platform branching | The practice of rendering different code paths based on the current platform (iOS, Android, web). In this app, implemented via IS_IOS, IS_NATIVE, IS_WEB constants and .web.tsx file splits. |
| Metro bundler | The JavaScript bundler used by React Native. Configured in metro.config.js; extended with Sentry's Expo config. |
| Hermes | The JavaScript engine used by React Native on iOS and Android. Enabled by default in modern React Native versions. |
| JSI (JavaScript Interface) | React Native's synchronous bridge between JavaScript and native code. Not yet active in this app (newArchEnabled: false). |
| Fabric | React Native's New Architecture renderer. Not yet active in this app (newArchEnabled: false). |
| Turbo Module | React Native's New Architecture native module system with lazy loading. Not yet active in this app (newArchEnabled: false). |
| Managed workflow | An Expo workflow where Expo manages the native project files. This app uses the bare workflow instead. |
| Bare workflow | An Expo workflow where the developer has full access to the native ios/ and android/ directories. Used by this app. |
| EAS (Expo Application Services) | Expo's cloud build and update infrastructure. Used for binary builds (EAS Build) and OTA updates (EAS Updates). |
| OTA update | An over-the-air JavaScript bundle update delivered via EAS Updates without requiring an App Store release. |
| TanStack Query | A server state management library (also known as React Query) providing caching, background refetching, and pagination for async data. Used exclusively for all AT Protocol API calls in this app. |
FeedDescriptor |
A pipe-delimited string encoding the type and identity of a feed (e.g., "following", `"feedgen |
RQKEY / FEED_RQKEY |
Functions that produce TanStack Query cache keys for specific query types. Used to identify and invalidate cached data. |
| Profile shadow | See Domain Terms above. |
cleanError |
A utility function that strips internal error prefixes and stack traces from API error strings before displaying them to users. |
sanitizeDisplayName / sanitizeHandle |
Utility functions that normalize user-generated display names and handles before rendering, including LTR enforcement to prevent BiDi text spoofing. |
| Lingui | The internationalization library used in this app. Provides compile-time string extraction (msg, <Trans>, <Plural> macros) and runtime translation via useLingui(). |
#/alf |
The internal Atomic Layout Framework — the app's design system providing style atoms (a.*), theme tokens (t.atoms.*, t.palette.*), and platform utilities (platform(), native(), web(), ios(), android()). |
| Portal group | A React pattern (implemented via createPortalGroup()) that allows child components to inject content into a parent's designated outlet without prop drilling. Used in SettingsList.Group and the onboarding Layout. |
useProfileShadow |
A hook that wraps a TanStack Query-cached profile with a local optimistic overlay, enabling immediate UI updates for follow/block/like actions. |
truncateAndInvalidate |
A TanStack Query cache utility that truncates cached pages to the first page and marks the query as stale, triggering a background refetch without a full loading flash. |