bluesky-social/social-app
May 5, 2026
| Stakeholder | Recommended Sections |
|---|---|
| Executives and product managers | Executive Summary (§4), Stakeholders & Concerns (§5), Quality Attributes (§13) |
| React Native developers joining the team | Solution Strategy (§7), Building Block View (§8), Data Flow & State Management (§10), Cross-Cutting Concerns (§11), Glossary (§15) |
| Architects evaluating the system | All sections |
| iOS/Android engineers working on native modules | System Context (§6), Solution Strategy §7.2, Architecture Decisions AD-2, AD-9, AD-11, Cross-Cutting Concerns §11.3 |
| QA engineers and test automation | Runtime View (§9), Risks & Technical Debt (§14) |
| Security reviewers | System Context (§6), Architecture Decisions AD-2, AD-8, Data Flow §10.4, Cross-Cutting Concerns §11.6, Risks §14 |
The Bluesky social-app is a cross-platform social networking application built on the AT Protocol (Authenticated Transfer Protocol), a decentralized social networking standard. The application enables users to post short-form content, follow other users, engage with feeds curated by third-party feed generators, send direct messages, manage moderation preferences, and participate in a federated social graph. The primary users are individuals seeking an open, decentralized alternative to centralized social media platforms. The application targets iOS, Android, and web browsers from a single React Native codebase.
The technical approach is built on Expo's bare workflow with React Native, enabling access to custom native modules while retaining Expo's build tooling (EAS Build) and over-the-air update infrastructure (EAS Updates). The AT Protocol SDK (@atproto/api) serves as the sole data access layer — there is no traditional REST API or GraphQL layer; all data operations are AT Protocol XRPC calls to the user's Personal Data Server (PDS) and the Bluesky AppView. TanStack Query (React Query) manages all server state, caching, and pagination. React Navigation provides the navigator hierarchy. Lingui handles internationalization across more than 40 locales. React Native Reanimated drives all scroll-synchronized and gesture-driven animations on the UI thread.
The most significant architectural characteristics of this system are: (1) its deep coupling to the AT Protocol as both the identity and data layer, making the application a first-class AT Protocol client rather than a traditional mobile app with a proprietary backend; (2) its cross-platform scope — iOS, Android, and web are all served from the same codebase with platform branching handled through Platform.OS checks, platform() utilities, and .web.tsx file splits; (3) its use of a large number of custom Expo native modules for capabilities such as GIF playback, emoji picking, shared preferences, and scroll forwarding; and (4) its investment in performance through Reanimated UI-thread animations, TanStack Query caching, and the React Compiler for automatic memoization.
This Architecture Overview covers 131 screens of the Bluesky social-app React Native application. The screens assessed include: Inbox, Shell (Search), Feed, Images (Onboarding), Choose Account Form, Status Bar Shadow, Shell (Profile Header), Handle Suggestions, Miscellaneous Notification Settings, Account Settings, Settings (Messages), Utils (Search), Mention Notification Settings, Settings, Deactivated, Log, Hashtag, No Saved Feeds Of Any Type, No Following Feed, No Feeds Pinned, Find Contacts Flow, List Hidden, Shared Preferences Tester, Form Container, Set New Password Form, State (Onboarding), Login Form, Chat List, Emoji Picker, Value Proposition Pager.shared, Gif, Password Updated Form, Verification Settings, Forgot Password Form, Value Proposition Pager, Expo Scroll Forwarder, Index.web (Step Find Contacts Intro), Index.web (Step Find Contacts), Activity List, Layout (Onboarding), Conversation, Avatar Creator Items, Edit Profile Dialog, Growable Banner, Starter Pack Card, Feed Section, Types (Onboarding Step Profile), Const (Post Thread), Value Proposition Pager.web, Interest Button, Metrics, Handle, Growable Avatar, Avatar Circle, Util (Onboarding), Post Liked By, Status Bar Shadow.web, Profile Labeler Liked By, Explore Trending Videos, Explore Trending Topics, Suggested Follows, Profile Followers, Profile Follows, Known Followers, Profile Search, Explore Recommendations, Feed (Profile Sections), Explore Suggested Accounts, Labels (Profile Sections), Search Results, Saved Feeds, Types (Profile Sections), Explore Interests Card, Explore, App Passwords, Activity Privacy Settings, External Media Preferences, Index.web (App Icon Settings), Settings List Item.web (App Icon Settings), App Icon Image, Settings List Item (App Icon Settings), Types (App Icon Settings), Automation Label Settings, Accessibility Settings, Appearance Settings, About Settings, Interests Settings, Content And Media Settings, Find Contacts Settings, Following Feed Preferences, Legacy Notification Settings, Reposts On Reposts Notification Settings, New Follower Notification Settings, Reply Notification Settings, Quote Notification Settings, Privacy And Security Settings, Like Notification Settings, Thread Preferences, Likes On Reposts Notification Settings, Repost Notification Settings, Splash, Starter Pack, Gesture Action, Signup Queued, Draggable Scroll, Takendown, Index.web (Video Feed), Error, Back Next Buttons, Step Details (Starter Pack Wizard), Captcha Web, Types (Video Feed), State (Signup), State (Starter Pack Wizard), Starter Pack Landing, Topic, Step Profiles (Starter Pack Wizard), Step Feeds (Starter Pack Wizard), Policies, Captcha Web View.web, Placeholder Canvas, Display Name, Post Quotes, Avatar Creator Circle, Activity Notification Settings, Profile Header Standard, Post Reposted By, Language Settings, Error State, About Section, and Profile Header Labeler.
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 131 screens represent a significant but not exhaustive portion of the full codebase.
Generated by Inkwell Forge — automated codebase documentation analysis. Subject matter expert review is recommended before distribution.
| Stakeholder | Role | Key Concerns |
|---|---|---|
| iOS Users | Primary mobile platform audience | Performance, Face ID/Touch ID support, iOS-specific UX conventions (swipe-back, haptics, safe area), App Clip support, dynamic app icons |
| Android Users | Secondary mobile platform audience | Back button behavior, adaptive icons, FCM push notifications, material conventions, large heap support |
| Web Users | Browser-based audience | Responsive layout, keyboard navigation, no native module dependencies, web-specific scroll behavior |
| Bluesky Social (Product Team) | Application owner and AT Protocol operator | Feature velocity, AT Protocol compliance, moderation tooling, content safety |
| Third-Party Feed Generator Operators | External service providers | Feed API compatibility, error surfacing, graceful degradation when feeds are offline or misconfigured |
| Labeler/Moderation Service Operators | External moderation service providers | Labeler subscription UI, label display, subscription limits |
| React Native Developers (Internal) | Primary engineering audience | Code maintainability, consistent patterns, testability, platform branching clarity |
| App Store Reviewers (Apple/Google) | Gatekeepers for distribution | Privacy manifest compliance, permission usage descriptions, content policy adherence |
| Localization Contributors | Community translators | Lingui catalog completeness, string extraction correctness, 40+ locale support |
Derived from patterns observed across the 131 assessed screens:
The Bluesky social-app JavaScript bundle runs inside the React Native runtime on iOS and Android, and inside a browser on web. The application boundary encompasses the React Native JavaScript bundle, the custom Expo native modules, and the platform-specific native layers (iOS CocoaPods-linked modules, Android Gradle-linked modules).
Outside the application boundary are: the AT Protocol network (the user's Personal Data Server, the Bluesky AppView, feed generator servers, and labeler services), the Bluesky chat service (a separate DM service endpoint), the Bluesky moderation service (Ozone), push notification infrastructure (APNs on iOS, FCM on Android), the EAS Updates server for OTA bundle delivery, the Sentry error reporting service (when SENTRY_AUTH_TOKEN is configured), and the Bitdrift network instrumentation service.
The mobile client → backend API boundary is the HTTPS/XRPC boundary between the React Native JavaScript runtime and the AT Protocol network. Authentication at this boundary uses AT Protocol session tokens (JWTs) managed by the BskyAgent from @atproto/api. The base URL for the primary Bluesky AppView is https://bsky.social (production) or https://staging.bsky.dev (staging). The chat service uses a separate endpoint routed via DM_SERVICE_HEADERS.
[Diagram: System Context — show the React Native application as a box in the center with the iOS runtime, Android runtime, AT Protocol network (PDS, AppView, feed generators, labelers), Bluesky chat service, Bluesky moderation service (Ozone), APNs, FCM, EAS Updates server, Sentry, and Bitdrift connected]
| External System | Direction | Protocol | Purpose |
|---|---|---|---|
| Native iOS Runtime | Internal-platform | Native modules / JSI | iOS Keychain, Face ID, camera, photo library, APNs, safe area insets, haptics, App Clip |
| Native Android Runtime | Internal-platform | Native modules / JNI | Android Keystore, biometrics, FCM, permissions, large heap, adaptive icons |
| AT Protocol PDS (Personal Data Server) | Outbound | HTTPS / XRPC | Account creation, session management, record reads/writes (profile, follows, blocks, likes, reposts, posts) |
| Bluesky AppView | Outbound | HTTPS / XRPC | Feed queries, search, actor lookups, moderation decisions, trending topics |
| Bluesky Chat Service | Outbound | HTTPS / XRPC | Direct message conversations, conversation management |
| Bluesky Moderation Service (Ozone) | Outbound | HTTPS / XRPC | Moderation reports, appeals |
| Third-Party Feed Generator Servers | Outbound | HTTPS / XRPC | Custom algorithmic feed content |
| Third-Party Labeler Services | Outbound | HTTPS / XRPC | Content labeling and moderation |
| APNs (Apple Push Notification Service) | Inbound | Native / APNs | iOS push notifications for new messages, mentions, likes, follows |
| FCM (Firebase Cloud Messaging) | Inbound | Native / FCM | Android push notifications |
EAS Updates Server (updates.bsky.app) |
Outbound | HTTPS | OTA JavaScript bundle delivery with RSA code signing |
| Sentry | Outbound | HTTPS | Crash reporting and error monitoring (conditional on SENTRY_AUTH_TOKEN) |
| Bitdrift | Outbound | HTTPS | Network instrumentation and observability |
| hCaptcha | Outbound (via WebView/iframe) | HTTPS | CAPTCHA verification during signup |
Germ DM (landing.ger.mx) |
Outbound | HTTPS (deep link) | Third-party direct messaging integration |
| Layer | Technology | Notes |
|---|---|---|
| Framework | React Native (Expo bare workflow) | newArchEnabled: false in app.config.js |
| Build System | EAS Build | iOS deployment target 15.1; Android compileSdkVersion 36, targetSdkVersion 35 |
| Navigation | React Navigation (Stack + Tab navigators) | Not Expo Router; file-based routing is not used |
| Server State | TanStack Query v5 (@tanstack/react-query) |
All AT Protocol data fetching, caching, pagination |
| Animation | React Native Reanimated | UI-thread worklets for scroll-driven and gesture-driven animations |
| Gesture Handling | React Native Gesture Handler | Swipe gestures in chat, feed interactions |
| Internationalization | Lingui (@lingui/core, @lingui/react) |
40+ locales; Babel macro for string extraction |
| AT Protocol Client | @atproto/api |
All XRPC calls, session management, type definitions |
| Compiler Optimization | React Compiler (babel-plugin-react-compiler, target '19') |
Automatic memoization |
| Crash Reporting | Sentry (@sentry/react-native) |
Conditional on SENTRY_AUTH_TOKEN environment variable |
| Network Observability | Bitdrift (@bitdrift/react-native) |
Network instrumentation plugin |
| OTA Updates | EAS Updates | RSA code signing with rsa-v1_5-sha256; checkAutomatically: 'NEVER' |
| Image Rendering | expo-image |
Replaces React Native's built-in Image for caching and performance |
| Video | expo-video |
Native video playback |
| Font | Inter (variable font on web, static weights on Android) | Loaded via expo-font |
| UI Component Library | Internal design system (#/alf, #/components/*) |
No third-party UI library (MUI, Radix, shadcn) |
| Web Bundler | Webpack (via @expo/webpack-config) |
For web builds; Metro for native |
The codebase uses multiple coexisting platform-branching strategies:
Platform.OS === 'ios' / 'android' / 'web' inline checks — Used throughout for platform-specific behavior (e.g., IS_IOS, IS_ANDROID, IS_NATIVE, IS_WEB constants from #/env).Platform.select({ ios: ..., android: ..., web: ... }) — Used for value-based branching (e.g., onEndReachedThreshold, maxToRenderPerBatch)..web.tsx file splits — Used to provide web-specific implementations or stubs. Examples observed: StatusBarShadow.web.tsx (returns null), ValuePropositionPager.web.tsx, CaptchaWebView.web.tsx, VideoFeed/index.web.tsx (returns null), StepFindContactsIntro/index.web.tsx (throws), StepFindContacts/index.web.tsx (throws), AppIconSettings/index.web.tsx (throws), AppIconSettings/SettingsListItem.web.tsx (empty stub).platform() utility from #/alf — Used within the design system for style-level platform branching (e.g., platform({ios: ..., android: ...})).native() / web() utilities from #/alf — Used to apply styles conditionally to native or web only.IS_NATIVE / IS_WEB constants — Build-time constants from #/env used to gate entire feature sections (e.g., the Notification Sounds section in Messages Settings is gated by IS_NATIVE).The primary strategy is inline IS_* constant checks for behavioral branching and .web.tsx file splits for complete platform-specific implementations or stubs.
The application uses React Navigation with a combination of Stack and Tab navigators. Expo Router (file-based routing) is not used. The navigator hierarchy is documented in Section 8.2.
Key navigation characteristics observed:
NativeStackScreenProps is used for type-safe screen props throughout.CommonNavigatorParams is a shared type defining route keys and their param shapes.navigation.replace() is used for redirect screens (e.g., LegacyNotificationSettings redirects to NotificationSettings).useFocusEffect is used extensively for screen-focus lifecycle management (shell mode, data refresh, analytics).associatedDomains (iOS) and intentFilters (Android) in app.config.js, targeting bsky.app and staging.bsky.app.[Not documented — WHO: Navigation/routing engineer; WHAT: What is the root navigator type (Stack or Tab) and what are the top-level tab names and their associated stack navigators? WHERE: Section 8.2, Navigator Hierarchy tree]
usePostFeedQuery, useListConvosQuery, useProfileQuery, useNotificationSettingsQuery). These hooks are the single source of truth for server state.useProfileShadow and similar hooks maintain a local optimistic copy of server data that reflects in-flight mutations before server confirmation.useProfileFollowMutationQueue and useProfileBlockMutationQueue serialize rapid user interactions to prevent race conditions.SettingsList.Group uses a custom Portal system to teleport icon and title children to a header row, enabling flexible layout composition.useReducer-style state machines with dispatch actions and ScreenTransition animations.#/alf design system: All styling uses atomic style tokens from the internal #/alf module rather than inline styles or a third-party UI library.react-native-view-shot) are loaded via React.lazy() to reduce initial bundle size.| Quality Goal | Architectural Approach |
|---|---|
| Cross-Platform Consistency | IS_* constants, .web.tsx file splits, platform() / native() / web() utilities from #/alf |
| AT Protocol Compliance | @atproto/api SDK as the sole data access layer; all mutations use AT Protocol lexicons; bsky.validate for schema validation |
| Performance at Scale | TanStack Query caching, FlatList virtualization (windowSize, maxToRenderPerBatch), Reanimated UI-thread animations, React Compiler for automatic memoization |
| Internationalization | Lingui with 40+ locale catalogs; msg macro for extraction; Trans/Plural components for rendering |
| Moderation & Safety | moderateProfile() applied at every profile render surface; moderation.ui() gates content display; sanitizeDisplayName() and sanitizeHandle() applied before rendering |
| Container | Technology | Responsibility |
|---|---|---|
| React Native JavaScript Bundle | Metro (native) / Webpack (web) | All screen components, hooks, business logic, AT Protocol API calls, navigation |
| iOS Native Layer | CocoaPods-linked modules | Custom Expo modules (GIF view, emoji picker, scroll forwarder, Swiss Army), expo-video, expo-blur, expo-linear-gradient, react-native-reanimated, react-native-gesture-handler, Sentry native SDK |
| Android Native Layer | Gradle-linked modules | Same custom Expo modules, FCM integration, large heap configuration, adaptive icons |
| AT Protocol Network | External | PDS, AppView, feed generators, labelers, chat service, moderation service |
| EAS Updates Server | External | OTA JavaScript bundle delivery |
The following tree is derived from screen documentation. Screens not covered in the assessed documentation are marked with [not assessed].
Root Navigator [type not documented — verify with navigation engineer]
├── Auth Flow (condition: user is not authenticated or account is deactivated)
│ ├── Splash (/src/view/com/auth/splash)
│ ├── Login Flow
│ │ ├── FormContainer (/login/form-container)
│ │ ├── ChooseAccountForm (/login/choose-account-form)
│ │ ├── LoginForm (/login/login-form)
│ │ ├── ForgotPasswordForm (/login/forgot-password-form)
│ │ ├── SetNewPasswordForm (/login/set-new-password-form)
│ │ └── PasswordUpdatedForm (/login/password-updated-form)
│ ├── Signup Flow
│ │ ├── State (/signup/state)
│ │ ├── BackNextButtons (/signup/back-next-buttons)
│ │ ├── StepInfo → Policies (/signup/step-info/policies)
│ │ ├── StepHandle → HandleSuggestions (/signup/step-handle/handle-suggestions)
│ │ ├── StepCaptcha → CaptchaWeb (/signup/step-captcha/captcha-web)
│ │ │ → CaptchaWebView.web (/signup/step-captcha/captcha-web-view.web)
│ │ └── SignupQueued (/signup-queued)
│ ├── Deactivated (/deactivated)
│ └── Takendown (/takendown)
│
└── Main Navigator [type: Tab — not documented, verify with navigation engineer]
├── Home Tab [not assessed]
│ └── Home Stack
│ ├── NoFeedsPinned (/home/no-feeds-pinned)
│ └── Feed (/src/view/com/feeds/feed)
│
├── Search Tab
│ └── Search Stack
│ ├── Shell (/search/shell)
│ ├── Utils (/search/utils)
│ ├── SearchResults (/search/search-results)
│ ├── Explore (/search/explore)
│ ├── Explore Modules:
│ │ ├── ExploreTrendingVideos (/search/modules/explore-trending-videos)
│ │ ├── ExploreTrendingTopics (/search/modules/explore-trending-topics)
│ │ ├── ExploreRecommendations (/search/modules/explore-recommendations)
│ │ ├── ExploreSuggestedAccounts (/search/modules/explore-suggested-accounts)
│ │ └── ExploreInterestsCard (/search/modules/explore-interests-card)
│ ├── Hashtag (/hashtag)
│ └── Topic (/topic)
│
├── Notifications Tab [not assessed]
│ └── Notifications Stack
│ └── ActivityList (/notifications/activity-list)
│
├── Messages Tab
│ └── Messages Stack
│ ├── ChatList (/messages/chat-list)
│ ├── Inbox (/messages/inbox)
│ ├── Conversation (/messages/conversation)
│ └── Settings (/messages/settings)
│
└── Profile/Settings Tab [not assessed]
└── Settings Stack
├── Settings (/settings)
├── AccountSettings (/settings/account-settings)
├── AppPasswords (/settings/app-passwords)
├── ActivityPrivacySettings (/settings/activity-privacy-settings)
├── ExternalMediaPreferences (/settings/external-media-preferences)
├── AppIconSettings (native only)
│ ├── Index.web (/settings/app-icon-settings/index.web) [throws on web]
│ ├── SettingsListItem (/settings/app-icon-settings/settings-list-item)
│ ├── SettingsListItem.web (/settings/app-icon-settings/settings-list-item.web) [stub]
│ ├── AppIconImage (/settings/app-icon-settings/app-icon-image)
│ └── Types (/settings/app-icon-settings/types)
├── AutomationLabelSettings (/settings/automation-label-settings)
├── AccessibilitySettings (/settings/accessibility-settings)
├── AppearanceSettings (/settings/appearance-settings)
├── AboutSettings (/settings/about-settings)
├── InterestsSettings (/settings/interests-settings)
├── ContentAndMediaSettings (/settings/content-and-media-settings)
├── FindContactsSettings (/settings/find-contacts-settings)
├── FollowingFeedPreferences (/settings/following-feed-preferences)
├── LegacyNotificationSettings (/settings/legacy-notification-settings) [redirects]
├── NotificationSettings [not assessed]
│ └── Notification Sub-Settings:
│ ├── MiscellaneousNotificationSettings (/settings/notification-settings/miscellaneous-notification-settings)
│ ├── MentionNotificationSettings (/settings/notification-settings/mention-notification-settings)
│ ├── RepostsOnRepostsNotificationSettings (/settings/notification-settings/reposts-on-reposts-notification-settings)
│ ├── NewFollowerNotificationSettings (/settings/notification-settings/new-follower-notification-settings)
│ ├── ReplyNotificationSettings (/settings/notification-settings/reply-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)
│ └── ActivityNotificationSettings (/settings/notification-settings/activity-notification-settings)
├── PrivacyAndSecuritySettings (/settings/privacy-and-security-settings)
├── ThreadPreferences (/settings/thread-preferences)
├── LanguageSettings (/settings/language-settings)
└── VerificationSettings (/moderation/verification-settings)
Profile Screens (accessible from multiple tabs):
├── Profile Header Shell (/profile/header/shell)
├── ProfileHeaderStandard (/profile/header/profile-header-standard)
├── ProfileHeaderLabeler (/profile/header/profile-header-labeler)
├── Profile Header Components:
│ ├── StatusBarShadow (/profile/header/status-bar-shadow)
│ ├── StatusBarShadow.web (/profile/header/status-bar-shadow.web) [returns null]
│ ├── GrowableBanner (/profile/header/growable-banner)
│ ├── GrowableAvatar (/profile/header/growable-avatar)
│ ├── DisplayName (/profile/header/display-name)
│ ├── Handle (/profile/header/handle)
│ ├── Metrics (/profile/header/metrics)
│ ├── SuggestedFollows (/profile/header/suggested-follows)
│ └── EditProfileDialog (/profile/header/edit-profile-dialog)
├── Profile Sections:
│ ├── Feed (/profile/sections/feed)
│ ├── Labels (/profile/sections/labels)
│ └── Types (/profile/sections/types)
├── ProfileFollowers (/profile/profile-followers)
├── ProfileFollows (/profile/profile-follows)
├── KnownFollowers (/profile/known-followers)
├── ProfileSearch (/profile/profile-search)
├── ProfileLabelerLikedBy (/profile/profile-labeler-liked-by)
└── ErrorState (/profile/error-state)
Post Screens:
├── PostLikedBy (/post/post-liked-by)
├── PostQuotes (/post/post-quotes)
└── PostRepostedBy (/post/post-reposted-by)
Onboarding Flow:
├── Layout (/onboarding/layout)
├── State (/onboarding/state)
├── Util (/onboarding/util)
├── StepProfile:
│ ├── AvatarCreatorItems (/onboarding/step-profile/avatar-creator-items)
│ ├── AvatarCreatorCircle (/onboarding/step-profile/avatar-creator-circle)
│ ├── AvatarCircle (/onboarding/step-profile/avatar-circle)
│ ├── PlaceholderCanvas (/onboarding/step-profile/placeholder-canvas)
│ └── Types (/onboarding/step-profile/types)
├── StepInterests → InterestButton (/onboarding/step-interests/interest-button)
├── StepSuggestedStarterpacks → StarterPackCard (/onboarding/step-suggested-starterpacks/starter-pack-card)
├── StepFindContactsIntro:
│ └── Index.web (/onboarding/step-find-contacts-intro/index.web) [throws on web]
├── StepFindContacts:
│ └── Index.web (/onboarding/step-find-contacts/index.web) [throws on web]
└── StepFinished:
├── Images (/onboarding/step-finished/images)
├── ValuePropositionPager (/onboarding/step-finished/value-proposition-pager)
├── ValuePropositionPager.shared (/onboarding/step-finished/value-proposition-pager.shared)
└── ValuePropositionPager.web (/onboarding/step-finished/value-proposition-pager.web)
Starter Pack Screens:
├── StarterPack (/starter-pack)
├── StarterPackLanding (/starter-pack/starter-pack-landing)
└── Wizard:
├── State (/starter-pack/wizard/state)
├── StepDetails (/starter-pack/wizard/step-details)
├── StepProfiles (/starter-pack/wizard/step-profiles)
└── StepFeeds (/starter-pack/wizard/step-feeds)
Feeds Screens:
├── SavedFeeds (/saved-feeds)
├── NoSavedFeedsOfAnyType (/feeds/no-saved-feeds-of-any-type)
└── NoFollowingFeed (/feeds/no-following-feed)
List Screens:
├── ListHidden (/list/list-hidden)
├── ProfileList → FeedSection (/profile-list/feed-section)
└── ProfileList → AboutSection (/profile-list/about-section)
Video Feed:
├── Index.web (/video-feed/index.web) [returns null]
└── Types (/video-feed/types)
Find Contacts Flow (/find-contacts-flow)
Utility Screens:
├── Log (/log)
├── Error (/src/view/com/util/error)
└── SharedPreferencesTester (/e2e/shared-preferences-tester) [E2E only]
Custom Animations:
└── GestureAction (/src/lib/custom-animations/gesture-action)
Pager Components:
└── DraggableScroll (/src/view/com/pager/draggable-scroll)
Native Modules (Custom Expo Modules):
├── EmojiPicker (/modules/expo-emoji-picker/src/emoji-picker)
├── GifView (/modules/expo-bluesky-gif-view/src/gif)
└── ExpoScrollForwarder (/modules/expo-scroll-forwarder/src/expo-scroll-forwarder)
⚠️ The root navigator type and the complete tab structure are not fully documented in the assessed screens. The navigator hierarchy above is reconstructed from route keys and screen props observed across 131 screens. Verify the complete navigator tree against the application's root navigation configuration file before treating this as authoritative.
[Diagram: Building Block View — show the navigator hierarchy tree as above, with screen components as leaves, native module integration points (EmojiPicker, GifView, ExpoScrollForwarder, SharedPrefs), and the AT Protocol API boundary]
| Building Block | Type | Used By | Responsibility |
|---|---|---|---|
usePostFeedQuery |
Custom hook (TanStack Query) | Feed, ActivityList, ProfileSections/Feed | Fetches paginated AT Protocol feed data; handles polling, pagination, and cache invalidation |
useListConvosQuery |
Custom hook (TanStack Query) | ChatList, Inbox | Fetches paginated conversation lists from the AT Protocol chat service |
useProfileQuery |
Custom hook (TanStack Query) | Profile screens, Settings, Conversation | Fetches a single AT Protocol actor profile |
useNotificationSettingsQuery |
Custom hook (TanStack Query) | All notification settings screens | Fetches AppBskyNotificationDefs.Preferences |
usePreferencesQuery |
Custom hook (TanStack Query) | Explore, SavedFeeds, FollowingFeedPreferences, VerificationSettings, InterestsSettings | Fetches app.bsky.actor.getPreferences |
useSession / useSessionApi |
Context hooks | All authenticated screens | Provides currentAccount, accounts, resumeSession, createAccount, logoutCurrentAccount |
moderateProfile |
AT Protocol SDK function | All profile render surfaces | Computes ModerationDecision from profile + ModerationOpts |
useProfileShadow |
Custom hook | Profile headers, ChatList, ActivityNotificationSettings | Applies optimistic local mutations to a profile object |
Layout.* |
Internal component system | All screens | Provides Layout.Screen, Layout.Header.*, Layout.Content, Layout.Center |
SettingsList.* |
Internal component system | All settings screens | Provides Container, Item, Group, LinkItem, PressableItem, Divider, BadgeText |
Toggle.* |
Internal component system | All settings and notification screens | Provides Group, Item, Radio, Platform, LabelText for checkbox/radio controls |
PostFeed |
Component | Feed, ActivityList, ProfileSections/Feed, FeedSection | Renders a virtualized, paginated list of posts with interstitials, polling, and error handling |
ProfileHeaderShell |
Component | ProfileHeaderStandard, ProfileHeaderLabeler | Outer wrapper providing banner, avatar, back button, live status, and moderation alerts |
GestureActionView |
Component (wraps GestureAction) |
ChatList, Inbox | Provides swipe-to-action gesture with animated background reveal |
BskyAgent (from @atproto/api) |
Service SDK | All data-fetching hooks | Authenticated AT Protocol XRPC client; manages session tokens |
#/alf design system |
Internal module | All screens | Provides atoms, useTheme, platform(), native(), web(), useBreakpoints(), useGutters() |
@lingui/react + @lingui/core |
i18n library | All screens | Provides Trans, Plural, msg, useLingui for 40+ locale support |
Participants: Root navigator, session state (useSession), BskyAgent, TanStack Query client, Splash screen, Auth flow, Main navigator
Sequence:
SessionProvider initializes and reads persisted session data from device storage.BskyAgent is configured with the stored access token.currentAccount is defined and not deactivated/takendown, the Main navigator is rendered; otherwise the Auth flow is rendered.Deactivated screen is shown.Takendown screen is shown.Splash screen is shown with "Sign In" and "Create Account" CTAs.onboardingDispatch({type: 'start'}) is called if the user is new; otherwise the Main navigator is shown directly.Error path: If session restoration fails (e.g., expired token), the user is redirected to the login flow. If the AT Protocol PDS is unreachable, the Deactivated or error screen is shown with a retry option.
[Diagram: Sequence — App Launch & Auth State Resolution]
Participants: PostFeed component, usePostFeedQuery hook, TanStack Query cache, AT Protocol AppView (app.bsky.feed.getTimeline / app.bsky.feed.getFeed), FlatList (via List component)
Sequence:
PostFeed mounts with a FeedDescriptor prop (e.g., "following" or "feedgen|at://...").usePostFeedQuery(feed, feedParams) fires; TanStack Query checks the cache for RQKEY(feed, feedParams).feedItems is computed via useMemo — pages are flattened, interstitials are inserted at configured positions, and blocked/muted authors are filtered.List renders the first initialNumToRender items (calculated by useInitialNumToRender).onEndReached fires when onEndReachedThreshold is crossed; fetchNextPage() is called.feedItems recomputes; List renders new items.setInterval at POLL_FREQ (60 seconds) calls pollLatest() to check for new content; if found, hasNew is set to true and LoadLatestBtn appears.Error path: Network errors during initial load render PostFeedErrorMessage with a retry button. Errors during fetchNextPage render LoadMoreRetryBtn at the bottom of the list. HTTP 429 from feed generators surfaces a "high traffic" message.
[Diagram: Sequence — Feed Load & Infinite Scroll]
Participants: AppState (React Native), PostFeed, useMessagesEventBus, useFocusEffect
Sequence:
AppState transitions to 'background'.PostFeed's AppState.addEventListener('change') listener fires; polling is paused.useFocusEffect cleanup fires in ChatList and Inbox).AppState transitions to 'active'.PostFeed's AppState listener fires checkForNew(), which calls pollLatest() to check for new feed content.ChatList and Inbox re-register their poll interval requests with useMessagesEventBus.useRefreshOnFocus hooks on focused screens trigger refetch() calls to refresh stale data.Error path: If the network is unavailable when the app returns to foreground, pollLatest() errors are silently ignored. Data remains stale until the next successful poll or user-initiated pull-to-refresh.
[Diagram: Sequence — Background → Foreground Transition]
Participants: ChatList, Inbox, useMessagesEventBus, useListConvosQuery, AT Protocol chat service
Sequence:
ChatList or Inbox gains focus; useFocusEffect fires.useMessagesEventBus().requestPollInterval(MESSAGE_SCREEN_POLL_INTERVAL) is called (10-second interval).useListConvosQuery is refetched via the event bus mechanism.useFocusEffect cleanup calls unsub(), removing the poll interval request.Error path: Poll errors are caught and logged via logger.error. The UI retains the last successfully fetched state.
[Diagram: Sequence — Messaging Real-Time Poll Cycle]
Participants: Signup state machine (useReducer), BskyAgent, AT Protocol PDS (com.atproto.server.createSession), hCaptcha (via CaptchaWeb / CaptchaWebView.web), useOnboardingDispatch
Sequence:
SignupContext initializes with createInitialState.StepInfo (email, password, date of birth, invite code).StepHandle (handle selection with HandleSuggestions).serviceDescription.phoneVerificationRequired is true, StepCaptcha is shown; CaptchaWeb (native) or CaptchaWebView.web (web) loads the hCaptcha challenge.code is extracted and stored in pendingSubmit.useSubmitSignup fires createAccount via useSessionApi(), sending email, handle, password, birthDate, inviteCode, and verificationCode to the AT Protocol PDS.onboardingDispatch({type: 'start'}) transitions the user to the Onboarding flow.Error path: Network errors surface a connectivity message. InvalidInviteCodeError surfaces a specific invite code message. Handle conflicts surface a handle-specific error.
[Diagram: Sequence — Account Creation (Signup) Flow]
| Source | Examples | Access Pattern |
|---|---|---|
| AT Protocol AppView | Feed posts, actor profiles, search results, trending topics, suggested feeds | TanStack Query hooks → BskyAgent XRPC GET |
| AT Protocol PDS | User preferences, saved feeds, notification settings, profile records | TanStack Query hooks → BskyAgent XRPC GET/PUT |
| AT Protocol Chat Service | Conversations, messages | TanStack Query hooks → BskyAgent XRPC GET/POST |
| Device sensors / OS APIs | Contacts (expo-contacts), camera, photo library, location |
Native module hooks |
| Push notifications | APNs (iOS), FCM (Android) | expo-notifications native module |
| Local device storage | Language preferences, theme preferences, search history, dev mode flags | useStorage hook (AsyncStorage-backed), persisted module |
| Native shared preferences | Notification sound settings (ExpoBackgroundNotificationHandler) |
Custom native module (expo-bluesky-swiss-army) |
| Static bundled assets | Onboarding images, splash screens, fonts | require() resolved at build time by Metro/Webpack |
The application uses a layered state management approach:
| Layer | Technology | Scope | Examples |
|---|---|---|---|
| Server state | TanStack Query v5 | Global, cached | Feed posts, profiles, preferences, conversations |
| Session/auth state | React Context (SessionProvider) |
Global | currentAccount, accounts, BskyAgent |
| Shell/UI state | React Context (ShellProvider) |
Global | minimalShellMode, colorMode, activeStarterPack |
| Preferences state | React Context + persisted storage | Global | Language, theme, content filters, notification settings |
| Wizard/flow state | useReducer + React Context |
Flow-scoped | Signup state, Onboarding state, Starter Pack Wizard state |
| Screen-local state | useState |
Screen-scoped | isPTRing, showAccounts, isScrolledDown |
| Optimistic/shadow state | Custom hooks (useProfileShadow) |
Component-scoped | Follow state, like state before server confirmation |
| Animation state | Reanimated SharedValue |
UI-thread | scrollY, transX, iconScale |
Route params represent data flowing between screens. Key patterns observed:
name (handle/DID) + rkey (record key) are passed as route params to PostLikedBy, PostQuotes, PostRepostedBy; the screen constructs the AT URI via makeRecordUri(name, 'app.bsky.feed.post', rkey).name (handle or DID) is passed to ProfileFollowers, ProfileFollows, KnownFollowers, ProfileSearch; the screen resolves it to a DID via useResolveDidQuery.q (query) and tab (active tab index) are passed to SearchScreenShell.conversation (convoId) and accept (boolean) are passed to Conversation.name + rkey are passed to StarterPack; code is passed to StarterPackScreenShort.pushToConversation (convoId) is passed to ChatList to auto-navigate on notification tap.| Data | Storage Mechanism | Encryption | Notes |
|---|---|---|---|
| Session tokens (access JWT, refresh JWT) | (inferred from useSession hook — verify against #/state/session implementation) |
(not documented — verify) | Managed by SessionProvider; likely Keychain (iOS) / Keystore (Android) |
| Language preferences | useStorage hook (AsyncStorage-backed) |
None | Keyed by account DID |
| Theme preferences | useStorage hook or persisted module |
None | Device-level |
| Search term history | useStorage(account, [did, 'searchTermHistory']) |
None | Up to 6 terms, keyed by account DID |
| Search account history | useStorage(account, [did, 'searchAccountHistory']) |
None | Up to 10 DIDs, keyed by account DID |
| Notification sound settings | ExpoBackgroundNotificationHandler native module |
None | Platform-native key-value (UserDefaults/SharedPreferences) |
| Dev mode flag | useStorage(device, [...]) |
None | Device-level |
| Policy update NUX state | device.set([PolicyUpdate202508], ...) |
None | Device-level storage |
| Reminders (email confirm snooze) | persisted.write('reminders', ...) |
None | AsyncStorage-backed |
| OTA update channel | useApplyPullRequestOTAUpdate |
None | iOS only, dev builds |
[Not documented — WHO: Platform/security engineer; WHAT: What storage mechanism is used for session tokens (access JWT, refresh JWT)? Is it Keychain on iOS and Keystore on Android, or AsyncStorage? Is it encrypted?; WHERE: Section 10.4, Persistence Layer table, "Session tokens" row]
The standard pipeline for AT Protocol data:
AT Protocol XRPC response (JSON)
→ TanStack Query cache (keyed by RQKEY)
→ Custom hook (e.g., usePostFeedQuery, useProfileQuery)
→ useMemo transformation (flatten pages, filter, deduplicate)
→ moderateProfile() / moderation.ui() (apply content moderation)
→ sanitizeDisplayName() / sanitizeHandle() (sanitize for display)
→ Screen component renders
For feed content specifically:
usePostFeedQuery → data.pages → feedItems (useMemo: flatten + interstitial insertion + author filter)
→ List → renderItem → PostFeedItem → RichText + embedded media
For notification settings:
useNotificationSettingsQuery → preferences → PreferenceControls
→ channels (useMemo from preference.list + preference.push)
→ Toggle.Group renders
→ user interaction → useNotificationSettingsUpdateMutation → API write
| Mechanism | Used For | Examples |
|---|---|---|
| TanStack Query cache | Shared server state across screens | Profile data shared between Profile header and Settings; feed data shared between Feed and ProfileSections/Feed |
| React Context (global) | Auth, shell, preferences | useSession, useSetMinimalShellMode, useLanguagePrefs |
| Navigation params | Screen-to-screen data transfer | Post URI, profile handle, conversation ID, search query |
useProfileShadow |
Optimistic profile state | Follow state reflected immediately in ChatList, ProfileHeader, and ActivityNotificationSettings |
precacheProfile / precacheConvoQuery |
Cache warming before navigation | ChatList pre-populates profile and conversation cache before navigating to Conversation screen |
unstableCacheProfileView |
Manual cache write | Search screen writes profile to cache on profile click |
queryClient.invalidateQueries / resetQueries |
Cross-screen cache invalidation | Profile update invalidates POST_FEED_RQKEY_ROOT and postThreadQueryKeyRoot; interest change resets 5 recommendation queries |
Safe-area handling is implemented consistently across the application using react-native-safe-area-context. The useSafeAreaInsets() hook is used in:
StatusBarShadow — sets gradient height to topInset to precisely cover the status bar area.ProfileHeaderShell — uses topInset for back button positioning.Deactivated screen — applies safeAreaInsets.bottom for bottom padding.SignupQueued screen — applies useSafeAreaInsets for bottom action area padding.Takendown screen — applies safe area insets for bottom action area.The Layout.Screen component (used as the root wrapper on virtually all screens) is expected to handle safe area insets system-wide. The react-native-edge-to-edge plugin is configured in app.config.js with enforceNavigationBarContrast: false for Android, enabling edge-to-edge rendering.
Keyboard handling is not fully consistent across the application:
KeyboardAwareScrollView from react-native-keyboard-controller is used in Takendown (appeal form), StepProfiles (search input), and StepFeeds (search input).KeyboardAvoidingView usage is not directly observed in the assessed screens; keyboard avoidance appears to be delegated to KeyboardAwareScrollView where needed.Keyboard.dismiss() is called in ForgotPasswordForm when the hosting provider selector is opened.TextField.Input components (used throughout settings and login screens) handle keyboard behavior via their own internal implementation.[Not documented — WHO: React Native developer; WHAT: Is there a global KeyboardAvoidingView wrapper at the navigator level, or is keyboard handling entirely per-screen? WHERE: Section 11.2, Keyboard Handling]
react-native-gesture-handler is used for swipe gestures in the messaging screens (GestureActionView in ChatList and Inbox) and for the DraggableScroll horizontal scroll component. The GestureDetector and Gesture APIs from react-native-gesture-handler are used in GestureAction.
The webpack configuration explicitly aliases react-native-gesture-handler to false on web ('react-native-gesture-handler': false), causing a build error if it is accidentally imported in web-only code. This is a deliberate architectural guard.
[Not documented — WHO: React Native developer; WHAT: Is GestureHandlerRootView wrapping the root navigator? If not, swipe gestures in GestureActionView may not function correctly on Android.; WHERE: Section 11.3, Gesture Handling System, and Section 14, Risks & Technical Debt]
Platform branching is not fully consistent across the codebase — multiple strategies coexist (see Section 7.2). This is an architectural smell but appears to be a deliberate pragmatic choice given the codebase's scale.
The most consistent pattern is the use of IS_* constants from #/env for behavioral branching and .web.tsx file splits for complete platform-specific implementations. The platform() / native() / web() utilities from #/alf are used for style-level branching.
Inconsistencies observed:
IS_NATIVE for feature gating; others use Platform.OS === 'ios' directly.Notification Sounds section in Settings (/messages/settings) is gated by IS_NATIVE; the App Icon Settings link in Settings (/settings) is gated by IS_INTERNAL && IS_NATIVE.SuggestedFollows component returns null on Android (IS_ANDROID) due to scroll interaction conflicts — this is a platform-specific behavioral difference not surfaced through the standard branching utilities.Error handling is per-screen and per-mutation rather than centralized:
isNetworkError(e) utility; surface a connectivity message to the user.try/catch blocks; error messages are sanitized via cleanError(e) before display.isError boolean and error object from query hooks; screens render error states (e.g., ListMaybePlaceholder, Admonition with type="error", ErrorScreen).Toast.show(...) for transient errors; inline ErrorMessage or Admonition for persistent errors.detectKnownError in PostFeedErrorMessage maps specific error strings to user-friendly messages (HTTP 429, BlockedActorError, FeedgenDoesNotExist, etc.).logger.error(...) from #/logger is used throughout for structured error logging. Sentry integration (when configured) captures crashes.No global React Error Boundary is observed in the assessed screens. Error boundaries, if present, are at the navigator or provider level.
Authentication is enforced at multiple layers:
currentAccount must be defined and not deactivated/takendown).useSession() provides currentAccount; components return null or redirect if no account is present.useRequireAuth() wraps mutating actions (e.g., follow, subscribe) to redirect unauthenticated users to sign-in.BskyAgent attaches the access JWT to all XRPC requests; the AT Protocol PDS rejects unauthenticated requests.The access JWT is managed by BskyAgent and refreshed automatically. The resumeSession function is called after account activation and handle changes to refresh the in-memory session.
App Password restriction: Several operations (account deactivation, handle changes) require a full-privilege session token, not an App Password. The server returns 'Bad token scope' for App Password sessions; the client surfaces this as a specific error message.
Loading and empty states are handled consistently through a set of shared components:
ListMaybePlaceholder: Used across feed, search, and list screens for loading/empty/error states with a spinner, error message, or empty message.PostFeedLoadingPlaceholder: Skeleton rows for the post feed during initial load.ChatListLoadingPlaceholder: Skeleton rows for the chat list.Loader: Spinner component used inline in buttons, headers, and content areas.EmptyState: Icon + message component for empty list states.Admonition: Info/warning/error callout box used for inline error states in settings screens.Skele.Text: Skeleton text placeholder used in ItemTextWithSubtitle during loading.The pattern of {data && (...)} guards (rendering nothing until data loads) is used in header subtitle areas (e.g., PostLikedBy, PostRepostedBy, ProfileFollowers).
#/logger): Used throughout for structured logger.error(...), logger.warn(...), logger.debug(...) calls. The Log screen (/log) provides an in-app viewer for the in-memory log buffer.@sentry/react-native): Configured conditionally via USE_SENTRY = Boolean(process.env.SENTRY_AUTH_TOKEN). Metro is configured via getSentryExpoConfig; Webpack uses sentryWebpackPlugin. Sentry captures crashes and errors in production builds.@bitdrift/react-native): Configured as an Expo plugin with networkInstrumentation: true, providing network-level observability.useAnalytics from #/analytics): A custom analytics abstraction used throughout. Fires ax.metric(eventName, payload) for user interactions. The underlying analytics provider is not identifiable from the assessed screens.app.config.js declares collected data types (crash data, performance data, diagnostic data) and accessed API types (file timestamps, disk space, system boot time, user defaults) in the iOS privacy manifest.| Pattern | Usage |
|---|---|
FlatList virtualization |
List component (wrapping Animated.FlatList) used in all feed, search, and list screens |
windowSize tuning |
windowSize={9} (Feed), windowSize={11} (ChatList, Explore), windowSize={platform({android: 11})} (Explore) |
maxToRenderPerBatch tuning |
maxToRenderPerBatch={5} (iOS Feed), maxToRenderPerBatch={1} (Android Feed), maxToRenderPerBatch={platform({android: 10, ios: 20})} (Explore) |
removeClippedSubviews |
removeClippedSubviews={true} in Feed |
useInitialNumToRender |
Custom hook calculating initial render count from screen height; used in Feed, ChatList, Hashtag, KnownFollowers |
useMemo for derived data |
Extensively used for flattening paginated data, computing feedItems, channels, items, profiles |
useCallback for handlers |
Extensively used for onRefresh, onEndReached, renderItem, onPress |
React.memo |
Applied to ChatListItem, PostFeed, List, SearchScreenInner, SuggestedProfileCard |
| Reanimated UI-thread worklets | All scroll-driven animations (banner parallax, avatar scale, status bar shadow) run on the UI thread |
| TanStack Query caching | All server state cached with RQKEY-based keys; staleTime and gcTime configured per query hook |
| Prefetching | shouldPrefetch = IS_NATIVE && isPageAdjacent enables feed query on adjacent tabs; precacheProfile / precacheConvoQuery warm cache before navigation |
FlashList |
Not observed in assessed screens; FlatList (via List) is the primary virtualization primitive |
| React Compiler | babel-plugin-react-compiler with target: '19' is configured globally; provides automatic memoization |
The following cross-cutting concerns are absent or not observed in the assessed documentation and are architecturally significant:
GestureHandlerRootView wrapper: Not observed in the assessed screens. If the root navigator is not wrapped in GestureHandlerRootView, swipe gestures in GestureActionView (used in ChatList and Inbox) may not function correctly on Android. This should be verified against the root component.@atproto/api) is the sole data access layer. There is no proprietary REST API, GraphQL endpoint, or direct database connection. All data operations are AT Protocol XRPC calls.detectKnownError function in PostFeedErrorMessage handles feed generator-specific error strings.app.config.js is the single source of truth for native configuration. Custom Expo plugins (withStarterPackAppClip, withGradleJVMHeapSizeIncrease, etc.) handle native configuration that cannot be expressed in app.config.js alone. iOS deployment target is 15.1; Android compileSdkVersion is 36.@tanstack/react-query) is used for all server state management. No Redux, Zustand, or MobX is used for server state.usePostFeedQuery, useProfileQuery). Cache keys (RQKEY) are the primary mechanism for cache invalidation across screens. queryClient.invalidateQueries and queryClient.resetQueries are used for cross-screen cache management.NativeStackScreenProps usage and CommonNavigatorParams type pattern — verify against the navigation configuration file) React Navigation provides more explicit control over navigator configuration and type-safe route params via ParamList types, which is important for a large codebase with many screens.CommonNavigatorParams. Deep links are configured via associatedDomains (iOS) and intentFilters (Android) in app.config.js.@lingui/core, @lingui/react) is used for all internationalization. The @lingui/babel-plugin-lingui-macro Babel plugin enables compile-time string extraction.Plural, and enables runtime locale switching.msg, Trans, or Plural macros. The lingui.config.ts defines 40+ supported locales. The eslint-plugin-lingui enforces correct macro usage. The bsky-internal/lingui-msg-rule ESLint rule enforces project-specific Lingui conventions.babel-plugin-react-compiler with target: '19' is configured globally in babel.config.js. The eslint-plugin-react-compiler enforces compiler compatibility.useMemo, useCallback, and React.memo equivalents, reducing the manual memoization burden on developers.react-compiler/react-compiler ESLint rule is set to 'warn' to surface incompatible patterns. Some screens still use explicit useMemo/useCallback for cases where the compiler cannot infer the correct behavior.useAnimatedStyle, useAnimatedProps, useAnimatedReaction, and interpolate run on the UI thread.SharedValue is used for scroll position; runOnJS bridges back to React state only when necessary (e.g., setActiveAction in GestureAction). collapsable={false} must be set on animated views that need to be measured.rsa-v1_5-sha256) with a certificate at ./code-signing/certificate.pem.checkAutomatically: 'NEVER' means the application controls when to check for updates (not on every launch). The runtimeVersion policy is 'appVersion', meaning OTA updates are scoped to a specific app version. The UPDATES_ENABLED flag gates OTA in TestFlight and production only (not development).newArchEnabled: false is set in app.config.js.newArchEnabled: false in app.config.js — verify against engineering decision records) The New Architecture may introduce compatibility issues with existing native modules and third-party libraries. The application prioritizes stability over adopting the New Architecture..web.tsx file splits are used to provide web-specific stubs that either throw an error, return null, or return an empty component..web.tsx files for web builds, ensuring native-only code is never bundled for web without requiring runtime IS_WEB checks in every component.StepFindContactsIntro/index.web.tsx, StepFindContacts/index.web.tsx, AppIconSettings/index.web.tsx) will cause React render errors if not caught by an error boundary. Stubs that return null or empty components (StatusBarShadow.web.tsx, VideoFeed/index.web.tsx, AppIconSettings/SettingsListItem.web.tsx) silently suppress features on web.modules/ directory: expo-bluesky-gif-view, expo-emoji-picker, expo-scroll-forwarder, expo-bluesky-swiss-army (shared preferences, background notification handler).expo-bluesky-swiss-army module's SharedPrefs index file contains stub implementations that throw NotImplementedError on web — the actual implementations are in platform-specific native code.@sentry/react-native) is integrated conditionally: USE_SENTRY = Boolean(process.env.SENTRY_AUTH_TOKEN). Metro is configured via getSentryExpoConfig; Webpack uses sentryWebpackPlugin.SENTRY_AUTH_TOKEN environment variable must be set in EAS Build secrets for production builds. The Metro configuration uses getSentryExpoConfig as the base, which may affect source map generation and bundle splitting.| Quality Attribute | Current State | Evidence | Risk Level |
|---|---|---|---|
| Performance | Good — FlatList virtualization, Reanimated UI-thread animations, TanStack Query caching, React Compiler | windowSize, maxToRenderPerBatch, removeClippedSubviews in Feed; useAnimatedStyle worklets in ProfileHeaderShell; RQKEY-based caching throughout |
Low |
| Maintainability | Moderate — consistent patterns (TanStack Query hooks, #/alf design system, Lingui) but multiple coexisting platform-branching strategies |
IS_* constants, .web.tsx splits, platform() utility all used for branching; 131 screens assessed with consistent hook patterns |
Medium |
| Security | Moderate — AT Protocol JWT auth, code-signed OTA updates, sanitizeDisplayName/sanitizeHandle, cleanError for error message sanitization; session token storage mechanism not documented |
SENTRY_AUTH_TOKEN-conditional Sentry; RSA code signing for OTA; validWebLink() in Policies screen; stateParam CSRF check in CaptchaWeb |
Medium |
| Reliability | Moderate — per-screen error handling, isNetworkError detection, TanStack Query retry; no global error boundary observed |
detectKnownError in PostFeedErrorMessage; logger.error throughout; cleanError for user-facing messages |
Medium |
| Portability | Good — iOS, Android, and web served from single codebase; .web.tsx stubs for native-only features |
IS_NATIVE, IS_WEB, IS_IOS, IS_ANDROID constants; .web.tsx file splits; webpack alias for react-native-gesture-handler: false on web |
Low |
| Usability | Good — consistent Layout.* shell, SettingsList.* patterns, Toggle.* controls; 40+ locale support; haptic feedback; safe area handling |
useSafeAreaInsets in multiple screens; useHaptics in profile and settings; Lingui with 40+ locales; useReducedMotion in animations |
Low |
| Internationalization | Good — Lingui with 40+ locales; msg macro for extraction; Trans/Plural for rendering; languageName() for locale-aware language display |
lingui.config.ts defines 40+ locales; bsky-internal/lingui-msg-rule ESLint rule; Plural used in header subtitles |
Low |
| Testability | Low — no test infrastructure observed in assessed screens; E2E test screen (SharedPreferencesTester) exists but test coverage is not documented |
testID props on key UI elements (e.g., testID="messagesInboxScreen", testID="nextBtn", testID="saveChangesBtn"); SharedPreferencesTester E2E screen |
High |
| ID | Category | Description | Severity | Recommendation |
|---|---|---|---|---|
| R-1 | Risk | GestureHandlerRootView wrapper not confirmed — if the root navigator is not wrapped in GestureHandlerRootView, swipe gestures in GestureActionView (ChatList, Inbox) may silently fail on Android |
High | Verify the root component wraps the navigator in GestureHandlerRootView; add to CI checks |
| R-2 | Risk | No global React Error Boundary observed — unhandled render errors in any screen could crash the entire navigator subtree | High | Add a global error boundary at the root navigator level with a fallback UI and Sentry error reporting |
| R-3 | Risk | Session token storage mechanism not documented — if tokens are stored in AsyncStorage (unencrypted) rather than Keychain/Keystore, they are vulnerable to extraction on rooted/jailbroken devices | High | Verify session token storage in #/state/session; migrate to Keychain (iOS) / Keystore (Android) if not already using them |
| R-4 | Tech Debt | SetError action defined in Starter Pack Wizard Action type but has no corresponding case in the reducer switch — dispatching {type: 'SetError'} has no effect |
Medium | Add the SetError case to the reducer or remove the action type from the union |
| R-5 | Tech Debt | likeUri variable name in ProfileHeaderLabeler holds the repost count — naming inconsistency between variable name and actual data |
Low | Rename to repostCount or quoteCount as appropriate |
| R-6 | Risk | Web stubs that throw (StepFindContactsIntro/index.web.tsx, StepFindContacts/index.web.tsx, AppIconSettings/index.web.tsx) will cause React render errors if reached on web without an error boundary |
Medium | Ensure error boundaries are in place above these screens on web; or redirect before rendering |
| R-7 | Tech Debt | No client-side rate limiting or debouncing on notification settings toggles — rapid toggling across 10+ notification settings screens could generate many sequential API calls | Low | Add debouncing (e.g., 500ms) to useNotificationSettingsUpdateMutation calls in PreferenceControls |
| R-8 | Tech Debt | unstableCacheProfileView in Search screen is marked "unstable" — may bypass normal cache validation |
Low | Investigate and stabilize or replace with a standard TanStack Query cache write |
| R-9 | Risk | MAX_LABELERS limit hardcoded as "twenty" in CantSubscribePrompt text — must be manually kept in sync with the MAX_LABELERS constant |
Medium | Replace hardcoded string with a dynamic reference to the MAX_LABELERS constant |
| R-10 | Tech Debt | // TODO remove double login comment in LoginForm — suggests a redundant session creation call that could cause double session creation |
Medium | Investigate and remove the redundant login call |
| R-11 | Tech Debt | onDismiss error in FindContactsSettings uses "Failed to follow all matches" error message text for a dismiss action — copy-paste error |
Low | Fix the error message to accurately describe the dismiss action failure |
| R-12 | Risk | PlaceholderCanvas renders a 750×750px off-screen view at all times while mounted — on lower-end devices this consumes GPU/memory resources |
Low | Unmount PlaceholderCanvas when not needed (e.g., after avatar capture completes) |
| R-13 | Tech Debt | SuggestedFollows component returns null on Android due to scroll interaction conflicts — this is a known platform limitation without a documented resolution path |
Medium | Document the Android limitation; investigate whether react-native-gesture-handler configuration changes could resolve the conflict |
| R-14 | Risk | Log screen (/log) has no authentication or authorization enforcement — any user who navigates to /sys/log can see the full system log including potentially sensitive runtime data |
High | Gate the Log screen behind IS_INTERNAL or an authenticated admin role check; ensure the logger does not capture sensitive data (passwords, tokens) |
| R-15 | Tech Debt | NewArchEnabled: false — the application is not using React Native's New Architecture (Fabric, Turbo Modules, JSI) |
Medium | Plan migration to New Architecture as third-party library support matures; track compatibility of key dependencies |
| R-16 | Risk | VideoFeed/index.web.tsx returns null — the video feed feature is not implemented on web, but the route may be accessible |
Low | Verify that the video feed route is not exposed on web, or add a proper "not available" UI |
| R-17 | Tech Debt | Multiple coexisting platform-branching strategies (IS_* constants, .web.tsx splits, platform() utility, Platform.OS checks) — inconsistency increases cognitive load for developers |
Medium | Establish and document a single preferred platform-branching strategy; migrate inconsistent usages over time |
| R-18 | Risk | ParamList typing — CommonNavigatorParams is used for type-safe route params, but not all screens may have fully typed params (e.g., @ts-ignore or any casts) |
Medium | Audit CommonNavigatorParams for completeness; ensure all route params are typed |
| Term | Definition |
|---|---|
| AT Protocol | Authenticated Transfer Protocol — a decentralized social networking protocol developed by Bluesky. All data operations in this application are AT Protocol XRPC calls. |
| PDS (Personal Data Server) | The AT Protocol server that stores a user's data (posts, follows, profile, preferences). Each user's data lives on their PDS. |
| AppView | The Bluesky-operated AT Protocol service that aggregates and indexes data from across the network, providing search, feed generation, and actor lookup. |
| DID (Decentralized Identifier) | A stable, globally unique identifier for an AT Protocol account (e.g., did:plc:abc123). DIDs do not change when a user changes their handle. |
| Handle | A human-readable username in the AT Protocol (e.g., alice.bsky.social). Handles are mutable; DIDs are stable. |
| XRPC | Cross-platform Remote Procedure Call — the HTTP-based RPC protocol used by the AT Protocol. Methods are defined by Lexicons. |
| Lexicon | An AT Protocol schema definition that specifies the shape of records, queries, and procedures (e.g., app.bsky.feed.post, com.atproto.server.createSession). |
| Feed Generator | A third-party server that implements the AT Protocol feed generator interface, providing custom algorithmic feeds. |
| Labeler | An AT Protocol service that applies content labels to posts and profiles for moderation purposes. |
| Starter Pack | An AT Protocol record that bundles a list of suggested accounts and feeds for new users to follow. |
| Convo | A conversation in the Bluesky chat system, identified by a convoId. |
| Facet | An AT Protocol rich text annotation (link, mention, hashtag) applied to a span of text in a post. |
| Shadow (profile) | A locally-cached optimistic copy of a profile that reflects in-flight mutations (e.g., follow state) before server confirmation. |
| App Clip | An iOS feature that allows users to experience a small part of an app without installing it. Bluesky uses an App Clip for starter pack landing pages. |
| Ozone | The Bluesky moderation service that processes moderation reports and appeals. |
| Germ DM | A third-party direct messaging service integrated into the Bluesky profile header. |
| NUX | New User Experience — an onboarding prompt or card shown to users who have not yet completed a specific onboarding step. |
| TID | Timestamp ID — a time-ordered, lexicographically sortable identifier used as record keys in AT Protocol repositories. |
| CAR file | Content Addressable aRchive — the binary format used to export an AT Protocol repository. |
| JSONL | JSON Lines — a format where each line is a valid JSON object, used for chat data export. |
| Term | Definition |
|---|---|
| Navigator | A React Navigation component that manages a set of screens and their transitions (e.g., Stack navigator, Tab navigator, Drawer navigator). |
| ParamList | A TypeScript type that maps route keys to their parameter shapes, enabling type-safe navigation in React Navigation (e.g., CommonNavigatorParams). |
| Route key | The string identifier for a screen within a navigator (e.g., 'MessagesInbox', 'NotificationSettings'). |
| Stack navigator | A React Navigation navigator that presents screens in a stack, with push/pop transitions. |
| Tab navigator | A React Navigation navigator that presents screens as tabs with a tab bar. |
| Deep link | A URL that navigates directly to a specific screen within the application (e.g., https://bsky.app/profile/alice.bsky.social). |
| Native module | A module that bridges JavaScript to platform-native code (Swift/Objective-C on iOS, Kotlin/Java on Android). |
| Platform branching | The practice of writing different code paths for different platforms (iOS, Android, web) within a single codebase. |
| Metro bundler | The JavaScript bundler used by React Native for native builds. Configured in metro.config.js. |
| Hermes | The JavaScript engine used by React Native (and this application). Provides faster startup and lower memory usage than V8. |
| JSI | JavaScript Interface — the C++ layer that enables direct communication between JavaScript and native code without the Bridge. Not yet enabled in this application (newArchEnabled: false). |
| Fabric | React Native's new rendering system (part of the New Architecture). Not yet enabled in this application. |
| Turbo Module | React Native's new native module system (part of the New Architecture). Not yet enabled in this application. |
| Managed workflow | An Expo workflow where Expo manages the native project files. This application uses the bare workflow instead. |
| Bare workflow | An Expo workflow where the developer has full access to the native iOS and Android project files. Used by this application. |
| EAS Build | Expo Application Services Build — the cloud build service used to compile iOS and Android binaries. |
| EAS Updates | Expo Application Services Updates — the OTA update service used to deliver JavaScript bundle updates. |
| Worklet | A Reanimated function that runs on the native UI thread rather than the JavaScript thread, enabling smooth animations. |
| SharedValue | A Reanimated value that can be read and written from both the JavaScript thread and the UI thread. Used for scroll position and animation state. |
RQKEY |
React Query Key — a function that generates a TanStack Query cache key for a specific query (e.g., RQKEY(feed, feedParams)). |
FeedDescriptor |
A string format used by usePostFeedQuery to identify which feed to load (e.g., "following", `"feedgen |
#/alf |
The internal design system module providing atomic style tokens, theme hooks, and platform utilities. Referenced via the #/ TypeScript path alias. |
| Portal | A React pattern (used in SettingsList.Group) that renders children into a different part of the component tree, enabling flexible layout composition. |
| Mutation queue | A pattern (used in useProfileFollowMutationQueue) that serializes rapid user interactions to prevent race conditions in AT Protocol mutations. |
IS_NATIVE / IS_WEB / IS_IOS / IS_ANDROID |
Build-time boolean constants from #/env that indicate the current platform. Used for platform branching throughout the codebase. |
cleanError |
A utility function from #/lib/strings/errors that sanitizes raw error objects into user-displayable strings. |
sanitizeDisplayName |
A utility function that applies AT Protocol moderation decisions to a display name before rendering. |
makeRecordUri |
A utility function from #/lib/strings/url-helpers that constructs an AT Protocol at:// URI from a handle/DID, collection namespace, and record key. |