Tarkov Kappa Navi uses Dexie.js as an abstraction layer over the browser’s IndexedDB to store user data locally. All progress, settings, and customizations are persisted in the client browser.
Database Class
The database is defined in src/db/database.ts:
export class TarkovKappaNaviDB extends Dexie {
profile!: Table<Profile, string>;
progress!: Table<ProgressRow, string>;
nowPins!: Table<NowPins, string>;
notes!: Table<NoteRow, string>;
logs!: Table<ProgressLog, number>;
hideoutProgress!: Table<HideoutProgressRow, string>;
mapPins!: Table<MapPinRow, string>;
hideoutInventory!: Table<HideoutItemInventoryRow, string>;
hideoutLevelInventory!: Table<HideoutLevelInventoryRow, [string, string]>;
pinPresets!: Table<PinPresetRow, string>;
}
Tables
profile
Stores the current user profile. Always contains a single row with id: "me".
Primary Key: id
Schema:
interface Profile {
id: string; // Always "me"
currentLevel: number; // Player level 1-79
wipeId: string; // Identifies the current wipe
autoStartUnlocked?: boolean; // Auto-start unlocked tasks
lang?: 'ja' | 'en'; // UI/API language
onboardingDone?: boolean; // First-time onboarding complete
updatedAt: number; // Epoch milliseconds
}
Usage:
- Stores player level for unlock calculations
- Tracks current wipe to isolate progress data
- Controls auto-start behavior for newly unlocked tasks
progress
Tracks completion status for each task.
Primary Key: taskId
Indexes: status
Schema:
type TaskStatus = 'not_started' | 'in_progress' | 'done';
interface ProgressRow {
taskId: string;
status: TaskStatus;
completedAt: number | null; // Epoch ms, null if incomplete
updatedAt: number; // Epoch ms
}
Query Patterns:
// Get all in-progress tasks
const inProgress = await db.progress
.where('status')
.equals('in_progress')
.toArray();
// Get status for specific task
const status = await db.progress.get(taskId);
// Update task status
await db.progress.put({
taskId: 'task-id',
status: 'done',
completedAt: Date.now(),
updatedAt: Date.now()
});
nowPins
Stores pinned “active” tasks shown in the Now panel. Single row with id: "me".
Primary Key: id
Schema:
interface NowPins {
id: string; // Always "me"
taskIds: string[]; // Max 10 tasks
}
Usage:
// Load current pins
const pins = await db.nowPins.get('me');
// Update pins
await db.nowPins.put({
id: 'me',
taskIds: ['task-1', 'task-2', 'task-3']
});
notes
Per-task user notes.
Primary Key: taskId
Schema:
interface NoteRow {
taskId: string;
text: string;
updatedAt: number; // Epoch ms
}
Usage:
// Save note
await db.notes.put({
taskId: 'some-task',
text: 'Need to bring IFAK',
updatedAt: Date.now()
});
// Get note
const note = await db.notes.get(taskId);
// Delete note
await db.notes.delete(taskId);
logs
Audit log of all status transitions for analytics and undo functionality.
Primary Key: ++id (auto-increment)
Indexes: taskId, at
Schema:
interface ProgressLog {
id?: number; // Auto-increment
taskId: string;
from: TaskStatus;
to: TaskStatus;
at: number; // Epoch ms
reason: string; // e.g. "manual", "bulk_prereq_complete"
}
Query Patterns:
// Get all logs for a task
const taskLogs = await db.logs
.where('taskId')
.equals(taskId)
.reverse()
.toArray();
// Get recent logs
const recent = await db.logs
.orderBy('at')
.reverse()
.limit(50)
.toArray();
// Log a status change
await db.logs.add({
taskId: 'task-id',
from: 'not_started',
to: 'in_progress',
at: Date.now(),
reason: 'manual'
});
hideoutProgress
Tracks built hideout station levels.
Primary Key: levelId
Indexes: stationId
Schema:
interface HideoutProgressRow {
levelId: string; // Unique level ID from tarkov.dev
stationId: string; // Station ID
level: number; // Level number
builtAt: number; // Epoch ms when built
}
Query Patterns:
// Get all built levels for a station
const builtLevels = await db.hideoutProgress
.where('stationId')
.equals(stationId)
.toArray();
// Mark level as built
await db.hideoutProgress.put({
levelId: 'level-id',
stationId: 'station-id',
level: 2,
builtAt: Date.now()
});
// Get all built level IDs
const allBuilt = await db.hideoutProgress.toArray();
const builtIds = new Set(allBuilt.map(row => row.levelId));
mapPins
User-created map pins with labels and visual customization.
Primary Key: id (UUID)
Indexes: [mapId+wipeId+viewMode] (compound)
Schema:
type PinColor = 'red' | 'blue' | 'yellow' | 'green' | 'purple' | 'white';
type PinShape = 'circle' | 'diamond' | 'square' | 'triangle' | 'star' | 'marker';
interface MapPinRow {
id: string; // crypto.randomUUID()
mapId: string; // tarkov.dev mapId
wipeId: string; // Profile.wipeId
viewMode: '2d' | '3d'; // Separate pins per view mode
label: string; // Max 100 characters
color: PinColor;
shape: PinShape;
x: number; // 0-100 (%)
y: number; // 0-100 (%)
createdAt: number;
updatedAt: number;
}
Query Patterns:
// Get pins for a specific map/wipe/view
const pins = await db.mapPins
.where('[mapId+wipeId+viewMode]')
.equals([mapId, wipeId, '3d'])
.toArray();
// Create pin
await db.mapPins.add({
id: crypto.randomUUID(),
mapId: 'customs',
wipeId: 'default',
viewMode: '3d',
label: 'USEC Camp',
color: 'red',
shape: 'marker',
x: 45.2,
y: 67.8,
createdAt: Date.now(),
updatedAt: Date.now()
});
// Delete pin
await db.mapPins.delete(pinId);
hideoutInventory
Legacy global item inventory (deprecated, kept for migration).
Primary Key: itemId
Schema:
interface HideoutItemInventoryRow {
itemId: string;
ownedCount: number; // >= 0
}
This table is deprecated. Use hideoutLevelInventory for per-level item tracking.
hideoutLevelInventory
Per-level item inventory for hideout upgrades.
Primary Key: [levelId+itemId] (compound)
Indexes: itemId
Schema:
interface HideoutLevelInventoryRow {
levelId: string; // HideoutLevelModel.id
itemId: string; // Item ID from tarkov.dev
ownedCount: number; // >= 0
}
Query Patterns:
// Get owned count for specific item in specific level
const row = await db.hideoutLevelInventory.get([levelId, itemId]);
const owned = row?.ownedCount ?? 0;
// Update owned count
await db.hideoutLevelInventory.put({
levelId: 'level-id',
itemId: 'item-id',
ownedCount: 5
});
// Get all items for a level
const items = await db.hideoutLevelInventory
.where('[levelId+itemId]')
.between([levelId, ''], [levelId, '\uffff'])
.toArray();
pinPresets
Saved map pin configurations for quick restore.
Primary Key: id (UUID)
Indexes: createdAt
Schema:
interface PinPresetRow {
id: string; // crypto.randomUUID()
name: string;
pins: Array<{
mapId: string;
viewMode: '2d' | '3d';
label: string;
color: string;
shape: string;
x: number;
y: number;
}>;
createdAt: number;
}
Usage:
// Save preset
await db.pinPresets.add({
id: crypto.randomUUID(),
name: 'Customs Quest Locations',
pins: [...currentPins],
createdAt: Date.now()
});
// Load all presets
const presets = await db.pinPresets
.orderBy('createdAt')
.reverse()
.toArray();
// Delete preset
await db.pinPresets.delete(presetId);
Validation Schemas
All database types have corresponding Zod schemas in src/db/schemas.ts for validation during import/export:
import { z } from 'zod';
export const profileSchema = z.object({
id: z.string(),
currentLevel: z.number().int().min(1).max(79),
wipeId: z.string().default('default'),
autoStartUnlocked: z.boolean().optional(),
updatedAt: z.number(),
});
export const progressRowSchema = z.object({
taskId: z.string(),
status: z.enum(['not_started', 'in_progress', 'done']),
completedAt: z.number().nullable(),
updatedAt: z.number(),
});
// Full export/import schema
export const exportDataSchema = z.object({
profile: profileSchema,
progress: z.array(progressRowSchema),
nowPins: nowPinsSchema,
notes: z.array(noteRowSchema),
logs: z.array(progressLogSchema),
hideoutProgress: z.array(hideoutProgressRowSchema).optional(),
mapPins: z.array(mapPinRowSchema).optional(),
hideoutItemInventory: z.array(
z.union([hideoutLevelInventoryRowSchema, legacyGlobalInventoryRowSchema]),
).optional(),
});
Database Instance
A singleton instance is exported for use throughout the app:
import { db } from '@/db/database';
// Use in components or services
const profile = await db.profile.get('me');
const allProgress = await db.progress.toArray();
Migration Strategy
The database uses Dexie’s versioning system. Current version is 1:
this.version(1).stores({
profile: 'id',
progress: 'taskId, status',
nowPins: 'id',
notes: 'taskId',
logs: '++id, taskId, at',
hideoutProgress: 'levelId, stationId',
mapPins: 'id, [mapId+wipeId+viewMode]',
hideoutInventory: 'itemId',
hideoutLevelInventory: '[levelId+itemId], itemId',
pinPresets: 'id, createdAt',
});
For schema changes, increment the version and provide upgrade logic:
this.version(2).stores({
// Updated schema
}).upgrade(tx => {
// Migration logic
});