OpenAIExpoJan 13, 2026, 4:30 PM

How the Minecraft Speedrunning Community stays fast with Expo

A condensed section focused on the key takeaways first.

Original Post

Quick Digest

Summary

A condensed section focused on the key takeaways first.

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

How the Minecraft Speedrunning Community stays fast with Expo

Key Points

  • Realtime push pipeline using WebSockets and expo-server-sdk-node
  • App Integrity (App Attest & Play Integrity) protects API routes
  • Native-feel headers via expo-glass-effect and a useScreenOptions hook

Summary

PaceMan.gg is a community-driven, real-time Minecraft speedrun tracker built with the Expo SDK (mostly TypeScript). The app surfaces active runs, live split progress, and a leaderboard across iOS and Android. Its push pipeline uses an ActiveRunsService to emit WebSocket events to a PushNotificationsService that decides when to notify, retrieves push tokens from MySQL/Redis, and sends pushes via expo-server-sdk-node. Client-side, a NotificationsProvider (expo-notifications) handles registration and token lifecycle. API routes are protected with @expo/app-integrity using App Attest (iOS) and Play Integrity (Android) validated on the backend with node-app-attest and @googleapis/playintegrity. For native-feel UI the app uses expo-glass-effect with a useScreenOptions hook to adapt header blur across platforms. Expo cloud services enabled building and shipping without a Mac.

Key Points

  • Architecture: ActiveRunsService -> WebSocket -> PushNotificationsService -> fetch tokens -> expo-server-sdk-node -> push notifications.
  • Client: NotificationsProvider (expo-notifications) registers tokens, handles foreground/background events, and calls getIntegrityHeaders() before API requests.
  • Security: App Integrity workflow—client performs App Attest / Play Integrity against a challenge, sends integrity headers; backend middleware verifyIntegrity() validates using node-app-attest and @googleapis/playintegrity and returns 401 on failure.
  • Storage & caching: push tokens and preferences persisted in MySQL with Redis used for caching/state.
  • Implementation tips: use expo-secure-store to keep attestation key IDs, re-attest on failure, and centralize token re-registration behind the NotificationsProvider.
  • UI: use expo-glass-effect isLiquidGlassAvailable() in a useScreenOptions hook to apply platform-appropriate header blur and achieve a native scrolling feel.
  • Developer workflow: Expo cloud services allowed cross-platform development and CI/CD without macOS for early builds.

Practical takeaways for engineers

  • Realtime events -> decision microservice pattern simplifies notification logic and scaling.
  • Protect mobile-only API routes with App Integrity + server-side validation to reduce token abuse.
  • Keep push token lifecycle centralized in a provider component and persist securely (expo-secure-store).
  • Use expo-glass-effect (feature-detect) to progressively enhance native UI affordances without breaking fallbacks.

Full Translation

Translations

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

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の使用ガイドラインに従っています。