OpenAIExpo2026/01/13 16:30

How the Minecraft Speedrunning Community stays fast with Expo

要点だけを先に読めるように短く再構成したセクションです。

元記事

Quick Digest

要約

要点だけを先に読めるように短く再構成したセクションです。

openaijamodel: gpt-5-mini-2025-08-07

Minecraft Speedrunning コミュニティが Expo で高速を維持する方法

Key Points

  • Expoでリアルタイム通知
  • App IntegrityでAPI保護
  • ネイティブ感をexpo-glass-effectで実現

Summary

PaceMan.gg は Minecraft のスピードランをリアルタイムで追跡するモバイルアプリで、Expo SDK を中心に構築されています。リアルタイム更新は WebSocket→ActiveRunsService→PushNotificationsService のパイプラインで処理され、expo-server-sdk-node を使ってプッシュ通知を配信します。トークンは MySQL/Redis に保存され、クライアント側は expo-notifications の NotificationsProvider でトークン登録と通知ハンドリングを行います。API の保護には @expo/app-integrity(App Attest / Play Integrity)を導入し、expo-secure-store でキーを安全に保持します。UI では expo-glass-effect を使ってプラットフォームごとにネイティブ寄りのヘッダー表現を実現しています。

Key Points

  • アーキテクチャ
    • クライアント: Expo + TypeScript、NotificationsProvider によるトークン登録とフォアグラウンド/バックグラウンド処理。
    • バックエンド: ActiveRunsService (WebSocket 発行) → PushNotificationsService (通知判定・送信)、MySQL/Redis でトークン管理。
  • プッシュ実装の実務
    • イベント到着時にペース判定を行い、通知が必要なら push トークンを取得して expo-server-sdk-node 経由で送信。
    • トークンの再登録・更新は NotificationsProvider 側で管理。
  • セキュリティ(App Integrity)
    • クライアントで getIntegrityHeaders() を実行し、App Attest / Play Integrity のチャレンジ応答をヘッダに付与。
    • サーバ側で node-app-attest / @googleapis/playintegrity を用いてミドルウェア verifyIntegrity() により検証し、失敗時は 401 を返す。
    • expo-secure-store を使って app-attest の keyId を安全に保存し、必要時に再アテストを実施する。
  • ネイティブ感の演出
    • expo-glass-effect の isLiquidGlassAvailable() を参照し、useScreenOptions フックでプラットフォーム毎の header 設定(透明/ブラー/背景色)を統一。
  • 実運用上の利点
    • Expo のクラウドサービスにより Mac が無くても iOS/Android のビルドや配布が可能。
    • マイクロサービス化で通知判定ロジックを独立させ、スケーリングとデバッグを容易にする。

実装の参考: NotificationsProvider でのトークン登録、PushNotificationsService によるイベント判定→送信、getIntegrityHeaders()/verifyIntegrity() による API 保護を優先的に設計してください。

Full Translation

翻訳

原文の流れを保ったまま読める翻訳セクションです。

openaijamodel: gpt-5-mini-2025-08-07

Minecraft SpeedrunningコミュニティがExpoで高速を維持する方法

How the Minecraft Speedrunning Community stays fast with Expo

Users • React Native • Development • January 13, 2026 • 8 minutes read

Chitraksh Tarun — Guest Author

本記事では、Minecraft SpeedrunningコミュニティがどのようにExpoを使ってモバイルアプリとプッシュ通知インフラを構築し、スピードランの発生をリアルタイムでユーザーに通知しているかを紹介します。この記事はChitraksh Tarun氏(現在SWE Intern @ ClubzFM)によるゲスト投稿です。彼は2021年4月からMinecraftをスピードランしており、PaceMan.ggモバイルアプリのメンテナーでもあります。

概要

Minecraft Speedrunningコミュニティは非常にスピーディです。世界中でいつでもスピードランが行われており、コミュニティは記録が出そうなランをリアルタイムで追いかけたいと考えています。PaceMan.ggは、アクティブなMinecraftスピードランを追跡するコミュニティ主導のツールで、進行状況をリアルタイムリーダーボードで表示します。

PaceMan.ggのモバイルアプリはExpo SDKで作られており、モバイルインターフェースを通してリアルタイムのスピードラン更新を配信します。最近、このアプリにプッシュ通知サポートを追加しました。簡単に言うと、“有望なペースになったスピードランがあればユーザーに通知され、アプリで進行を追跡したり、Twitchで配信されていれば視聴したりできる”という仕組みです。

以下では、リアルタイムで高速に動作するアプリを実現するために行ったアーキテクチャ設計と開発上の判断を分解して説明します。特に、expo-notifications、@expo/app-integrity、expo-glass-effectを通知インフラ、APIセキュリティ、ネイティブ風インターフェース設計の一部にどのように使っているかに焦点を当てます。

PaceMan.ggモバイルアプリ

基本的にこのアプリはシンプルなリアルタイムMinecraftスピードランボードです。起動直後のHomeタブには現在走っているスピードランナー全員と、各ランがどのステージ(split)にいるかが表示されます。

  • Leaderboardタブ:指定した期間(daily, weekly, monthly, all time)内の最速ランを表示
  • Statsタブ:各splitごとのスピードランナー統計の詳細表示

短時間で状況を把握できるシンプルなUIを目指しています。アプリは完全にExpoで開発されており(アプリv1.2.0時点で99.5% TypeScript、0.5% Other)、ExpoのServicesを使うことでiOSとAndroid間で容易に機能の均等化ができています。実際、初期の数バージョンはMacなしで開発されました(Windows上のWSL、iPhone、古いAndroidを使用)。Expoのクラウドサービスにより、Macがなくてもアプリをビルド・配信できました。

Expo Notificationsを使ったプッシュ通知

アプリの中核機能の一つは、スピードランが“良いペース”に入ったときに通知を受け取ることです。これにより、リアルタイムでランを把握し、配信されていれば視聴に切り替えられます。

アーキテクチャ概要:

  • express.jsで実装されたPushNotificationsService(通知管理)とActiveRunsService(現在アクティブなスピードラン管理)の2つのマイクロサービス/バックエンドを用意
  • 各スピードランイベントはActiveRunsServiceからWebSocketイベントとしてPushNotificationsServiceへ送信され、そこで通知すべきか(“良いペースか”)を判定
  • 通知対象と判断した場合、プッシュトークンを取得して通知を送信
  • サーバー側はexpo-server-sdk-nodeと連携し、トークン等の保存にはMySQL DBとRedisを使用

クライアント側では、Betoのチュートリアルに触発された<NotificationsProvider />を実装しており、expo-notificationsを使ってトークン登録からアプリのフォアグラウンド/バックグラウンドでの通知イベント処理までを扱っています。

App Integrityによるセキュリティ

PushNotificationsServiceのバックエンドには、モバイルアプリがトークンや設定をCRUDするためのAPIルートがあります。@expo/app-integrityパッケージの導入と、これらのCRUDがモバイルアプリからのみ使われることを前提としている点を踏まえ、App Integrityを用いてルートにセキュリティレイヤーを追加しました。

流れは次の通りです。

  1. モバイルアプリはAPIリクエスト前にgetIntegrityHeaders()を実行し、App Attest(iOS)またはPlay Integrity(Android)でユニークなchallengeに対して検証を行い、その結果をリクエストと共に送る。
  2. バックエンドはミドルウェアverifyIntegrity()で受信データの整合性を検証し、成功ならCRUDを実行、失敗なら401 Unauthorizedを返す。
  3. 検証は node-app-attest(iOS)と @googleapis/playintegrity(Android)ライブラリをバックエンドで使用して行う。

以下はgetIntegrityHeaders()関数のトリミング・整形済みスニペットです(元の実装を保持しています):

export const getIntegrityHeaders = async ( ) => {
  if ( Platform.OS !== "android" && Platform.OS !== "ios" ) return ;
  // Get unique challenge
  const challenge = await getChallenge ( ) ;
  // Handle Android
  if ( Platform.OS === "android" ) {
    await waitForIntegrityProviderReady ( ) ;
    const integrityToken = await requestIntegrityCheckAsync ( challenge ) ;
    return integrityToken ;
  }
  // Handle iOS
  if ( Platform.OS === "ios" ) {
    let keyId = await getItemAsync ( "app-attest-key" ) ;
    // Attest first time, if no attestation available
    if ( ! keyId || typeof keyId !== "string" || keyId.trim().length === 0 ) {
      keyId = await generateKeyAsync ( ) ;
      await setItemAsync ( "app-attest-key" , keyId ) ;
      const attestation = await attestKeyAsync ( keyId , challenge ) ;
      return attestation ;
    }
    // Assert if attestation exists
    try {
      const request = { expoToken , challenge , } ;
      const assertion = await generateAssertionAsync ( keyId , JSON.stringify ( request ) ) ;
      const rawAuthentication = JSON.stringify ( { keyId , assertion , } ) ;
      const authentication = Buffer.from ( rawAuthentication ).toString ( "base64" ) ;
      return authentication ;
    } catch {
      // Re-attest, if current attestation key fails for whatever reason.
      const newKeyId = await generateKeyAsync ( ) ;
      await setItemAsync ( "app-attest-key" , newKeyId ) ;
      const attestation = await attestKeyAsync ( newKeyId , challenge ) ;
      return attestation ;
    }
  }
  return ;
} ;

このように、ユニークなchallengeに対するApp Integrity検証と、expo-secure-storeを使ってKeychainにkey IDを安全に保存する組み合わせにより、バックエンドは正規のアプリインストールからのアクセスに限定され、セキュアでクリーンな構成を維持できます。

Notifications Providerは再登録やトークン更新ロジックも管理しており、これらもApp Integrityで保護されるため、ユーザーが通知を見逃さないようにしています。

expo-glass-effectを使ったネイティブ風ヘッダー

UIを可能な限りネイティブっぽく見せるために、ヘッダーに適切なブラー/スタイリング効果を適用しました。これによりスクロール時の挙動が各プラットフォームの期待値に近づきます。

実装のポイント:

  • Android:ソリッドなヘッダー
  • iOS(iOS 26未満):半透明のブラー
  • iOS(iOS 26以降):透明なブラー

Anurabh Vermaの実装に触発され、expo-glass-effectのisLiquidGlassAvailable()ハンドラを使って、ヘッダーの見た目をページ間で一貫させるuseScreenOptions()フックを実装しました。コードスニペットは次の通りです:

// @/hooks/use-screen-options.ts
import { useColorsForUI } from "@/hooks/use-colors-for-ui" ;
import type { NativeStackNavigationOptions } from "@react-navigation/native-stack" ;
import { isLiquidGlassAvailable } from "expo-glass-effect" ;
import { useColorScheme } from "nativewind" ;
import { Platform } from "react-native" ;
export const useScreenOptions = ( ) : NativeStackNavigationOptions => {
  const { colorScheme } = useColorScheme ( ) ;
  const { backgroundColor } = useColorsForUI ( ) ; // Custom hook to retrieve certain hex codes
  return {
    headerShadowVisible : false ,
    headerTransparent : Platform.select ( { ios : true , android : false , } ) ,
    headerStyle : { backgroundColor : Platform.select ( { android : backgroundColor , } ) , } ,
    headerBlurEffect : ! isLiquidGlassAvailable ( ) ? colorScheme === "light" ? "systemChromeMaterialLight" : "systemChromeMaterialDark" : "none" ,
    headerBackButtonDisplayMode : "minimal" ,
  } ;
} ;

上記フックは次のように使用します:

// @/app/_layout.tsx
import { useScreenOptions } from "@/hooks/use-screen-options" ;
export default function RootLayout ( ) {
  // ...
  const screenOptions = useScreenOptions ( ) ;
  // ...
  return (
    <Stack screenOptions={screenOptions}>
      { /* Remaining Stack Elements */ }
    </Stack>
  )
}

このアプローチにより、ヘッダーのスクロール体験がネイティブらしい感触になります。今後はパッケージの<GlassView />コンポーネントへの投資を進め、Liquid Glass UI要素を段階的に追加していく予定です。UIを過度に派手にせず、落ち着いた流動性を保つことを重視しています。

PaceMan.ggの今後の計画

アプリにはWidgetsのような追加機能や、多くの改善・洗練の余地があります。今後もExpoやコミュニティが提供する技術を深掘りして、Minecraft Speedrunningコミュニティにとって素晴らしいモバイル体験を維持していきたいと考えています。

P.S. 私(Chitraksh)がExpoでモバイルアプリを作る際、このプロジェクトが可能になったのはMinecraft Speedrunningコミュニティの開発者たちによる裏側のツール、ユーティリティ、インターフェースの素晴らしい貢献のおかげです。特に以下の方々に大きな感謝を述べます:Specnr, Boyenn, Saanvi, Duncan, Jojoe, RedLime, Cylo およびその他多くの開発者たち。

注意: PaceMan.ggはリアルタイムのスピードラン進捗トラッカーとしてコミュニティ主導で運営されるアプリケーションです。本アプリケーションはMinecraft、Mojang、Microsoftのいずれとも提携も認可もされていません。Minecraftの使用ガイドラインに従っています。