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を用いてルートにセキュリティレイヤーを追加しました。
流れは次の通りです。
- モバイルアプリはAPIリクエスト前にgetIntegrityHeaders()を実行し、App Attest(iOS)またはPlay Integrity(Android)でユニークなchallengeに対して検証を行い、その結果をリクエストと共に送る。
- バックエンドはミドルウェアverifyIntegrity()で受信データの整合性を検証し、成功ならCRUDを実行、失敗なら401 Unauthorizedを返す。
- 検証は 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()フックを実装しました。コードスニペットは次の通りです:
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 ( ) ;
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" ,
} ;
} ;
上記フックは次のように使用します:
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の使用ガイドラインに従っています。