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