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/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
- 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 } }
);