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