Skip to main content

Database (MongoDB)

True Life uses MongoDB 7.0 with the native driver for data persistence. Database operations are server-only.

Connection

The database connection is established during server bootstrap:

import { getMongoDb, withMongoTransaction } from "@core/db/client";
 
// Get the database instance
const db = await getMongoDb();

Configuration

Configure MongoDB via server convars:

# server.cfg
# ⚠️ Local/dev only — do not use these credentials in production
set mongodb_uri "mongodb://devuser:localpass@localhost:27017"
set mongodb_database "true_life"

Repository Pattern

Database operations are encapsulated in repository files:

// src/modules/characters/repository.ts
"use server";
 
import { getMongoDb } from "@core/db/client";
import type { Character } from "@modules/core/characters/types";
 
export async function findById(id: string): Promise<Character | null> {
	const db = await getMongoDb();
	return db.collection<Character>("characters").findOne({ id });
}
 
export async function findByPlayerId(playerId: string): Promise<Character[]> {
	const db = await getMongoDb();
	return db.collection<Character>("characters").find({ playerId }).toArray();
}
 
export async function create(character: Character): Promise<void> {
	const db = await getMongoDb();
	await db.collection<Character>("characters").insertOne(character);
}
 
export async function update(id: string, data: Partial<Character>): Promise<void> {
	const db = await getMongoDb();
	await db.collection<Character>("characters").updateOne({ id }, { $set: data });
}

Transactions

Use transactions for atomic operations:

"use server";
 
import { withMongoTransaction } from "@core/db/client";
 
export async function transferFunds(fromAccount: string, toAccount: string, amount: number) {
	return await withMongoTransaction(async (db, session) => {
		// Debit source account
		const debitResult = await db
			.collection("accounts")
			.updateOne({ id: fromAccount, balance: { $gte: amount } }, { $inc: { balance: -amount } }, { session });
 
		if (debitResult.modifiedCount === 0) {
			throw new Error("Insufficient funds");
		}
 
		// Credit destination account
		await db.collection("accounts").updateOne({ id: toAccount }, { $inc: { balance: amount } }, { session });
 
		return { success: true };
	});
}

Indexes

Create indexes in the init module:

// src/modules/init/repository.ts
"use server";
 
import { getMongoDb } from "@core/db/client";
 
export async function ensureIndexes(): Promise<void> {
	const db = await getMongoDb();
 
	// Characters collection
	await db
		.collection("characters")
		.createIndexes([{ key: { id: 1 }, unique: true }, { key: { playerId: 1 } }, { key: { "data.name": 1 } }]);
 
	// Accounts collection
	await db.collection("accounts").createIndexes([{ key: { id: 1 }, unique: true }, { key: { characterId: 1 } }]);
}

Type Safety

Define collection types for type-safe queries:

// src/modules/characters/types.ts
export interface Character {
	id: string;
	playerId: string;
	data: {
		name: string;
		dateOfBirth: string;
		gender: "male" | "female";
		appearance: AppearanceData;
		playtime: number; // Total playtime in minutes
	};
	createdAt: Date;
	updatedAt: Date;
}
// Repository with typed collection
const db = await getMongoDb();
const characters = db.collection<Character>("characters");
 
// TypeScript knows the return type
const char = await characters.findOne({ id: "123" });
// char: Character | null

Aggregation Pipelines

Use aggregation for complex queries:

export async function getCharacterStats(playerId: string) {
	const db = await getMongoDb();
 
	return db
		.collection("characters")
		.aggregate([
			{ $match: { playerId } },
			{
				$group: {
					_id: "$playerId",
					totalCharacters: { $sum: 1 },
					totalPlaytime: { $sum: "$data.playtime" },
				},
			},
		])
		.toArray();
}

Best Practices

  1. Always use "use server" directive - Repository files are server-only
  2. Use transactions for multi-document operations - Ensure atomicity
  3. Create indexes for common queries - Improve performance
  4. Type your collections - Leverage TypeScript for safety
  5. Handle errors gracefully - Wrap in try/catch
  6. Use projections - Only fetch needed fields
// Good: Only fetch needed fields
const name = await db.collection("characters").findOne({ id }, { projection: { "data.name": 1 } });