Skip to main content
Quest Hunter tracks your progress through multiple systems, recording every quest started, location completed, and XP point earned. Your progress is stored in real-time and synced across devices.

Progress Data Structure

Your progress is tracked across three main database tables:

User Quests

Tracks which quests you’ve started and completed, including timestamps.

User Locations

Records every location checkpoint you’ve completed with photo verification.

User Profile

Stores your account information from Clerk authentication.

XP System

Experience Points (XP) are the primary measure of your progress in Quest Hunter.

How XP Works

  • Each quest has a fixed XP value defined in the quest schema
  • XP is only awarded upon complete quest completion
  • Partial completion (some locations done) does not award XP
  • You cannot earn XP from the same quest twice
XP values vary by quest based on difficulty, estimated time, number of locations, and overall complexity.

Earning XP

To earn XP from a quest:
  1. Start the Quest: Create a userQuests record with startedAt timestamp
  2. Complete All Locations: Visit every location in order with photo verification
  3. Finish the Quest: Mark the quest as complete
  4. Receive XP: The quest’s XP value is added to your total
The completion validation is strict (convex/quests.ts:172-190):
const [questLocations, completedLocations] = await Promise.all([
  ctx.db
    .query("locations")
    .withIndex("by_quest", (q) => q.eq("questId", questId))
    .collect(),
  ctx.db
    .query("userLocations")
    .withIndex("by_user_and_quest", (q) =>
      q.eq("userId", user._id).eq("questId", questId)
    )
    .collect(),
]);

const requiredIds = new Set(questLocations.map((l) => l._id));
const completedIds = new Set(completedLocations.map((cl) => cl.locationId));
if ([...requiredIds].some((id) => !completedIds.has(id)))
  throw new ConvexError("Quest not finished");

await ctx.db.patch(userQuest._id, { completedAt: Date.now() });

Calculating Total XP

While the current codebase doesn’t show a dedicated XP aggregation query, you can calculate total XP by:
  1. Querying all completed quests for a user
  2. Fetching the XP value for each quest
  3. Summing the XP values
Example implementation:
const completedUserQuests = await ctx.db
  .query("userQuests")
  .withIndex("by_user", (q) => q.eq("userId", user._id))
  .filter((q) => q.neq(q.field("completedAt"), undefined))
  .collect();

const quests = await Promise.all(
  completedUserQuests.map((uq) => ctx.db.get(uq.questId))
);

const totalXP = quests
  .filter((quest) => quest !== null)
  .reduce((sum, quest) => sum + quest.xp, 0);

Quest Tracking

Your quest history is maintained in the userQuests table:

Schema Structure

userQuests: {
  userId: Id<"users">,
  questId: Id<"quests">,
  startedAt: number,        // Unix timestamp (required)
  completedAt?: number      // Unix timestamp (optional)
}

Quest States

A quest can be in one of three states:
No userQuests record exists for this quest and user combination. The quest appears in the “Recommended” or “New” tabs.
A userQuests record exists with startedAt but no completedAt. The quest can be resumed from the last completed location.Query implementation (convex/quests.ts:93-106):
const userQuests = await ctx.db
  .query("userQuests")
  .withIndex("by_user", (q) => q.eq("userId", user._id))
  .filter((q) => q.eq(q.field("completedAt"), undefined))
  .collect();
A userQuests record exists with both startedAt and completedAt timestamps. The quest appears in the “Done” tab and cannot be restarted.Query implementation (convex/quests.ts:74-91):
const completedUserQuests = await ctx.db
  .query("userQuests")
  .withIndex("by_user", (q) => q.eq("userId", user._id))
  .filter((q) => q.neq(q.field("completedAt"), undefined))
  .collect();

Database Indexes

Three indexes optimize quest progress queries:
Index NameFieldsUse Case
by_user[userId]Fetch all quests for a specific user
by_quest[questId]Find all users who’ve attempted a quest
by_user_and_quest[userId, questId]Check specific quest status for a user

Location Completion Tracking

Every location you complete is recorded in userLocations:

Schema Structure

userLocations: {
  userId: Id<"users">,
  questId: Id<"quests">,
  locationId: Id<"locations">,
  photoStorageId: Id<"_storage">,
  completedAt: number
}

Tracking Features

Photo Evidence

Every completion includes a photoStorageId linking to your verification photo in Convex storage.

Completion Time

The completedAt timestamp records exactly when you completed each location.

Quest Association

Both questId and locationId are stored, enabling queries by quest or specific location.

User History

All your location completions are permanently stored and queryable.

Querying Completed Locations

Fetch all locations completed for a specific quest (convex/locations.ts:36-50):
export const listCompleted = query({
  args: {
    questId: v.id("quests"),
  },
  handler: async (ctx, { questId }) => {
    const user = await requireUser(ctx);

    return await ctx.db
      .query("userLocations")
      .withIndex("by_user_and_quest", (q) =>
        q.eq("userId", user._id).eq("questId", questId)
      )
      .collect();
  },
});

Location Indexes

Two indexes enable efficient location tracking:
Index NameFieldsUse Case
by_user_and_quest[userId, questId]Get all completed locations for a quest
by_user_and_location[userId, locationId]Check if user completed specific location

User Profile Data

Your user profile is synced from Clerk authentication:

Schema Structure

users: {
  clerkId: string,
  email: string,
  firstName?: string,
  lastName?: string,
  imageUrl?: string
}

Profile Management

User data is automatically managed through Clerk webhooks (convex/users.ts):
  • Creation: New users are inserted when they first authenticate
  • Updates: Profile changes in Clerk sync to the database
  • Deletion: User data is removed if account is deleted
You don’t need to manually update your profile in Quest Hunter. Changes made in your Clerk account automatically sync.

Progress Statistics

While not explicitly shown in the current UI, you can derive various statistics from your progress data:

Available Metrics

Count of all userQuests records (with or without completedAt).
Count of userQuests records where completedAt is defined.
Percentage: (Completed Quests / Started Quests) × 100
Count of all userLocations records for the user.
Sum of XP values from all completed quests.
Average time between startedAt and completedAt for completed quests.
Most frequently completed quest category (abenteuer, kultur, natur, etc.).
List of recently completed locations or quests sorted by completedAt.

Time Tracking

All timestamps use Unix time (milliseconds since epoch):
// When starting a quest
startedAt: Date.now()

// When completing a quest or location
completedAt: Date.now()
This allows for precise duration calculations:
const durationMs = userQuest.completedAt - userQuest.startedAt;
const durationMinutes = Math.floor(durationMs / (1000 * 60));

Progress Persistence

All progress is automatically saved to the Convex database in real-time. You can safely close the app and resume quests later without losing progress.
Progress persistence guarantees:
  • Immediate Writes: Location completions save instantly
  • Cross-Device Sync: Progress syncs across all your devices
  • No Manual Save: Everything is automatic
  • Permanent History: Completed quests remain in your history forever

Best Practices

Complete Quests

Don’t leave quests partially complete. Finish all locations to earn the XP reward.

Track Your Stats

Monitor your completion rate and total XP to measure improvement over time.

Verify Photos

Ensure photo uploads succeed before moving to the next location. Failed uploads prevent progress.

Resume Quickly

In-progress quests can be resumed anytime. Don’t hesitate to pause and continue later.
  • Quests - Learn about quest structure and XP values
  • Locations - Understand location completion tracking
  • Leaderboard - See how your progress compares to others

Build docs developers (and LLMs) love