ClaudeExpoJan 28, 2026, 2:45 PM

The solo dev playbook: ship faster with Expo, EAS Build, and OTA Updates

A condensed section focused on the key takeaways first.

Original Post

Quick Digest

Summary

A condensed section focused on the key takeaways first.

claudeenmodel: claude-sonnet-4-20250514

Building Cross-Platform Apps Fast with Expo: A Solo Developer's Guide

Key Points

  • Built cross-platform app in one week using Expo without native development
  • EAS Build automated certificate management and platform-specific compilation
  • OTA Updates enabled instant bug fixes without app store review delays

Summary

A comprehensive guide from Liam Du, founder of Wellspoken, on building and shipping a cross-platform React Native app in just one week using Expo's ecosystem. The article demonstrates how solo developers can leverage Expo SDK, EAS Build, and OTA Updates to rapidly develop feature-rich apps without native development complexity.

Key Points

  • Rapid Development: Built and shipped iOS/Android app in one week using Expo SDK 54 with TypeScript
  • Core Expo Features Used:
    • expo-audio: High-quality audio recording with hooks-based API (useAudioRecorder, useAudioPlayer)
    • expo-camera: Video recording with permission management and camera switching
    • expo-notifications: Push notifications without APNs/FCM configuration
    • expo-secure-store: Secure credential storage
  • EAS Services: Automated build processes, certificate management, and OTA updates for instant bug fixes
  • Tech Stack: React Native + Expo, OpenAI API, Assembly AI, RevenueCat, Langchain
  • No Native Code: Avoided Xcode/Android Studio entirely while accessing native device features
  • Production Ready: Includes subscription paywall, progress tracking, and AI-powered speech analysis

Full Translation

Translations

A translation section that keeps the flow of the original article.

claudejamodel: claude-sonnet-4-20250514

ソロ開発者のプレイブック:Expo、EAS Build、OTAアップデートでより速くリリースする

ソロ開発者のプレイブック:Expo、EAS Build、OTAアップデートでより速くリリースする

Users • React Native • Development • January 28, 2026 • 13分で読める

Liam Du ゲスト著者

Expoを使用してクロスプラットフォームアプリを高速で構築・リリースする方法:音声、カメラ、通知、セキュアストレージ、さらにEAS Build、Update、Submitを活用。

これはWellspokenの創設者であり、より良いコミュニケーションの熱心な支持者であるLiam Duからのゲスト投稿です。

始まり

私はチームに製品アイデアを説明しようとしている会議にいました。コンセプトは頭の中では明確でした - よく考え抜いていて、理にかなっていることも分かっていました - しかし話し始めると、言葉が間違って出てきました。後戻りしました。もう一度試しました。思考の流れを失いました。

同僚が私を止めました。「理解できません。」

私は再び試し、違う言い回しで説明しました。それでもめちゃくちゃでした。

それが続きました。同じ会議で何度も。試行するたびにより混乱していくのを感じ、それが表現をより悪くし、さらに混乱させました。正直、恥ずかしかったです。

私は何を言おうとしているかを知っていました。アイデアは明確でした。問題は、私の思考と、特にプレッシャーや緊張している時に、それらを明確に表現する能力との間のギャップでした。

しばらくの間、これは私だけの問題だと思っていました。しかし、その後どこでもそれに気づき始めました。明らかに話していることを知っているが、会話でそれをきれいに表現できない賢い人たち。

そこで、これを助けるツールを探しました。スピーチコーチング(高価)、パブリックスピーキングコース(問題が違う)、声の投影やステージプレゼンスに焦点を当てたアプリ(形式が違う)を見つけました。私が実際に必要としていることに対処するものはありませんでした:日常の会話中にその場で思考を構造化することが上手くなること。

そこで、アプリを構築することにしました - あなたの表現力のためのジムです。リアルタイムで思考を整理し、明確に表現する認知スキルを訓練する、日々の一口サイズの練習。

それをWellspokenと名付けました。

なぜこのアプリを構築するためにExpoを選んだのか

私は自分にiOSとAndroidの両方でリリースするために1週間だけを与えました。デザインとエンジニアリングの両方を行うソロ開発者として、インフラストラクチャやプラットフォーム固有の問題に時間を無駄にする余裕はありませんでした。実際の製品の構築に集中する必要がありました。

Expoがそれを可能にしました:

  • ネイティブ開発不要:Expo SDKにより、SwiftやKotlinを書くことなくネイティブ機能にアクセスできました。音声録音、ファイルシステムアクセス、通知—すべてクリーンなTypescript APIで処理されます。
  • ビルド設定地獄なし:EAS Buildがすべてのプラットフォーム固有のコンパイルを処理しました。XcodeやAndroid Studioを一度も開きませんでした。
  • 証明書管理なし:プロビジョニングプロファイル、署名証明書、キーストア— EASがすべてを自動的に処理しました。コマンドラインチュートリアルに従うだけで完了しました。
  • リリース後の高速反復:OTAアップデートにより、アプリストアのレビューを待つことなくバグを修正し、改善をプッシュできました。アップデートをプッシュすれば、ユーザーは次回の再起動時にそれを取得します。これはリリース後の最初の週で何度も私を救いました。

タイトなタイムラインを持つソロ開発者として、ネイティブ開発にコンテキストスイッチしたり、認証情報管理を扱ったりする必要がないことが、リリースとセットアップ地獄にはまることの違いを生みました。

Wellspokenの機能とアーキテクチャ

コア機能:

  • パーソナライズされた練習セッション(模擬面接、トピック説明、ロールプレイシナリオ)
  • 集中を失った場所や苦労した場所を特定するAI駆動の音声分析
  • 日々の5分間の練習
  • 進捗追跡とストリーク
  • 無料トライアル付きサブスクリプションペイウォール

技術スタック:

  • React Native + Expo SDK 54
  • TypeScript
  • Langchain
  • 音声分析用OpenAI API
  • 転写用Assembly AI
  • サブスクリプション用RevenueCat
  • EAS Build、Update、Submit

Expoの実装方法

Wellspokenの構築を大幅に簡単にした、私が使用したコアExpo機能を説明します。

1. expo-audio:コア録音体験

音声録音はWellspokenの基盤です。すべての練習モードで高品質な音声キャプチャが必要で、ネイティブコードを書くことなくiOSとAndroidで一貫して動作する必要がありました。

expo-audioは、クリーンなフックベースのAPIでこれを簡単にしました:

// components/RecordingButton.tsx
import { useAudioRecorder, useAudioRecorderState, AudioModule, RecordingPresets, setAudioModeAsync } from 'expo-audio';
import * as Haptics from 'expo-haptics';

export function RecordingButton() {
  const recorder = useAudioRecorder(RecordingPresets.HIGH_QUALITY);
  const recorderState = useAudioRecorderState(recorder);
  const [hasPermission, setHasPermission] = useState(false);

  async function startRecording() {
    try {
      // Request permissions
      const { status } = await AudioModule.requestRecordingPermissionsAsync();
      if (status !== 'granted') return;

      // Configure audio mode
      await setAudioModeAsync({
        allowsRecording: true,
        playsInSilentMode: true,
      });

      // Prepare and start recording
      await recorder.prepareToRecordAsync();
      recorder.record();
      Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium);
    } catch (error) {
      console.error('Failed to start recording:', error);
    }
  }

  async function stopRecording() {
    try {
      await recorder.stop();
      Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
      // recorder.uri contains the file path
      const audioUri = recorder.uri;
      // Upload to backend for analysis...
    } catch (error) {
      console.error('Failed to stop recording:', error);
    }
  }

  return (
    <TouchableOpacity onPress={recorderState.isRecording ? stopRecording : startRecording}>
      <View style={recorderState.isRecording ? styles.recording : styles.idle}>
        {recorderState.isRecording ? <StopIcon /> : <MicrophoneIcon />}
      </View>
    </TouchableOpacity>
  );
}

なぜこれが素晴らしいのか:

  • useAudioRecorderフックがすべての状態を管理
  • RecordingPresets.HIGH_QUALITYがプラットフォーム固有のエンコーディング設定を処理
  • 同じコードがiOSとAndroidで動作
  • ネイティブモジュール設定不要

このRecordingButtonコンポーネントをすべての練習モード—Q&A、フレームワーク練習、スピード分解、フリーフォーム—で使用しています。一度書けば、どこでも使えます。

音声再生も同様にクリーンです:

// components/AudioPlaybackBar.tsx
import { useAudioPlayer, useAudioPlayerStatus } from 'expo-audio';

export function AudioPlaybackBar({ audioUri }: { audioUri: string }) {
  const player = useAudioPlayer(audioUri);
  const status = useAudioPlayerStatus(player);

  function togglePlayback() {
    if (status.playing) {
      player.pause();
    } else {
      player.play();
    }
  }

  return (
    <View>
      <TouchableOpacity onPress={togglePlayback}>
        {status.playing ? <PauseIcon /> : <PlayIcon />}
      </TouchableOpacity>
      <Slider
        value={status.currentTime}
        maximumValue={status.duration}
        onSlidingComplete={(value) => player.seekTo(value)}
      />
      <Text>
        {formatTime(status.currentTime)} / {formatTime(status.duration)}
      </Text>
    </View>
  );
}

useAudioPlayerフックが自動的に再生状態を管理し、useAudioPlayerStatusが再生進捗のリアルタイム更新を提供します。

2. expo-camera:Daily 60ビデオ練習

Wellspokenの機能の一つは「Daily 60」—ユーザーがトピックを説明している自分を録画する60秒のビデオ練習です。これには前面/背面カメラの切り替え、ビデオ録画、カメラ権限が必要でした。

expo-cameraがこれらすべてを処理しました:

// screens/practice/Daily60PracticeScreen.tsx
import { CameraView, useCameraPermissions, useMicrophonePermissions } from 'expo-camera';
import * as Haptics from 'expo-haptics';

export function Daily60PracticeScreen() {
  const [cameraPermission, requestCameraPermission] = useCameraPermissions();
  const [micPermission, requestMicPermission] = useMicrophonePermissions();
  const [facing, setFacing] = useState<'front' | 'back'>('front');
  const [isRecording, setIsRecording] = useState(false);
  const cameraRef = useRef<CameraView>(null);

  async function startRecording() {
    if (!cameraRef.current) return;
    try {
      setIsRecording(true);
      Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Heavy);
      // Automatically stops at 60 seconds
      const video = await cameraRef.current.recordAsync({
        maxDuration: 60,
      });
      // video.uri contains the recorded video
      await handleVideoComplete(video.uri);
    } catch (error) {
      console.error('Recording failed:', error);
    }
  }

  async function stopRecording() {
    if (!cameraRef.current) return;
    cameraRef.current.stopRecording();
  }

  function toggleCameraFacing() {
    setFacing(current => current === 'front' ? 'back' : 'front');
    Haptics.selectionAsync();
  }

  // Request permissions if needed
  if (!cameraPermission?.granted || !micPermission?.granted) {
    return (
      <View>
        <Text>ビデオ練習にはカメラとマイクへのアクセスが必要です</Text>
        <Button onPress={() => {
          requestCameraPermission();
          requestMicPermission();
        }}>権限を許可</Button>
      </View>
    );
  }

  return (
    <View style={{ flex: 1 }}>
      <CameraView
        ref={cameraRef}
        style={{ flex: 1 }}
        facing={facing}
        mode="video"
        onCameraReady={() => console.log('Camera ready')}
      >
        <View style={styles.controls}>
          <TouchableOpacity onPress={toggleCameraFacing}>
            <FlipCameraIcon />
          </TouchableOpacity>
          <TouchableOpacity onPress={isRecording ? stopRecording : startRecording}>
            <View style={isRecording ? styles.stopButton : styles.recordButton} />
          </TouchableOpacity>
        </View>
      </CameraView>
      {isRecording && (
        <View style={styles.timer}>
          <Text>{recordingDuration}s / 60s</Text>
        </View>
      )}
    </View>
  );
}

これを簡単にしたもの:

  • useCameraPermissionsフックが権限状態管理を処理
  • カメラフリップはfacingプロパティを変更するだけ
  • maxDuration付きのrecordAsyncが60秒で自動停止
  • 同じカメラUIコードがiOSとAndroidで動作

代替案は、SwiftとKotlinで別々にネイティブカメラコードを書き、異なる権限システムを扱い、2つのコードベースを管理することでした。expo-cameraは、1週間の作業になったかもしれないものを1日に変えました。

3. expo-notifications:日々の練習リマインダー

ユーザーに毎日戻ってきてもらうことはWellspokenにとって重要でした。練習リマインダーのためのプッシュ通知が必要でしたが、APNs設定、FCMセットアップ、またはデバイストークンの手動管理を扱いたくありませんでした。

expo-notificationsがすべてを処理しました:

// contexts/AuthContext.tsx
import * as Notifications from 'expo-notifications';
import { Platform } from 'react-native';

// Configure notification behavior
Notifications.setNotificationHandler({
  handleNotification: async () => ({
    shouldShowAlert: true,
    shouldPlaySound: true,
    shouldSetBadge: false,
  }),
});

async function registerForPushNotifications() {
  // Android notification channel setup
  if (Platform.OS === 'android') {
    await Notifications.setNotificationChannelAsync('default', {
      name: 'default',
      importance: Notifications.AndroidImportance.MAX,
      vibrationPattern: [0, 250, 250, 250],
    });
  }

  // Request permissions
  const { status: existingStatus } = await Notifications.getPermissionsAsync();
  let finalStatus = existingStatus;
  if (existingStatus !== 'granted') {
    const { status } = await Notifications.requestPermissionsAsync();
    finalStatus = status;
  }

  if (finalStatus !== 'granted') {
    return null;
  }

  // Get the Expo push token
  const tokenData = await Notifications
The solo dev playbook: ship faster with Expo, EAS Build, and OTA Updates | Expo | DocsDigest