Complete guide for building beautiful apps with Expo Router. Covers fundamentals, styling, components, navigation, animations, patterns, and native tabs.
日本語の概要は準備中です。原文の説明を表示しています。
Verify NowStack Mobile iOS/Expo changes in the Apple Simulator with `xcrun simctl`. Covers single or parallel Metro/device flows, deep-link navigation, preview routes, and OTP sign-in. Use for `ns ios verification`, "test on simulator", or changed `mobile-app/**`; never use computer-use.
インストールする前に、エージェントに与えられる指示の中身を確認できます。
8081. The default.NEVER drive the Simulator (or a browser) with the computer-use / control-the-computer tools. simctl for mobile, dev-browser for web. Computer-use is a last resort only when explicitly requested.
For a live browser-visible mirror, pair this workflow with .agents/skills/ns-preview/SKILL.md. The preview is a transport layer; this skill still owns navigation, app-state proof, and the verification verdict.
</objective>
<platform_guard>
iOS verification requires macOS (Xcode + Simulator). uname -s must be Darwin. A dev build must already exist (/ns ios local-setup → npm run ios once). Native modules (Stripe, IAP, Apple Sign In) only exist in the dev build, never Expo Go.
</platform_guard>
Run Metro/builds with Node 22 (eas.json pins 22.20.0) and a UTF-8 locale:
export PATH="$HOME/.nvm/versions/node/v22.20.0/bin:$PATH"
export LANG=en_US.UTF-8 LC_ALL=en_US.UTF-8
Node 18 fails reading app.config.ts (it requires the ESM site-config.ts); a non-UTF-8 locale fails CocoaPods pod install.
Resolve the slug, bundle id, and built .app once:
cd mobile-app
APP=$(find ~/Library/Developer/Xcode/DerivedData -path '*/Build/Products/Debug-iphonesimulator/*.app' -maxdepth 6 -type d 2>/dev/null | head -1)
BUNDLE_ID=$(/usr/libexec/PlistBuddy -c 'Print CFBundleIdentifier' "$APP/Info.plist")
SLUG=$(/usr/libexec/PlistBuddy -c 'Print CFBundleURLTypes:0:CFBundleURLSchemes:0' "$APP/Info.plist" 2>/dev/null)
echo "slug=$SLUG bundle=$BUNDLE_ID app=$APP"
SLUG is the deep-link scheme (the default product is nowstack; it's the scheme in site-config.ts/app.config.ts). Reading it from the built .app Info.plist is the reliable source — app.config.ts is TS and can't be required by plain Node.
Use this exact flow to render one mobile change — no gestures, no computer-use.
8081, so a sibling app's Metro on 8081 makes this app load the WRONG bundle. Free 8081, then start this app's Metro there (tell the user if you stopped another app's Metro):
cd mobile-app && npx expo start --port 8081 --dev-client
8081 and is foreground (same-app deep links then skip the cross-app "Open in …?" prompt):
xcrun simctl terminate booted "$BUNDLE_ID"; xcrun simctl launch booted "$BUNDLE_ID"
(app)/(tabs)/...):
xcrun simctl openurl booted "$SLUG://(app)/(tabs)/settings"
xcrun simctl io booted screenshot /tmp/shot.png
For a scrolled state without gestures, temporarily add contentOffset={{ x: 0, y: N }} to the ScrollView (Fast Refresh applies it), screenshot, then revert. Crop with sips/PIL to inspect header/detail.When several agents (or parallel tasks) drive the app at once, never share one Simulator on 8081 — every navigate/reload/screenshot from one agent disturbs the others. Give each agent its own autonomous pair: a dedicated Simulator device + a dedicated Metro port. The dev build (.app) is identical everywhere — install it on each device, then tell each one which Metro to load.
For each agent (example: agent A → port 8083):
UDID_A=$(xcrun simctl create "agent-a" "iPhone 16 Pro")
xcrun simctl boot "$UDID_A"
xcrun simctl install "$UDID_A" "$APP"
8083, 8084, …), in the background:
cd mobile-app && EXPO_NO_INTERACTIVE=1 npx expo start --port 8083 --dev-client &
expo-development-client deep link. The url must be URL-encoded (http://localhost:8083 → http%3A%2F%2Flocalhost%3A8083):
xcrun simctl launch "$UDID_A" "$BUNDLE_ID"
xcrun simctl openurl "$UDID_A" "$SLUG://expo-development-client/?url=http%3A%2F%2Flocalhost%3A8083"
<udid>, never booted (ambiguous once several devices are booted):
xcrun simctl openurl "$UDID_A" "$SLUG://(app)/(tabs)/settings"
xcrun simctl io "$UDID_A" screenshot /tmp/agent-a.png
Agent B repeats with $UDID_B, --port 8084, and ?url=...8084. Each agent's navigation/reloads/screenshots stay independent.
Teardown throwaway devices when done:
xcrun simctl shutdown "$UDID_A"; xcrun simctl delete "$UDID_A"
Pitfalls. Always address the explicit
<udid>(neverbooted). The?url=value must be URL-encoded. Multipleexpo startfrom the same project dir share.expo/and the file-watcher — it works on different ports, but if you see cache races add--clearto the first or give each a distinctTMPDIR. The Simulator shares the host network, solocalhost:<port>reaches Metro directly (no Android10.0.2.2).
simctl cannot type into a TextInput, so you do NOT walk the sign-in form by hand. Pick one:
Render the real screen OUTSIDE the (app) auth gate but INSIDE the root providers. Create mobile-app/app/<name>-preview.tsx, deep-link to it, screenshot, then trash the file when done. Force a theme with useTheme().setPreference('dark') if the simulator appearance doesn't reach a system preference.
Auth is Better Auth email OTP (convex/auth.ts). The OTP is NOT magic:
appstoretest@email.com → code is always 123456. generateOTP hardcodes it and sendVerificationOTP returns early — no Resend / no email needed. This is THE test login.RESEND_API_KEY on the deployment, the OTP is logged in the Convex dashboard logs ([AUTH] … OTP for <email>: <otp>) but the send call also throws — so prefer the test account. With RESEND_API_KEY set, a real email is sent and the code is NOT in the logs.To sign in WITHOUT typing, create a TEMP preview route that hits the same two endpoints the sign-in form uses (mobile-app/app/onboarding/sign-in-form.tsx), then trash it after:
// mobile-app/app/login-test-preview.tsx — TEMP, trash after verifying
import { useEffect, useState } from 'react';
import { View } from 'react-native';
import { authClient } from '@/lib/auth-client';
import { Text } from '@/components/ui/text';
const EMAIL = 'appstoretest@email.com';
const OTP = '123456';
export default function LoginTestPreview() {
const [status, setStatus] = useState('signing in…');
useEffect(() => {
(async () => {
await authClient.$fetch('/email-otp/send-verification-otp', {
method: 'POST',
body: { email: EMAIL, type: 'sign-in' },
});
const res = await authClient.$fetch('/sign-in/email-otp', {
method: 'POST',
body: { email: EMAIL, otp: OTP },
});
setStatus(res.error ? `error: ${res.error.message}` : 'signed in ✓ — deep-link to (app) now');
})();
}, []);
return (
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
<Text>{status}</Text>
</View>
);
}
Deep-link to it ($SLUG://login-test-preview), wait for "signed in ✓", then navigate to the real (app) screens — the session token now lives in SecureStore and auth-store.tsx picks it up. To read codes for an arbitrary email when Resend is unset: npx convex logs (root) and grep for [AUTH].
cd mobile-app && npx tsc --noEmit for TypeScript-sensitive changes.cd mobile-app && npm run lint when touching shared UI or many files.npx convex dev --once (root) for Convex changes.trash every temp *-preview.tsx route you created.まだレビューはありません。使ってみた感想をお寄せください。
概要と使いどころ
Complete guide for building beautiful apps with Expo Router. Covers fundamentals, styling, components, navigation, animations, patterns, and native tabs.
日本語の概要は準備中です。原文の説明を表示しています。
Routes general Convex requests to the right project skill. Use when the user asks which Convex skill to use or gives an underspecified Convex app task.
日本語の概要は準備中です。原文の説明を表示しています。
Use when optimizing Convex database read bandwidth, documents-read cost, query indexes, full-table scans, function-level usage dashboard findings, or code that uses .filter(), unbounded .collect(), N+1 reads, or expensive reactive Convex queries.
日本語の概要は準備中です。原文の説明を表示しています。
Build reusable Convex components with isolated tables and app-facing wrappers. Use for defineComponent, app.use, ComponentApi, or shared backend modules.
日本語の概要は準備中です。原文の説明を表示しています。
Plan safe Convex schema and data migrations. Use for widen-migrate-narrow rollouts, backfills, breaking field changes, table splits, or failed schema deploys.
日本語の概要は準備中です。原文の説明を表示しています。
Audit Convex performance for hot reads, write contention, subscriptions, function limits, OCC conflicts, latency, read amplification, and expensive reactive queries.
日本語の概要は準備中です。原文の説明を表示しています。