Nativestarter
Sample Readiness Report

Moving Bluesky to SwiftUI and Compose

Written from Bluesky's open-source app in the same format a team receives. We have no relationship with Bluesky, and this says nothing about their plans. It is a static read of a public repository; a report for your team also runs the app and talks to the team.

Bluesky is a trademark of Bluesky Social PBC. We then migrated the whole app with the Migration Pack, starting with the screens this report recommends. See the port and its results.

Recommendation

Stay on React Native for the core app. Move specific surfaces to native.

Repository
bluesky-social/social-app
Commit
ff10dbd
Commit date
2026-09-11
Targeted move
135–210 engineer-days

1Summary

Bluesky is not a React Native app that happens to have a few native modules. It is a web, iOS and Android product sharing one TypeScript codebase, with 89 web-specific files against 21 native-specific ones, and it already runs three pieces of pure native code as app extensions. A move to SwiftUI and Jetpack Compose would not replace one codebase with two. It would replace one with three, because the web client cannot come along.

The team also already does the hard native work React Native usually hides: ten local native modules, load-bearing patches to React Native and expo-notifications, and a signed OTA pipeline they operate themselves. That capability is exactly what makes a targeted native move cheap for them, and a full rewrite expensive.

If we were advising this team, we would recommend keeping React Native as the host and moving the two or three surfaces where native gives users something measurable — composer keyboard handling, video, and Android feed scrolling — behind native screens.

2At a glance

React Native
0.86.0
Expo SDK
57 (expo 57.0.8)
Architecture
Continuous Native Generation — no committed ios/ or android/
Navigation
React Navigation 7, 89 screen registrations, 78 screens plus 5 tab navigators
Targets
iOS, Android and web from one codebase
App source
1,625 TypeScript files, ~231,000 lines
Runtime dependencies
164
Local native modules
10, about 47 Swift / Kotlin / Objective-C files
Patched dependencies
17 patches, 1,298 lines, including React Native itself
OTA updates
expo-updates against a self-hosted, code-signed update server
E2E
21 Maestro flows, plus a perf test and a setup flow

3Native surface

Grouped by area rather than package by package. Difficulty is how hard the swap is for this app, not for apps in general.

Push and background notifications

High

expo-notifications (patched), modules/expo-background-notification-handler, BlueskyNSE

iOS
UNUserNotificationCenter, APNs, Notification Service Extension
Android
FirebaseMessagingService, NotificationCompat, channels

The patch is marked load-bearing: Android background handling routes through a custom module.

OTA updates

High

expo-updates (patched), updates.bsky.app, code signing

iOS
No equivalent for code — phased releases, remote config
Android
No equivalent for code — staged rollouts, Play In-App Updates

checkAutomatically is NEVER, so the app controls when updates apply. That release lever disappears.

Video playback and compression

High

expo-video, @bsky.app/video, @bsky.app/video-compressor, expo-video-thumbnails

iOS
AVPlayer, AVAssetExportSession
Android
Media3 ExoPlayer, Media3 Transformer

Already partly native via Bluesky's own packages; a good candidate to move first.

Keyboard and composer

High

react-native-keyboard-controller (patched, used in 15 files), expo-paste-input, @bsky.app/react-native-uitextview

iOS
SwiftUI TextEditor / UITextView with keyboard layout guide
Android
Compose BasicTextField, WindowInsets.ime

Composer behaviour is where users feel React Native most; also where parity checks matter most.

Lists, pagers and gestures

Medium

react-native-pager-view (patched), react-native-screens (patched), reanimated + worklets, gesture-handler, scroll-forwarder module

iOS
SwiftUI List / ScrollView, TabView paging
Android
LazyColumn, HorizontalPager

The screens patch fixes an Android RecyclerView crash during transitions — a sign of real list pain on Android.

Media and camera

Medium

expo-camera, expo-image, expo-image-picker, expo-image-manipulator, @bsky.app/expo-image-crop-tool, expo-media-library (patched), react-native-view-shot

iOS
AVFoundation, PhotosPicker, Core Image
Android
CameraX, Photo Picker, Coil

Mostly well-trodden native APIs; the crop tool and view-shot usage need a closer look.

Storage and device

Medium

react-native-mmkv, @react-native-async-storage/async-storage, expo-file-system, swiss-army SharedPrefs, expo-device, expo-application

iOS
UserDefaults / Keychain, FileManager
Android
DataStore, Android Keystore, Context.filesDir

A data migration is needed so existing sessions survive the switch.

App integrity and auth

Medium

react-native-device-attest, expo-web-browser, expo-linking

iOS
App Attest / DeviceCheck, ASWebAuthenticationSession, Universal Links
Android
Play Integrity API, Custom Tabs, App Links

Used in sign-up's captcha step. The repo's own type note says the Android variant implements Play Integrity only.

Observability and experiments

Low

@sentry/react-native, @bitdrift/react-native, @growthbook/growthbook-react

iOS
Sentry Cocoa, bitdrift iOS SDK, GrowthBook Swift SDK
Android
Sentry Android, bitdrift Android SDK, GrowthBook Kotlin SDK

Native SDKs exist for all three. Event names must match for parity checks.

Small platform utilities

Low

expo-haptics (patched), expo-clipboard, expo-sharing, expo-localization, expo-blur, expo-glass-effect, expo-screen-orientation, expo-keep-awake, and others

iOS
UIKit / SwiftUI built-ins
Android
Android framework / Compose built-ins

Individually trivial; together a long tail worth a checklist.

4Custom native code

Ten local modules and three iOS app extensions. This is native code the team already writes and owns, which matters for every recommendation below.

5Patches

17 patches totalling 1,298 lines. Two are labelled load-bearing in their own notes. The largest:

6The hard parts

  1. 1

    The web client does not migrate

    89 web-specific files and a shared component system (ALF) mean native iOS and Android would diverge from web immediately. Every feature would afterwards be built three times.

  2. 2

    Notifications span four codebases

    expo-notifications with a load-bearing patch, a custom Android background handler, an Android badge-count workaround, and an iOS Notification Service Extension.

  3. 3

    Losing a self-hosted, signed OTA pipeline

    The team controls when updates apply. Replacing that with store releases, remote config and phased rollouts changes how incidents are handled.

  4. 4

    The composer

    Keyboard controller in 15 files, custom text view, paste handling. It is the most-used surface and the hardest to prove parity on.

  5. 5

    Seventeen patches encode production knowledge

    Each patch is a bug the team already found. A rewrite has to rediscover or deliberately retire every one of them.

7Checkpoint order

  1. 1Parity harness first: run the existing 21 Maestro flows against a native build as well as the React Native one. Maestro drives both.
  2. 2Video playback and compression, as native views hosted in the React Native app. Already partly native, measurable, isolated.
  3. 3Android feed and pager screens, where the screens and pager-view patches point at real list pain.
  4. 4Composer, once parity checks are routine. Highest user impact, highest risk.
  5. 5Stop there and measure. Notifications, OTA and auth only move if a full rewrite is chosen.

8Brownfield or rewrite

Recommendation: brownfield

Bluesky already hosts pure native code (App Clip, share extension, notification service extension) next to React Native. Adding native screens inside the existing app keeps web, keeps OTA for everything else, and lets each move be reverted. A rewrite only makes sense if the team decides web should become a separate product.

9Effort estimate

AreaEngineer-days
Parity harness on native builds15–25
Video surfaces (both platforms)30–45
Android feed and pagers25–40
Composer (both platforms)45–70
Hosting, navigation bridging, release changes20–30
Targeted move135–210

For comparison, a full native rewrite of iOS and Android: roughly 78 screens × 2 platforms × 0.75–1.5 days, plus 90–140 days for the native surface above and 25 days of harness — about 230–400 engineer-days, before web divergence costs.

Assumptions

  • Agent-assisted checkpoint workflow with human review, not unassisted rewriting.
  • Engineers already comfortable in Swift and Kotlin; hiring time excluded.
  • Day ranges are from route and dependency counts in the repository plus our judgement — an order of magnitude, not a quote.
  • No change to the AT Protocol client, backend or web client.

10Stay on React Native if

  • Web stays a first-class client built from the same code. For Bluesky it is.
  • The team keeps shipping OTA fixes weekly and values that lever.
  • Performance problems are concentrated in a few surfaces rather than everywhere.

11Open questions for the team

  • How often are OTA updates actually shipped, and how many were incident fixes?
  • Which surfaces generate the most performance complaints or crashes, per platform?
  • Is there appetite for native-only features (widgets, Live Activities) that would change the calculation?
  • Who would own native iOS and Android code long term?

Evidence read: package.json, app.config.js, src/Navigation.tsx, modules/, plugins/, patches/, __e2e__/flows/, AGENTS.md

Want this for your app?

The Pack's assess-rn-app skill produced this inventory, and your agent can run it on your repository. If you want the founder to write the report, that is a support option: five working days, including a call with your team.