Quest Hunter follows a structured organization that separates concerns while maintaining clarity. This guide explains where different types of code live and the conventions used throughout the project.
Root Directory Layout
quest-hunter/
├── app/ # Expo Router pages (file-based routing)
├── components/ # React components
├── convex/ # Backend functions and schema
├── lib/ # Shared utilities and clients
├── hooks/ # Custom React hooks
├── assets/ # Static assets (images, fonts)
├── .vscode/ # Editor configuration
├── .github/ # CI/CD workflows
├── package.json # Dependencies and scripts
├── app.json # Expo app configuration
├── babel.config.js # Babel + NativeWind configuration
├── tailwind.config.js # Tailwind CSS configuration
├── tsconfig.json # TypeScript configuration
├── global.css # Global CSS variables
└── README.md # Project documentation
Application Routes (app/)
The app/ directory defines all routes using Expo Router’s file-based system.
Directory Structure
app/
├── _layout.tsx # Root layout (providers)
├── (auth)/ # Authentication routes (unprotected)
│ ├── _layout.tsx # Auth layout
│ ├── sign-in.tsx # /sign-in
│ └── oauth-native-callback.tsx # OAuth redirect handler
└── (tabs)/ # Main app routes (protected)
├── _layout.tsx # Tab bar layout
├── (quests)/ # Quest tab
│ ├── _layout.tsx # Quest stack layout
│ ├── index.tsx # /quests (list)
│ └── [id]/ # Dynamic quest route
│ ├── index.tsx # /quests/:id (detail)
│ └── location/
│ └── [locationId].tsx # /quests/:id/location/:locationId
├── (leaderboard)/ # Leaderboard tab
│ ├── _layout.tsx
│ └── index.tsx # /leaderboard
└── (profile)/ # Profile tab
├── _layout.tsx
└── index.tsx # /profile
Routing Conventions
Layout Files _layout.tsx files define shared UI for child routes (navigation, providers, headers)
Route Groups Folders in parentheses (name) group routes without adding URL segments
Dynamic Routes Folders in brackets [param] create dynamic route segments
Index Routes index.tsx files represent the default route for a directory
Root Layout (app/_layout.tsx)
The root layout sets up global providers:
< ClerkProvider > { /* Authentication */ }
< ConvexProviderWithClerk > { /* Backend + Auth integration */ }
< StatusBar /> { /* System status bar */ }
< RootLayoutNav /> { /* Navigation structure */ }
< PortalHost /> { /* Modal/dialog rendering */ }
</ ConvexProviderWithClerk >
</ ClerkProvider >
Protected Routes
Location: app/_layout.tsx:16-21
< Stack . Protected guard = {isSignedIn ?? false } >
< Stack . Screen name = "(tabs)" options = {{ headerShown : false }} />
</ Stack . Protected >
The (tabs) group is protected - unauthenticated users are redirected to (auth).
Components (components/)
Components are organized by domain and purpose:
components/
├── ui/ # Generic UI components
│ ├── button.tsx # Button component
│ ├── card.tsx # Card container
│ ├── input.tsx # Text input
│ ├── label.tsx # Form label
│ ├── text.tsx # Typography
│ ├── separator.tsx # Divider line
│ ├── tabs.tsx # Tab component
│ ├── alert-dialog.tsx # Modal dialog
│ ├── map.tsx # Map component
│ ├── screen.tsx # Screen container
│ ├── sign-in-form.tsx # Sign-in form
│ ├── social-connections.tsx # OAuth buttons
│ ├── loading-state.tsx # Loading spinner
│ ├── error-state.tsx # Error display
│ ├── empty-state.tsx # Empty state message
│ └── native-only-animated-view.tsx # Platform-specific animation
├── quests/ # Quest-specific components
│ └── quest-item.tsx # Quest list item
└── location/ # Location-specific components
├── location-action-bar.tsx # Action buttons for locations
├── complete-quest-dialog.tsx # Quest completion modal
└── cancel-quest-dialog.tsx # Quest cancellation modal
Component Conventions
UI components - Reusable, generic components in ui/
Domain components - Feature-specific components in named directories
Naming - kebab-case for files, PascalCase for exports
Styling - Use NativeWind classes via className prop
Example Component Structure
// components/ui/button.tsx
import { Text , Pressable } from 'react-native' ;
import { cva , type VariantProps } from 'class-variance-authority' ;
const buttonVariants = cva (
'flex-row items-center justify-center rounded-md' ,
{
variants: {
variant: {
default: 'bg-primary' ,
outline: 'border border-input' ,
},
size: {
default: 'h-10 px-4' ,
sm: 'h-9 px-3' ,
},
},
}
);
interface ButtonProps extends VariantProps < typeof buttonVariants > {
onPress : () => void ;
children : React . ReactNode ;
}
export const Button = ({ variant , size , ... props } : ButtonProps ) => {
return < Pressable className ={ buttonVariants ({ variant , size })} { ... props } />;
};
Components use class-variance-authority (CVA) for managing style variants, keeping component APIs clean and type-safe.
Backend (convex/)
The convex/ directory contains all backend logic:
convex/
├── _generated/ # Auto-generated Convex files
│ ├── api.d.ts # API types for frontend
│ ├── dataModel.d.ts # Database schema types
│ └── server.d.ts # Server function types
├── _utils/ # Backend utilities
│ ├── auth.ts # Auth helper functions
│ └── user.ts # User helper functions
├── schema.ts # Database schema definition
├── auth.config.ts # Clerk integration config
├── http.ts # HTTP routes (webhooks)
├── quests.ts # Quest queries & mutations
├── locations.ts # Location queries & mutations
└── users.ts # User mutations
Backend File Types
File Purpose Example schema.tsDatabase table definitions defineTable({ name: v.string() })*.tsQueries, mutations, actions export const get = query({...})http.tsHTTP endpoints export default httpRouter()auth.config.tsAuthentication setup Clerk JWT validation _utils/*.tsShared backend logic requireUser(ctx)
Schema Definition (convex/schema.ts)
import { defineSchema , defineTable } from "convex/server" ;
import { v } from "convex/values" ;
const schema = defineSchema ({
quests: defineTable ({
name: v . string (),
description: v . string (),
difficulty: v . union (
v . literal ( "einfach" ),
v . literal ( "mittel" ),
v . literal ( "schwer" )
),
xp: v . number (),
imageUrl: v . string (),
}),
locations: defineTable ({
questId: v . id ( "quests" ),
name: v . string (),
coordinates: v . object ({
latitude: v . number (),
longitude: v . number (),
}),
order: v . number (),
})
. index ( "by_quest" , [ "questId" ])
. index ( "by_quest_order" , [ "questId" , "order" ]),
users: defineTable ({
clerkId: v . string (),
email: v . string (),
firstName: v . optional ( v . string ()),
imageUrl: v . optional ( v . string ()),
}). index ( "by_clerk_id" , [ "clerkId" ]),
userQuests: defineTable ({
userId: v . id ( "users" ),
questId: v . id ( "quests" ),
startedAt: v . number (),
completedAt: v . optional ( v . number ()),
})
. index ( "by_user" , [ "userId" ])
. index ( "by_user_and_quest" , [ "userId" , "questId" ]),
userLocations: defineTable ({
userId: v . id ( "users" ),
questId: v . id ( "quests" ),
locationId: v . id ( "locations" ),
photoStorageId: v . id ( "_storage" ),
completedAt: v . number (),
})
. index ( "by_user_and_quest" , [ "userId" , "questId" ])
. index ( "by_user_and_location" , [ "userId" , "locationId" ]),
});
export default schema ;
Query Example (convex/quests.ts)
Location: convex/quests.ts:31-48
export const listRecommended = query ({
args: {},
handler : async ( ctx ) => {
const user = await requireUser ( ctx );
const [ allQuests , userQuests ] = await Promise . all ([
ctx . db . query ( "quests" ). collect (),
ctx . db
. query ( "userQuests" )
. withIndex ( "by_user" , ( q ) => q . eq ( "userId" , user . _id ))
. collect (),
]);
const completedQuestIds = deriveCompletedQuestIds ( userQuests );
return allQuests . filter (( quest ) => ! completedQuestIds . has ( quest . _id ));
},
});
Mutation Example (convex/quests.ts)
Location: convex/quests.ts:124-153
export const start = mutation ({
args: {
questId: v . id ( "quests" ),
},
handler : async ( ctx , { questId }) => {
const user = await requireUser ( ctx );
const quest = await ctx . db . get ( questId );
if ( ! quest ) throw new ConvexError ( "Quest not found" );
const existing = await ctx . db
. query ( "userQuests" )
. withIndex ( "by_user_and_quest" , ( q ) =>
q . eq ( "userId" , user . _id ). eq ( "questId" , questId )
)
. unique ();
if ( existing ) throw new ConvexError ( "Quest already started" );
return await ctx . db . insert ( "userQuests" , {
userId: user . _id ,
questId ,
startedAt: Date . now (),
});
},
});
Authentication Utility (convex/_utils/user.ts)
import { ConvexError } from "convex/values" ;
import { QueryCtx , MutationCtx } from "../_generated/server" ;
export async function requireUser (
ctx : QueryCtx | MutationCtx
) {
const identity = await ctx . auth . getUserIdentity ();
if ( ! identity ) throw new ConvexError ( "Not authenticated" );
const user = await ctx . db
. query ( "users" )
. withIndex ( "by_clerk_id" , ( q ) => q . eq ( "clerkId" , identity . subject ))
. unique ();
if ( ! user ) throw new ConvexError ( "User not found" );
return user ;
}
Shared Libraries (lib/)
Utilities and configuration shared across the app:
lib/
├── convex-client.ts # Convex client initialization
├── theme.ts # Theme constants and colors
└── utils.ts # Utility functions (cn, etc.)
Convex Client (lib/convex-client.ts)
import { ConvexReactClient } from "convex/react" ;
export const convex = new ConvexReactClient (
process . env . EXPO_PUBLIC_CONVEX_URL ?? "https://placeholder.convex.cloud"
);
Theme Constants (lib/theme.ts)
export const THEME = {
primary: "#007AFF" ,
secondary: "#5856D6" ,
// ...
} as const ;
Utilities (lib/utils.ts)
import { clsx , type ClassValue } from "clsx" ;
import { twMerge } from "tailwind-merge" ;
// Merge Tailwind classes without conflicts
export function cn ( ... inputs : ClassValue []) {
return twMerge ( clsx ( inputs ));
}
Custom Hooks (hooks/)
Reusable React hooks:
hooks/
└── use-hide-tab-bar.ts # Hook to hide tab bar on scroll
Hook Example
import { useNavigation } from 'expo-router' ;
import { useLayoutEffect } from 'react' ;
export function useHideTabBar () {
const navigation = useNavigation ();
useLayoutEffect (() => {
navigation . setOptions ({ tabBarStyle: { display: 'none' } });
}, [ navigation ]);
}
Configuration Files
TypeScript Configuration (tsconfig.json)
{
"extends" : "expo/tsconfig.base" ,
"compilerOptions" : {
"strict" : true ,
"paths" : {
"@/*" : [ "./*" ]
}
}
}
The @/* path alias allows importing from the root:
import { convex } from "@/lib/convex-client" ;
import { Button } from "@/components/ui/button" ;
Babel Configuration (babel.config.js)
Location: babel.config.js
export default function ( api ) {
api . cache ( true );
return {
presets: [
[ "babel-preset-expo" , { jsxImportSource: "nativewind" }],
"nativewind/babel" ,
],
};
}
This configuration:
Uses Expo’s preset for React Native
Configures NativeWind for Tailwind CSS support
Sets JSX import source for proper rendering
Tailwind Configuration (tailwind.config.js)
Location: tailwind.config.js
module . exports = {
content: [ "./app/**/*.{ts,tsx}" , "./components/**/*.{ts,tsx}" ],
presets: [ require ( "nativewind/preset" )],
theme: {
extend: {
colors: {
border: "hsl(var(--border))" ,
background: "hsl(var(--background))" ,
// Custom color system using CSS variables
},
},
},
plugins: [ require ( "tailwindcss-animate" )],
};
Global Styles (global.css)
Defines CSS variables used by Tailwind:
:root {
--background : 0 0 % 100 % ;
--foreground : 240 10 % 3.9 % ;
--primary : 240 5.9 % 10 % ;
/* ... */
}
Naming Conventions
Files
Type Convention Example Components kebab-case.tsx quest-item.tsxRoutes kebab-case.tsx sign-in.tsxUtilities kebab-case.ts convex-client.tsHooks use-*.ts use-hide-tab-bar.tsConfig *.config.js tailwind.config.js
Code
Type Convention Example Components PascalCase QuestItem, ButtonHooks camelCase (use prefix) useHideTabBarFunctions camelCase requireUser, deriveCompletedQuestIdsConstants UPPER_SNAKE_CASE SEVEN_DAYS_MS, THEMEVariables camelCase userQuests, completedIds
Import Organization
Imports should be organized in this order:
// 1. External dependencies
import { useState } from 'react' ;
import { View , Text } from 'react-native' ;
import { useQuery } from 'convex/react' ;
// 2. Internal absolute imports (using @/)
import { api } from '@/convex/_generated/api' ;
import { Button } from '@/components/ui/button' ;
import { cn } from '@/lib/utils' ;
// 3. Relative imports
import { QuestItem } from './quest-item' ;
// 4. Types
import type { Quest } from '@/convex/_generated/dataModel' ;
File Size Guidelines
Components : Keep under 200 lines; extract sub-components if larger
Queries/Mutations : One file per domain (quests.ts, users.ts)
Utilities : Small, focused functions (prefer multiple small files)
Layouts : Minimal logic, primarily composition
When a component file exceeds 200 lines, consider extracting parts into separate files in the same directory.
Development Workflow
Adding a New Feature
Define schema (if needed) in convex/schema.ts
Create queries/mutations in convex/[domain].ts
Build UI components in components/[domain]/
Create route in app/ with appropriate layout
Add navigation to relevant _layout.tsx
File Creation Checklist
Next Steps
Architecture Overview Learn how all these pieces fit together in the overall architecture
Tech Stack Understand the technologies powering each part of the structure