Skip to main content
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

  1. UI components - Reusable, generic components in ui/
  2. Domain components - Feature-specific components in named directories
  3. Naming - kebab-case for files, PascalCase for exports
  4. 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

FilePurposeExample
schema.tsDatabase table definitionsdefineTable({ name: v.string() })
*.tsQueries, mutations, actionsexport const get = query({...})
http.tsHTTP endpointsexport default httpRouter()
auth.config.tsAuthentication setupClerk JWT validation
_utils/*.tsShared backend logicrequireUser(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

TypeConventionExample
Componentskebab-case.tsxquest-item.tsx
Routeskebab-case.tsxsign-in.tsx
Utilitieskebab-case.tsconvex-client.ts
Hooksuse-*.tsuse-hide-tab-bar.ts
Config*.config.jstailwind.config.js

Code

TypeConventionExample
ComponentsPascalCaseQuestItem, Button
HookscamelCase (use prefix)useHideTabBar
FunctionscamelCaserequireUser, deriveCompletedQuestIds
ConstantsUPPER_SNAKE_CASESEVEN_DAYS_MS, THEME
VariablescamelCaseuserQuests, 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

  1. Define schema (if needed) in convex/schema.ts
  2. Create queries/mutations in convex/[domain].ts
  3. Build UI components in components/[domain]/
  4. Create route in app/ with appropriate layout
  5. Add navigation to relevant _layout.tsx

File Creation Checklist

  • Use appropriate naming convention
  • Add to correct directory
  • Include TypeScript types
  • Use path aliases (@/*) for imports
  • Add to navigation if it’s a route
  • Export from index if it’s a component library

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

Build docs developers (and LLMs) love