Misc
This guide will help you understand codebase & configuration for mobile and some common errors.
Basic Foundation
Core Technologies:
- Expo SDK 54 (
~54.0.27) with React Native 0.81.5 and React 19.1.0. - New Architecture enabled via
newArchEnabled: trueinapp.config.ts. - TypeScript for end-to-end type safety.
- NativeWind 4.2.1 with TailwindCSS 4.0.6 for utility-first styling.
- Jotai for atomic state management and selective re-rendering.
- React Navigation 7.x (
native-stack+bottom-tabs) for app navigation. - React Query (TanStack Query) via
jotai-tanstack-queryintegration. - oRPC contract-first API layer with full type safety.
- Firebase (
@react-native-firebase/*v22.4.0) for Analytics, Crashlytics, Messaging, and Remote Config. - MMKV (
react-native-mmkv ~3.3.3) for encrypted key-value storage. - Vision Camera (
react-native-vision-camera ^4.7.3) for card scanning and ML-powered capture flows. - Flash List (
@shopify/flash-list 2.0.2) for high-performance lists. - Reanimated (
react-native-reanimated ~4.2.0) for performant animations.
Project Structure:
- app/: Main app screens/pages.
- app/{feature}/_atoms/: Feature-specific atoms (
queries,mutations,ui,effects,forms, plus feature-specific extras liketypes/mutations-optimistic). - components/: Reusable UI components.
- db/: SQLite database layer (Drizzle ORM, schema, queries, loaders).
- lib/: Core utilities and configurations.
- lib/atoms/: Global app-wide atoms (profile, onboarding, force-update, notifications).
- lib/store/shared/: Cross-feature shared atoms (game, user, price, misc).
- lib/store/utils/: Atom utilities (
atomWithSwr,atomWithDebouncedMutation, debounced field helpers, atom compare/debounce helpers). - lib/network/: Network status monitoring and offline handling.
- lib/storage.tsx: MMKV storage wrapper and JSON helpers.
- lib/api/: oRPC client setup and configuration.
- lib/i18n/locales/: i18n language files.
- lib/react-query/: Query utilities and React Query provider setup.
- navigators/: Navigation setup and routing.
- types/: TypeScript type definitions.
- utils/: Helper functions and utilities.
The project uses React Navigation rather than expo-router for mobile-specific navigation control.
Setup CI/CD
The CI/CD setup for this project leverages GitHub Actions to automate the build and deployment process. Here’s a brief overview of the configuration:
GitHub Actions Workflow
The project uses a composite GitHub Action defined in .github/actions/eas-build/action.yml to handle the build process for different environments (development, staging, production).
Key Inputs:
APP_ENV: Specifies the environment for the build (development, staging, production).AUTO_SUBMIT: Determines if the build should be automatically submitted to app stores.EXPO_TOKEN: Required token for accessing the Expo account.VERSION: The version of the app to be built.ANDROIDandIOS: Flags to trigger builds for Android and iOS platforms respectively.
Build Steps:
- Pre-Build Script: Generates necessary native folders based on the
APP_ENV. - Platform-Specific Builds: Executes builds for Android and iOS based on the provided flags.
Scripts in package.json
The package.json includes several scripts to facilitate the build and deployment process:
- Build Commands:
build:staging:iosandbuild:staging:android: Build the app for staging environment.build:prod:iosandbuild:prod:android: Build the app for production environment.
- Update Commands:
app:update:staging: Updates the app on the staging channel.app:update:prod: Updates the app on the production channel.
These scripts are integrated into the CI/CD pipeline to ensure seamless deployment and updates across different environments.
Styling with NativeWind
The project uses NativeWind for utility-class styling in React Native.
Configuration
- Tailwind Configuration
// @ts-nocheck
const colors = require("./src/components/ui/colors")
/** @type {import('tailwindcss').Config} */
module.exports = {
// NOTE: Update this to include the paths to all of your component files.
content: ["./src/**/*.{js,jsx,ts,tsx}"],
presets: [require("nativewind/preset")],
darkMode: "false",
theme: {
extend: {
fontFamily: {
mulish: ["Mulish-Regular"],
mulishBold: ["Mulish-Bold"],
mulishExtraBold: ["Mulish-ExtraBold"],
mulishMedium: ["Mulish-Medium"],
mulishSemiBold: ["Mulish-SemiBold"],
},
colors,
spacing: {
0: "0px",
0.5: "2px",
1: "4px",
1.5: "6px",
2: "8px",
2.5: "10px",
3: "12px",
3.5: "14px",
3.75: "15px",
4: "16px",
5: "20px",
6: "24px",
7: "28px",
8: "32px",
9: "36px",
10: "40px",
11: "44px",
12: "48px",
14: "56px",
16: "64px",
20: "80px",
24: "96px",
28: "112px",
32: "128px",
36: "144px",
40: "160px",
44: "176px",
48: "192px",
52: "208px",
56: "224px",
60: "240px",
64: "256px",
68: "272px",
72: "288px",
80: "320px",
96: "384px",
},
},
},
plugins: [],
}- TypeScript Support
The nativewind-env.d.ts file provides NativeWind TypeScript support.
/// <reference types="nativewind/types" />Important Note to Ensure Monorepo Compatibility
Metro Configuration
Use the real monorepo-aware Metro config from apps/mobile/metro.config.js:
const { getDefaultConfig } = require("expo/metro-config")
const { withNativeWind } = require("nativewind/metro")
const path = require("node:path")
// Find the project and workspace directories
const projectRoot = __dirname
// This can be replaced with `find-yarn-workspace-root`
const monorepoRoot = path.resolve(projectRoot, "../..")
const config = getDefaultConfig(projectRoot)
// 1. Watch all files within the monorepo
config.watchFolders = [monorepoRoot]
// 2. Let Metro know where to resolve packages and in what order
config.resolver.nodeModulesPaths = [
path.resolve(projectRoot, "node_modules"),
path.resolve(monorepoRoot, "node_modules"),
]
// 3. Enable symlink support for pnpm
config.resolver.unstable_enableSymlinks = true
config.resolver.unstable_enablePackageExports = true
// 3b. Block web-only packages hoisted at monorepo root
config.resolver.blockList = [
/.*\/node_modules\/@storybook\/.*/,
/.*\/node_modules\/storybook\/.*/,
/.*\/node_modules\/@chromatic-com\/.*/,
/.*\/apps\/storybook\/.*/,
/.*\/apps\/admin\/node_modules\/.*/,
]
// 4. Add extra node modules for workspace package resolution
config.resolver.extraNodeModules = {
"react-native-card-scanner": path.resolve(
monorepoRoot,
"packages/mobile/react-native-card-scanner",
),
}
config.resolver.sourceExts.push("sql")
config.resolver.sourceExts.push("cjs")
// Add transformer config to handle import.meta polyfill
config.transformer = {
...config.transformer,
unstable_transformImportMeta: true,
}
// Add .pte (PyTorch ExecuTorch) and .mdb (ObjectBox database) as asset extensions
config.resolver.assetExts.push("pte")
config.resolver.assetExts.push("mdb")
module.exports = withNativeWind(config, { input: "./global.css" })Local Database (SQLite + Drizzle)
Mobile uses a local SQLite database (cn.db) via expo-sqlite, with Drizzle ORM (drizzle-orm/expo-sqlite) for type-safe queries.
- WAL mode and tuned pragmas are applied at startup for better write/read performance.
- Writes are serialized via
runDbWrite()to avoid overlapping SQLite writes. - Full-text search is supported with FTS5 virtual tables in the database schema/migrations.
- Migrations are run at app startup via
useMigrations(SQLiteDB, migrations)inAppNavigator.
export const dbName = "cn.db"
export const db = SQLite.openDatabaseSync(dbName, {
enableChangeListener: true,
})
// Enable WAL mode for better concurrency (critical for multi-game updates)
db.execSync("PRAGMA journal_mode = WAL")
// Set synchronous mode to NORMAL for faster writes while maintaining data integrity
db.execSync("PRAGMA synchronous = NORMAL")
// Set cache size to 64MB for better query performance
db.execSync("PRAGMA cache_size = -64000")
// Store temporary tables and indices in memory for better performance
db.execSync("PRAGMA temp_store = MEMORY")
// Enable memory-mapped I/O for better read performance (256MB)
db.execSync("PRAGMA mmap_size = 268435456")
// Optimize page size for mobile devices (4KB is optimal for most mobile storage)
db.execSync("PRAGMA page_size = 4096")
// Enable foreign keys
db.execSync("PRAGMA foreign_keys = ON")
// Wait briefly for locked databases instead of failing immediately.
db.execSync("PRAGMA busy_timeout = 5000")
// Serialize write transactions to avoid overlapping SQLite writes.
let writeQueue: Promise<void> = Promise.resolve()
export async function runDbWrite<T>(task: () => Promise<T>): Promise<T> {
let release: (() => void) | undefined
const next = new Promise<void>((resolve) => {
release = resolve
})
const previous = writeQueue
writeQueue = previous.then(() => next)
await previous
try {
return await task()
} finally {
release?.()
}
}MMKV Storage
The app uses an encrypted MMKV instance in src/lib/storage.tsx:
- MMKV instance is encrypted with key
"cn". - Helpers:
getItem,setItem,readJsonSync,writeJsonSync,removeItem,deleteKey. - Jotai persistence uses
atomWithMMKVStoragefromsrc/lib/jotaiStorage.ts.
Common persisted data includes auth-related values, user preferences, scanned-card history, and CDN data versions.
export const storage = new MMKV({
id: "storage",
encryptionKey: "cn",
})
export const atomWithMMKVStorage = <T>(key: string, initialValue: T) => {
return atomWithStorage<T>(key, initialValue, jotaiStorage as SyncStorage<T>)
}Network & Offline Support
Offline behavior is centered in src/lib/network/:
isDefinitelyOfflineAtomprovides conservative offline detection.networkRecoveryVersionAtomincrements on offline -> online transitions.- Query atoms depend on
networkRecoveryVersionAtomand gate on online status. - Active queries are refetched on reconnect, and paused mutations are resumed.
OfflineBanner(src/components/offline-banner/offline-banner.tsx) displays connectivity status in-app.