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