diff --git a/__fixtures__/test-project/.gitignore b/__fixtures__/test-project/.gitignore
index 31d9637ed..a43a18726 100644
--- a/__fixtures__/test-project/.gitignore
+++ b/__fixtures__/test-project/.gitignore
@@ -22,3 +22,18 @@ api/src/lib/generateGraphiQLHeader.*
 !.yarn/releases
 !.yarn/sdks
 !.yarn/versions
+
+# Expo
+.expo
+web-build
+expo-env.d.ts
+.kotlin/
+*.orig.*
+*.jks
+*.p8
+*.p12
+*.key
+*.mobileprovision
+.metro-health-check*
+*.pem
+*.tsbuildinfo
diff --git a/__fixtures__/test-project/app/.env.example b/__fixtures__/test-project/app/.env.example
new file mode 100644
index 000000000..4f700f16d
--- /dev/null
+++ b/__fixtures__/test-project/app/.env.example
@@ -0,0 +1,2 @@
+# EXPO_PUBLIC_RWJS_API_GRAPHQL_URL=http://localhost:8911/graphql
+# EXPO_PUBLIC_API_URL=http://localhost:8911
diff --git a/__fixtures__/test-project/app/ambient.d.ts b/__fixtures__/test-project/app/ambient.d.ts
new file mode 100644
index 000000000..6e4456a16
--- /dev/null
+++ b/__fixtures__/test-project/app/ambient.d.ts
@@ -0,0 +1,33 @@
+/* eslint-disable no-var */
+
+declare global {
+  /**
+   * FQDN or absolute path to the GraphQL serverless function, without the trailing slash.
+   * Example: `./redwood/functions/graphql` or `https://api.redwoodjs.com/graphql`
+   */
+  var RWJS_API_GRAPHQL_URL: string
+
+  /**
+   * URL or absolute path to serverless functions, without the trailing slash.
+   * Example: `./redwood/functions/` or `https://api.redwoodjs.com/`
+   **/
+  var RWJS_API_URL: string
+
+  // Provided by Vite.config in the user's project
+  var RWJS_ENV: {
+    RWJS_API_GRAPHQL_URL: string
+    /** URL or absolute path to serverless functions */
+    RWJS_API_URL: string
+  }
+
+  namespace NodeJS {
+    interface Global {
+      /** URL or absolute path to the GraphQL serverless function */
+      RWJS_API_GRAPHQL_URL: string
+      /** URL or absolute path to serverless functions */
+      RWJS_API_URL: string
+    }
+  }
+}
+
+export {}
diff --git a/__fixtures__/test-project/app/app.json b/__fixtures__/test-project/app/app.json
new file mode 100644
index 000000000..428a0b339
--- /dev/null
+++ b/__fixtures__/test-project/app/app.json
@@ -0,0 +1,42 @@
+{
+  "expo": {
+    "name": "app",
+    "slug": "app",
+    "version": "1.0.0",
+    "orientation": "portrait",
+    "icon": "./assets/images/icon.png",
+    "scheme": "app",
+    "userInterfaceStyle": "automatic",
+    "newArchEnabled": true,
+    "ios": {
+      "supportsTablet": true
+    },
+    "android": {
+      "adaptiveIcon": {
+        "foregroundImage": "./assets/images/adaptive-icon.png",
+        "backgroundColor": "#ffffff"
+      },
+      "edgeToEdgeEnabled": true
+    },
+    "web": {
+      "bundler": "metro",
+      "output": "static",
+      "favicon": "./assets/images/favicon.png"
+    },
+    "plugins": [
+      "expo-router",
+      [
+        "expo-splash-screen",
+        {
+          "image": "./assets/images/splash-icon.png",
+          "imageWidth": 200,
+          "resizeMode": "contain",
+          "backgroundColor": "#ffffff"
+        }
+      ]
+    ],
+    "experiments": {
+      "typedRoutes": true
+    }
+  }
+}
diff --git a/__fixtures__/test-project/app/app/(tabs)/_layout.tsx b/__fixtures__/test-project/app/app/(tabs)/_layout.tsx
new file mode 100644
index 000000000..dde407a02
--- /dev/null
+++ b/__fixtures__/test-project/app/app/(tabs)/_layout.tsx
@@ -0,0 +1,65 @@
+import React from 'react'
+
+import MaterialIcons from '@expo/vector-icons/MaterialIcons'
+import { Link, Tabs } from 'expo-router'
+import { Button, Platform, View } from 'react-native'
+
+import { HapticTab } from '@/components/HapticTab'
+import TabBarBackground from '@/components/ui/TabBarBackground'
+import { Colors } from '@/constants/Colors'
+import { useAuth } from '@/context/auth'
+import { useColorScheme } from '@/hooks/useColorScheme'
+
+export default function TabLayout() {
+  const colorScheme = useColorScheme()
+  const { isAuthenticated, logOut } = useAuth()
+
+  return (
+     (
+          
+            {isAuthenticated ? (
+              
+            ) : (
+              Login
+            )}
+          
+        ),
+      }}
+    >
+       (
+            
+          ),
+        }}
+      />
+       (
+            
+          ),
+        }}
+      />
+    
+  )
+}
diff --git a/__fixtures__/test-project/app/app/(tabs)/admin.tsx b/__fixtures__/test-project/app/app/(tabs)/admin.tsx
new file mode 100644
index 000000000..adac650cf
--- /dev/null
+++ b/__fixtures__/test-project/app/app/(tabs)/admin.tsx
@@ -0,0 +1,35 @@
+import React from 'react'
+
+import { gql, useQuery } from '@apollo/client'
+
+import { Post, PostProps } from '@/components/Post'
+import ThemedScrollView from '@/components/ThemedScrollView'
+
+export const FIND_POSTS_QUERY = gql`
+  query FindPosts {
+    posts {
+      id
+      title
+      body
+      author {
+        email
+        fullName
+      }
+      createdAt
+    }
+  }
+`
+
+export default function AdminScreen() {
+  const { data } = useQuery<{ posts: PostProps['post'][] }>(FIND_POSTS_QUERY)
+
+  const posts = data?.posts ?? []
+
+  return (
+    
+      {posts.map((post) => (
+        
+      ))}
+    
+  )
+}
diff --git a/__fixtures__/test-project/app/app/(tabs)/index.tsx b/__fixtures__/test-project/app/app/(tabs)/index.tsx
new file mode 100644
index 000000000..94f15ec84
--- /dev/null
+++ b/__fixtures__/test-project/app/app/(tabs)/index.tsx
@@ -0,0 +1,42 @@
+import React from 'react'
+
+import { gql, useQuery } from '@apollo/client'
+
+import { BlogPost, BlogPostProps } from '@/components/BlogPost'
+import ThemedScrollView from '@/components/ThemedScrollView'
+import { ThemedView } from '@/components/ThemedView'
+
+const QUERY = gql`
+  query BlogPostsQuery {
+    blogPosts: posts {
+      id
+      title
+      body
+      author {
+        email
+        fullName
+      }
+      createdAt
+    }
+  }
+`
+
+export default function HomeScreen() {
+  const { data } = useQuery(QUERY)
+
+  const blogPosts: BlogPostProps['blogPost'][] = data?.blogPosts ?? []
+
+  return (
+    
+      
+        {blogPosts.map((blogPost, index) => (
+          
+        ))}
+      
+    
+  )
+}
diff --git a/__fixtures__/test-project/app/app/+not-found.tsx b/__fixtures__/test-project/app/app/+not-found.tsx
new file mode 100644
index 000000000..062898069
--- /dev/null
+++ b/__fixtures__/test-project/app/app/+not-found.tsx
@@ -0,0 +1,34 @@
+import React from 'react'
+
+import { Link, Stack } from 'expo-router'
+import { StyleSheet } from 'react-native'
+
+import { ThemedText } from '@/components/ThemedText'
+import { ThemedView } from '@/components/ThemedView'
+
+export default function NotFoundScreen() {
+  return (
+    <>
+      
+      
+        This screen does not exist.
+        
+          Go to home screen!
+        
+      
+    >
+  )
+}
+
+const styles = StyleSheet.create({
+  container: {
+    flex: 1,
+    alignItems: 'center',
+    justifyContent: 'center',
+    padding: 20,
+  },
+  link: {
+    marginTop: 15,
+    paddingVertical: 15,
+  },
+})
diff --git a/__fixtures__/test-project/app/app/_layout.tsx b/__fixtures__/test-project/app/app/_layout.tsx
new file mode 100644
index 000000000..96ae354cc
--- /dev/null
+++ b/__fixtures__/test-project/app/app/_layout.tsx
@@ -0,0 +1,50 @@
+import React from 'react'
+
+import {
+  DarkTheme,
+  DefaultTheme,
+  ThemeProvider,
+} from '@react-navigation/native'
+import { useFonts } from 'expo-font'
+import { Stack } from 'expo-router'
+import { StatusBar } from 'expo-status-bar'
+
+import { RedwoodApolloProvider } from '@cedarjs/web/apollo'
+
+import { AuthProvider, useAuth } from '@/context/auth'
+import { useColorScheme } from '@/hooks/useColorScheme'
+
+import 'react-native-reanimated'
+
+export default function RootLayout() {
+  const colorScheme = useColorScheme()
+  const [loaded] = useFonts({
+    SpaceMono: require('../assets/fonts/SpaceMono-Regular.ttf'),
+  })
+
+  if (!loaded) {
+    // Async font loading only occurs in development.
+    return null
+  }
+
+  return (
+    
+      
+        
+          
+            
+            
+            
+          
+          
+        
+      
+    
+  )
+}
diff --git a/__fixtures__/test-project/app/app/modal.tsx b/__fixtures__/test-project/app/app/modal.tsx
new file mode 100644
index 000000000..e56835426
--- /dev/null
+++ b/__fixtures__/test-project/app/app/modal.tsx
@@ -0,0 +1,77 @@
+import React, { useEffect, useState } from 'react'
+
+import { useRouter } from 'expo-router'
+import { Alert, Button, StyleSheet, TextInput } from 'react-native'
+
+import { ThemedView } from '@/components/ThemedView'
+import { useAuth } from '@/context/auth'
+
+export default function Modal() {
+  const [username, setUsername] = useState('')
+  const [password, setPassword] = useState('')
+  const { isAuthenticated, logIn } = useAuth()
+  const router = useRouter()
+
+  useEffect(() => {
+    if (isAuthenticated && router.canGoBack()) {
+      router.push('../')
+    }
+  }, [isAuthenticated])
+
+  const onLogin = async () => {
+    if (!username || !password) {
+      Alert.alert('Alert', `Both username and password are required`)
+      return
+    }
+
+    const response = await logIn({ username, password })
+
+    if (response.message) {
+      Alert.alert('Alert', response.message)
+    } else if (response.error) {
+      Alert.alert('Alert', response.error)
+    }
+  }
+
+  return (
+    
+      
+
+      
+
+      
+    
+  )
+}
+
+const styles = StyleSheet.create({
+  container: {
+    flex: 1,
+    justifyContent: 'center',
+    padding: 20,
+  },
+  title: {
+    fontSize: 28,
+    marginBottom: 20,
+    textAlign: 'center',
+  },
+  input: {
+    borderWidth: 1,
+    borderColor: '#ccc',
+    padding: 12,
+    marginBottom: 16,
+    borderRadius: 6,
+  },
+})
diff --git a/__fixtures__/test-project/app/app/posts/[id].tsx b/__fixtures__/test-project/app/app/posts/[id].tsx
new file mode 100644
index 000000000..a818e93bb
--- /dev/null
+++ b/__fixtures__/test-project/app/app/posts/[id].tsx
@@ -0,0 +1,47 @@
+import React, { useEffect } from 'react'
+
+import { gql, useQuery } from '@apollo/client'
+import { useLocalSearchParams, useNavigation } from 'expo-router'
+
+import { BlogPost, BlogPostProps } from '@/components/BlogPost'
+import ThemedScrollView from '@/components/ThemedScrollView'
+import { ThemedView } from '@/components/ThemedView'
+
+const QUERY = gql`
+  query FindPostById($id: Int!) {
+    post(id: $id) {
+      id
+      title
+      body
+      author {
+        email
+        fullName
+      }
+      createdAt
+    }
+  }
+`
+
+export default function PostScreen() {
+  const navigation = useNavigation()
+  const { id, title } = useLocalSearchParams<{ id: string; title: string }>()
+  const { data } = useQuery<{ post: BlogPostProps['blogPost'] }>(QUERY, {
+    variables: { id: Number(id) },
+  })
+
+  useEffect(() => {
+    navigation.setOptions({ title })
+  }, [])
+
+  const blogPost = data?.post
+
+  return (
+    
+      
+        {blogPost ? (
+          
+        ) : null}
+      
+    
+  )
+}
diff --git a/__fixtures__/test-project/app/assets/fonts/SpaceMono-Regular.ttf b/__fixtures__/test-project/app/assets/fonts/SpaceMono-Regular.ttf
new file mode 100755
index 000000000..28d7ff717
Binary files /dev/null and b/__fixtures__/test-project/app/assets/fonts/SpaceMono-Regular.ttf differ
diff --git a/__fixtures__/test-project/app/assets/images/adaptive-icon.png b/__fixtures__/test-project/app/assets/images/adaptive-icon.png
new file mode 100644
index 000000000..03d6f6b6c
Binary files /dev/null and b/__fixtures__/test-project/app/assets/images/adaptive-icon.png differ
diff --git a/__fixtures__/test-project/app/assets/images/favicon.png b/__fixtures__/test-project/app/assets/images/favicon.png
new file mode 100644
index 000000000..e75f697b1
Binary files /dev/null and b/__fixtures__/test-project/app/assets/images/favicon.png differ
diff --git a/__fixtures__/test-project/app/assets/images/icon.png b/__fixtures__/test-project/app/assets/images/icon.png
new file mode 100644
index 000000000..a0b1526fc
Binary files /dev/null and b/__fixtures__/test-project/app/assets/images/icon.png differ
diff --git a/__fixtures__/test-project/app/assets/images/splash-icon.png b/__fixtures__/test-project/app/assets/images/splash-icon.png
new file mode 100644
index 000000000..03d6f6b6c
Binary files /dev/null and b/__fixtures__/test-project/app/assets/images/splash-icon.png differ
diff --git a/__fixtures__/test-project/app/components/BlogPost.tsx b/__fixtures__/test-project/app/components/BlogPost.tsx
new file mode 100644
index 000000000..904436864
--- /dev/null
+++ b/__fixtures__/test-project/app/components/BlogPost.tsx
@@ -0,0 +1,60 @@
+import React from 'react'
+
+import { Link } from 'expo-router'
+import { StyleSheet } from 'react-native'
+
+import { ThemedText } from './ThemedText'
+import { ThemedView } from './ThemedView'
+
+export interface BlogPostProps {
+  blogPost: {
+    id: number
+    title: string
+    body: string
+    createdAt: string
+    author: { email: string; fullName: string }
+  }
+  isLast: boolean
+}
+
+export function BlogPost({ blogPost, isLast }: BlogPostProps) {
+  return (
+    
+      
+        {new Intl.DateTimeFormat('en-US', {
+          year: 'numeric',
+          month: 'long',
+          day: 'numeric',
+        }).format(new Date(blogPost.createdAt))}{' '}
+        - By: {blogPost.author.fullName}
+      
+      ({blogPost.author.email})
+      
+        {blogPost.title}
+      
+      
+        {blogPost.body}
+      
+    
+  )
+}
+
+const styles = StyleSheet.create({
+  info: {
+    fontSize: 14,
+    lineHeight: 20,
+  },
+  body: {
+    marginBlock: 8,
+  },
+  border: {
+    paddingBottom: 8,
+    borderBottomColor: '#e5e7eb',
+    borderBottomWidth: 1,
+  },
+})
diff --git a/__fixtures__/test-project/app/components/HapticTab.tsx b/__fixtures__/test-project/app/components/HapticTab.tsx
new file mode 100644
index 000000000..3cac35f42
--- /dev/null
+++ b/__fixtures__/test-project/app/components/HapticTab.tsx
@@ -0,0 +1,20 @@
+import React from 'react'
+
+import { BottomTabBarButtonProps } from '@react-navigation/bottom-tabs'
+import { PlatformPressable } from '@react-navigation/elements'
+import * as Haptics from 'expo-haptics'
+
+export function HapticTab(props: BottomTabBarButtonProps) {
+  return (
+     {
+        if (process.env.EXPO_OS === 'ios') {
+          // Add a soft haptic feedback when pressing down on the tabs.
+          Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light)
+        }
+        props.onPressIn?.(ev)
+      }}
+    />
+  )
+}
diff --git a/__fixtures__/test-project/app/components/Post.tsx b/__fixtures__/test-project/app/components/Post.tsx
new file mode 100644
index 000000000..6d04b2028
--- /dev/null
+++ b/__fixtures__/test-project/app/components/Post.tsx
@@ -0,0 +1,106 @@
+import React from 'react'
+
+import { gql, useMutation } from '@apollo/client'
+import { Link } from 'expo-router'
+import { Alert, Button, StyleSheet } from 'react-native'
+
+import { ThemedText } from './ThemedText'
+import { ThemedView } from './ThemedView'
+
+import { FIND_POSTS_QUERY } from '@/app/(tabs)/admin'
+
+const DELETE_POST_MUTATION = gql`
+  mutation DeletePostMutation($id: Int!) {
+    deletePost(id: $id) {
+      id
+    }
+  }
+`
+
+export interface PostProps {
+  post: {
+    id: number
+    title: string
+    body: string
+    createdAt: string
+    author: { email: string; fullName: string }
+  }
+}
+
+export function Post({ post }: PostProps) {
+  const [deletePost] = useMutation(DELETE_POST_MUTATION, {
+    onCompleted: () => {
+      Alert.alert('Alert', 'Post deleted')
+    },
+    onError: (error) => {
+      Alert.alert('Alert', error.message)
+    },
+    // This refetches the query on the list page. Read more about other ways to
+    // update the cache over here:
+    // https://www.apollographql.com/docs/react/data/mutations/#making-all-other-cache-updates
+    refetchQueries: [{ query: FIND_POSTS_QUERY }],
+    awaitRefetchQueries: true,
+  })
+
+  const onDelete = () => {
+    Alert.alert('Alert', `Are you sure you want to delete post ${post.id}?`, [
+      { text: 'Cancel' },
+      { text: 'OK', onPress: () => deletePost({ variables: { id: post.id } }) },
+    ])
+  }
+
+  return (
+    
+      
+        {new Intl.DateTimeFormat('en-US', {
+          year: 'numeric',
+          month: 'long',
+          day: 'numeric',
+        }).format(new Date(post.createdAt))}{' '}
+        - By: {post.author.fullName}
+      
+      
+        {post.id}: {post.title}
+      
+      ID: {post.id}
+      
+        
+          
+            Show
+          
+        
+        
+      
+    
+  )
+}
+
+const styles = StyleSheet.create({
+  container: {
+    padding: 8,
+    borderColor: '#e5e7eb',
+    borderWidth: 1,
+  },
+  info: {
+    fontSize: 14,
+    lineHeight: 20,
+  },
+  show: {
+    fontSize: 12,
+    lineHeight: 16,
+    textTransform: 'uppercase',
+    color: '#6b7280',
+  },
+  actions: {
+    display: 'flex',
+    flexDirection: 'row',
+    gap: 16,
+    justifyContent: 'flex-end',
+    alignItems: 'center',
+  },
+})
diff --git a/__fixtures__/test-project/app/components/ThemedScrollView.tsx b/__fixtures__/test-project/app/components/ThemedScrollView.tsx
new file mode 100644
index 000000000..20c044126
--- /dev/null
+++ b/__fixtures__/test-project/app/components/ThemedScrollView.tsx
@@ -0,0 +1,38 @@
+import type { PropsWithChildren } from 'react'
+import React from 'react'
+
+import { ScrollView, StyleSheet } from 'react-native'
+import Animated, { useAnimatedRef } from 'react-native-reanimated'
+
+import { ThemedView } from '@/components/ThemedView'
+import { useBottomTabOverflow } from '@/components/ui/TabBarBackground'
+
+export default function ThemedScrollView({ children }: PropsWithChildren) {
+  const scrollRef = useAnimatedRef()
+  const bottom = useBottomTabOverflow()
+
+  return (
+    
+      
+        {children}
+      
+    
+  )
+}
+
+const styles = StyleSheet.create({
+  container: {
+    flex: 1,
+  },
+  content: {
+    flex: 1,
+    padding: 32,
+    gap: 16,
+    overflow: 'hidden',
+  },
+})
diff --git a/__fixtures__/test-project/app/components/ThemedText.tsx b/__fixtures__/test-project/app/components/ThemedText.tsx
new file mode 100644
index 000000000..317b37549
--- /dev/null
+++ b/__fixtures__/test-project/app/components/ThemedText.tsx
@@ -0,0 +1,62 @@
+import React from 'react'
+
+import { StyleSheet, Text, type TextProps } from 'react-native'
+
+import { useThemeColor } from '@/hooks/useThemeColor'
+
+export type ThemedTextProps = TextProps & {
+  lightColor?: string
+  darkColor?: string
+  type?: 'default' | 'title' | 'defaultSemiBold' | 'subtitle' | 'link'
+}
+
+export function ThemedText({
+  style,
+  lightColor,
+  darkColor,
+  type = 'default',
+  ...rest
+}: ThemedTextProps) {
+  const color = useThemeColor({ light: lightColor, dark: darkColor }, 'text')
+
+  return (
+    
+  )
+}
+
+const styles = StyleSheet.create({
+  default: {
+    fontSize: 16,
+    lineHeight: 24,
+  },
+  defaultSemiBold: {
+    fontSize: 16,
+    lineHeight: 24,
+    fontWeight: '600',
+  },
+  title: {
+    fontSize: 32,
+    fontWeight: 'bold',
+    lineHeight: 32,
+  },
+  subtitle: {
+    fontSize: 20,
+    fontWeight: 'bold',
+  },
+  link: {
+    lineHeight: 30,
+    fontSize: 16,
+    color: '#0a7ea4',
+  },
+})
diff --git a/__fixtures__/test-project/app/components/ThemedView.tsx b/__fixtures__/test-project/app/components/ThemedView.tsx
new file mode 100644
index 000000000..0e24ab358
--- /dev/null
+++ b/__fixtures__/test-project/app/components/ThemedView.tsx
@@ -0,0 +1,24 @@
+import React from 'react'
+
+import { View, type ViewProps } from 'react-native'
+
+import { useThemeColor } from '@/hooks/useThemeColor'
+
+export type ThemedViewProps = ViewProps & {
+  lightColor?: string
+  darkColor?: string
+}
+
+export function ThemedView({
+  style,
+  lightColor,
+  darkColor,
+  ...otherProps
+}: ThemedViewProps) {
+  const backgroundColor = useThemeColor(
+    { light: lightColor, dark: darkColor },
+    'background'
+  )
+
+  return 
+}
diff --git a/__fixtures__/test-project/app/components/ui/TabBarBackground.ios.tsx b/__fixtures__/test-project/app/components/ui/TabBarBackground.ios.tsx
new file mode 100644
index 000000000..e1374703d
--- /dev/null
+++ b/__fixtures__/test-project/app/components/ui/TabBarBackground.ios.tsx
@@ -0,0 +1,21 @@
+import React from 'react'
+
+import { useBottomTabBarHeight } from '@react-navigation/bottom-tabs'
+import { BlurView } from 'expo-blur'
+import { StyleSheet } from 'react-native'
+
+export default function BlurTabBarBackground() {
+  return (
+    
+  )
+}
+
+export function useBottomTabOverflow() {
+  return useBottomTabBarHeight()
+}
diff --git a/__fixtures__/test-project/app/components/ui/TabBarBackground.tsx b/__fixtures__/test-project/app/components/ui/TabBarBackground.tsx
new file mode 100644
index 000000000..5373d4902
--- /dev/null
+++ b/__fixtures__/test-project/app/components/ui/TabBarBackground.tsx
@@ -0,0 +1,6 @@
+// This is a shim for web and Android where the tab bar is generally opaque.
+export default undefined
+
+export function useBottomTabOverflow() {
+  return 0
+}
diff --git a/__fixtures__/test-project/app/constants/Colors.ts b/__fixtures__/test-project/app/constants/Colors.ts
new file mode 100644
index 000000000..5889ed1ab
--- /dev/null
+++ b/__fixtures__/test-project/app/constants/Colors.ts
@@ -0,0 +1,26 @@
+/**
+ * Below are the colors that are used in the app. The colors are defined in the light and dark mode.
+ * There are many other ways to style your app. For example, [Nativewind](https://www.nativewind.dev/), [Tamagui](https://tamagui.dev/), [unistyles](https://reactnativeunistyles.vercel.app), etc.
+ */
+
+const tintColorLight = '#0a7ea4'
+const tintColorDark = '#fff'
+
+export const Colors = {
+  light: {
+    text: '#11181C',
+    background: '#fff',
+    tint: tintColorLight,
+    icon: '#687076',
+    tabIconDefault: '#687076',
+    tabIconSelected: tintColorLight,
+  },
+  dark: {
+    text: '#ECEDEE',
+    background: '#151718',
+    tint: tintColorDark,
+    icon: '#9BA1A6',
+    tabIconDefault: '#9BA1A6',
+    tabIconSelected: tintColorDark,
+  },
+}
diff --git a/__fixtures__/test-project/app/context/auth.ts b/__fixtures__/test-project/app/context/auth.ts
new file mode 100644
index 000000000..ea216f87a
--- /dev/null
+++ b/__fixtures__/test-project/app/context/auth.ts
@@ -0,0 +1,12 @@
+globalThis.RWJS_ENV = {
+  RWJS_API_GRAPHQL_URL: process.env.EXPO_PUBLIC_RWJS_API_GRAPHQL_URL!,
+  RWJS_API_URL: process.env.EXPO_PUBLIC_API_URL!,
+}
+globalThis.RWJS_API_GRAPHQL_URL = process.env.EXPO_PUBLIC_RWJS_API_GRAPHQL_URL!
+globalThis.RWJS_API_URL = process.env.EXPO_PUBLIC_API_URL!
+
+import { createDbAuthClient, createAuth } from '@cedarjs/auth-dbauth-web'
+
+const dbAuthClient = createDbAuthClient()
+
+export const { AuthProvider, useAuth } = createAuth(dbAuthClient)
diff --git a/__fixtures__/test-project/app/hooks/useColorScheme.ts b/__fixtures__/test-project/app/hooks/useColorScheme.ts
new file mode 100644
index 000000000..b370337ae
--- /dev/null
+++ b/__fixtures__/test-project/app/hooks/useColorScheme.ts
@@ -0,0 +1 @@
+export { useColorScheme } from 'react-native'
diff --git a/__fixtures__/test-project/app/hooks/useColorScheme.web.ts b/__fixtures__/test-project/app/hooks/useColorScheme.web.ts
new file mode 100644
index 000000000..8f3a67513
--- /dev/null
+++ b/__fixtures__/test-project/app/hooks/useColorScheme.web.ts
@@ -0,0 +1,22 @@
+import { useEffect, useState } from 'react'
+
+import { useColorScheme as useRNColorScheme } from 'react-native'
+
+/**
+ * To support static rendering, this value needs to be re-calculated on the client side for web
+ */
+export function useColorScheme() {
+  const [hasHydrated, setHasHydrated] = useState(false)
+
+  useEffect(() => {
+    setHasHydrated(true)
+  }, [])
+
+  const colorScheme = useRNColorScheme()
+
+  if (hasHydrated) {
+    return colorScheme
+  }
+
+  return 'light'
+}
diff --git a/__fixtures__/test-project/app/hooks/useThemeColor.ts b/__fixtures__/test-project/app/hooks/useThemeColor.ts
new file mode 100644
index 000000000..c95428617
--- /dev/null
+++ b/__fixtures__/test-project/app/hooks/useThemeColor.ts
@@ -0,0 +1,21 @@
+/**
+ * Learn more about light and dark modes:
+ * https://docs.expo.dev/guides/color-schemes/
+ */
+
+import { Colors } from '@/constants/Colors'
+import { useColorScheme } from '@/hooks/useColorScheme'
+
+export function useThemeColor(
+  props: { light?: string; dark?: string },
+  colorName: keyof typeof Colors.light & keyof typeof Colors.dark
+) {
+  const theme = useColorScheme() ?? 'light'
+  const colorFromProps = props[theme]
+
+  if (colorFromProps) {
+    return colorFromProps
+  } else {
+    return Colors[theme][colorName]
+  }
+}
diff --git a/__fixtures__/test-project/app/package.json b/__fixtures__/test-project/app/package.json
new file mode 100644
index 000000000..2b068af21
--- /dev/null
+++ b/__fixtures__/test-project/app/package.json
@@ -0,0 +1,45 @@
+{
+  "name": "app",
+  "main": "expo-router/entry",
+  "version": "1.0.0",
+  "scripts": {
+    "start": "expo start",
+    "reset-project": "node ./scripts/reset-project.js",
+    "android": "expo start --android",
+    "ios": "expo start --ios",
+    "web": "expo start --web"
+  },
+  "dependencies": {
+    "@cedarjs/auth-dbauth-web": "0.0.5",
+    "@cedarjs/web": "0.0.5",
+    "@expo/vector-icons": "^14.1.0",
+    "@react-navigation/bottom-tabs": "^7.3.10",
+    "@react-navigation/elements": "^2.3.8",
+    "@react-navigation/native": "^7.1.6",
+    "expo": "~53.0.13",
+    "expo-blur": "~14.1.5",
+    "expo-constants": "~17.1.6",
+    "expo-font": "~13.3.1",
+    "expo-haptics": "~14.1.4",
+    "expo-image": "~2.3.0",
+    "expo-linking": "~7.1.5",
+    "expo-router": "~5.1.1",
+    "expo-splash-screen": "~0.30.9",
+    "expo-status-bar": "~2.2.3",
+    "expo-system-ui": "~5.0.9",
+    "react": "19.0.0",
+    "react-dom": "19.0.0",
+    "react-native": "0.79.4",
+    "react-native-gesture-handler": "~2.24.0",
+    "react-native-reanimated": "~3.17.4",
+    "react-native-safe-area-context": "5.4.0",
+    "react-native-screens": "~4.11.1",
+    "react-native-web": "~0.20.0",
+    "react-native-webview": "13.13.5"
+  },
+  "devDependencies": {
+    "@babel/core": "^7.25.2",
+    "@types/react": "~19.0.10"
+  },
+  "private": true
+}
diff --git a/__fixtures__/test-project/app/scripts/reset-project.js b/__fixtures__/test-project/app/scripts/reset-project.js
new file mode 100755
index 000000000..6f6763a21
--- /dev/null
+++ b/__fixtures__/test-project/app/scripts/reset-project.js
@@ -0,0 +1,112 @@
+#!/usr/bin/env node
+
+/**
+ * This script is used to reset the project to a blank state.
+ * It deletes or moves the /app, /components, /hooks, /scripts, and /constants directories to /app-example based on user input and creates a new /app directory with an index.tsx and _layout.tsx file.
+ * You can remove the `reset-project` script from package.json and safely delete this file after running it.
+ */
+
+const fs = require('fs')
+const path = require('path')
+const readline = require('readline')
+
+const root = process.cwd()
+const oldDirs = ['app', 'components', 'hooks', 'constants', 'scripts']
+const exampleDir = 'app-example'
+const newAppDir = 'app'
+const exampleDirPath = path.join(root, exampleDir)
+
+const indexContent = `import { Text, View } from "react-native";
+
+export default function Index() {
+  return (
+    
+      Edit app/index.tsx to edit this screen.
+    
+  );
+}
+`
+
+const layoutContent = `import { Stack } from "expo-router";
+
+export default function RootLayout() {
+  return ;
+}
+`
+
+const rl = readline.createInterface({
+  input: process.stdin,
+  output: process.stdout,
+})
+
+const moveDirectories = async (userInput) => {
+  try {
+    if (userInput === 'y') {
+      // Create the app-example directory
+      await fs.promises.mkdir(exampleDirPath, { recursive: true })
+      console.log(`š /${exampleDir} directory created.`)
+    }
+
+    // Move old directories to new app-example directory or delete them
+    for (const dir of oldDirs) {
+      const oldDirPath = path.join(root, dir)
+      if (fs.existsSync(oldDirPath)) {
+        if (userInput === 'y') {
+          const newDirPath = path.join(root, exampleDir, dir)
+          await fs.promises.rename(oldDirPath, newDirPath)
+          console.log(`ā”ļø /${dir} moved to /${exampleDir}/${dir}.`)
+        } else {
+          await fs.promises.rm(oldDirPath, { recursive: true, force: true })
+          console.log(`ā /${dir} deleted.`)
+        }
+      } else {
+        console.log(`ā”ļø /${dir} does not exist, skipping.`)
+      }
+    }
+
+    // Create new /app directory
+    const newAppDirPath = path.join(root, newAppDir)
+    await fs.promises.mkdir(newAppDirPath, { recursive: true })
+    console.log('\nš New /app directory created.')
+
+    // Create index.tsx
+    const indexPath = path.join(newAppDirPath, 'index.tsx')
+    await fs.promises.writeFile(indexPath, indexContent)
+    console.log('š app/index.tsx created.')
+
+    // Create _layout.tsx
+    const layoutPath = path.join(newAppDirPath, '_layout.tsx')
+    await fs.promises.writeFile(layoutPath, layoutContent)
+    console.log('š app/_layout.tsx created.')
+
+    console.log('\nā
 Project reset complete. Next steps:')
+    console.log(
+      `1. Run \`npx expo start\` to start a development server.\n2. Edit app/index.tsx to edit the main screen.${
+        userInput === 'y'
+          ? `\n3. Delete the /${exampleDir} directory when you're done referencing it.`
+          : ''
+      }`
+    )
+  } catch (error) {
+    console.error(`ā Error during script execution: ${error.message}`)
+  }
+}
+
+rl.question(
+  'Do you want to move existing files to /app-example instead of deleting them? (Y/n): ',
+  (answer) => {
+    const userInput = answer.trim().toLowerCase() || 'y'
+    if (userInput === 'y' || userInput === 'n') {
+      moveDirectories(userInput).finally(() => rl.close())
+    } else {
+      console.log("ā Invalid input. Please enter 'Y' or 'N'.")
+      rl.close()
+    }
+  }
+)
diff --git a/__fixtures__/test-project/app/tsconfig.json b/__fixtures__/test-project/app/tsconfig.json
new file mode 100644
index 000000000..ce27fee39
--- /dev/null
+++ b/__fixtures__/test-project/app/tsconfig.json
@@ -0,0 +1,10 @@
+{
+  "extends": "expo/tsconfig.base",
+  "compilerOptions": {
+    "strict": true,
+    "paths": {
+      "@/*": ["./*"]
+    }
+  },
+  "include": ["**/*.ts", "**/*.tsx", ".expo/types/**/*.ts", "expo-env.d.ts"]
+}
diff --git a/__fixtures__/test-project/package.json b/__fixtures__/test-project/package.json
index dc5914a84..c56223cf0 100644
--- a/__fixtures__/test-project/package.json
+++ b/__fixtures__/test-project/package.json
@@ -3,7 +3,8 @@
   "workspaces": {
     "packages": [
       "api",
-      "web"
+      "web",
+      "app"
     ]
   },
   "devDependencies": {
@@ -25,6 +26,8 @@
   "packageManager": "yarn@4.6.0",
   "resolutions": {
     "@storybook/react-dom-shim@npm:7.6.20": "https://verdaccio.tobbe.dev/@storybook/react-dom-shim/-/react-dom-shim-8.0.8.tgz",
-    "react-is": "19.0.0-rc-f2df5694-20240916"
+    "react-is": "19.0.0",
+    "react": "19.0.0",
+    "react-dom": "19.0.0"
   }
-}
\ No newline at end of file
+}