Understanding Mythic Framework’s dual-database system
Mythic Framework uses a dual-database architecture: MongoDB as the primary database for game data, and MySQL for compatibility and relational data. This approach combines the flexibility of NoSQL with the structure of SQL.
// Auth Database - User authentication and permissions{ database: "auth", collections: [ "users", // User accounts "roles", // Permission roles "bans", // Ban records "whitelist" // Whitelist entries ]}// Game Database - All game-related data{ database: "fivem", collections: [ "characters", // Player characters "inventory", // Character inventories "vehicles", // Player vehicles "properties", // Owned properties "jobs", // Job data "businesses", // Business data "logs", // Game logs // ... many more ]}
MongoDB operations are handled through the Node.js wrapper in mythic-base using asynchronous callback patterns.Location:mythic-base/core/sv_database.js
Database Architecture: Mythic uses two separate MongoDB databases accessed via the Database component:
Database.Game — Game data (characters, inventory, vehicles, etc.)
Database.Auth — Authentication data (users, roles, bans)
After RetrieveComponents(), use Database.Game:method() directly. The full path is Database.Game but the local shorthand is standard.
-- Find one document (with callback)Database.Game:findOne({ collection = 'characters', query = { SID = 1 }}, function(success, character) if success and character then print('Found character:', character.First, character.Last) else print('Character not found') endend)-- Find multiple documentsDatabase.Game:find({ collection = 'vehicles', query = { owner = characterId, stored = false }, options = { limit = 10, sort = { purchaseDate = -1 } }}, function(success, vehicles) if success and vehicles then for _, vehicle in ipairs(vehicles) do print('Vehicle:', vehicle.model) end endend)-- Insert documentDatabase.Game:insertOne({ collection = 'logs', document = { type = 'admin_action', action = 'teleport', admin = adminId, target = targetId, timestamp = os.time() }}, function(success, result) if success then print('Log inserted with ID:', result._id) endend)-- Update documentDatabase.Game:updateOne({ collection = 'characters', query = { SID = characterId }, update = { ['$set'] = { Cash = newMoney } }}, function(success, result) if success then print('Character updated') endend)-- Delete document (Auth database example)Database.Auth:deleteOne({ collection = 'bans', query = { identifier = 'steam:110000...' }}, function(success, result) if success then print('Ban record deleted') endend)
Database.Game:findOne({ collection = 'characters', query = { SID = 1 }}, function(success, char) if success and char then print('Found character:', char.First, char.Last) endend)
Callback receives:(success, document)
find(params, callback)
Find multiple documents.
Database.Game:find({ collection = 'characters', query = { User = accountId }, options = { limit = 5, sort = { created = -1 } }}, function(success, chars) if success and chars then for _, char in ipairs(chars) do print(char.First, char.Last) end endend)
Callback receives:(success, documents)
insertOne(params, callback)
Insert a single document.
Database.Game:insertOne({ collection = 'characters', document = { SID = GetNextSID(), User = accountId, First = 'John', Last = 'Doe', Gender = 0, DOB = '1990-01-01', Phone = 5551234, Created = os.time() }}, function(success, result) if success then print('Character created with ID:', result._id) endend)
-- Execute queryexports.oxmysql:execute('UPDATE users SET money = ? WHERE id = ?', { newMoney, userId})-- Fetch single rowlocal user = exports.oxmysql:single('SELECT * FROM users WHERE id = ?', { userId })-- Fetch multiple rowslocal users = exports.oxmysql:query('SELECT * FROM users WHERE banned = ?', { false })-- Insert and get IDlocal insertId = exports.oxmysql:insert('INSERT INTO logs (action, player) VALUES (?, ?)', { 'login', playerId})-- Prepared statements (safer)local result = exports.oxmysql:prepare('SELECT * FROM vehicles WHERE owner = ?', { characterId })
-- Select alllocal vehicles = exports.oxmysql:query('SELECT * FROM vehicles WHERE owner = ?', { characterId})-- Select specific columnslocal names = exports.oxmysql:query('SELECT first_name, last_name FROM characters WHERE id = ?', { charId})-- With JOINlocal data = exports.oxmysql:query([[ SELECT c.*, v.plate, v.model FROM characters c LEFT JOIN vehicles v ON v.owner = c.id WHERE c.id = ?]], { charId })
INSERT Queries
-- Simple insertexports.oxmysql:insert('INSERT INTO logs (action, player, timestamp) VALUES (?, ?, ?)', { 'purchase', playerId, os.time()})-- Insert and get IDlocal vehicleId = exports.oxmysql:insert([[ INSERT INTO vehicles (owner, model, plate) VALUES (?, ?, ?)]], { characterId, 'adder', 'ABC123'})
UPDATE Queries
-- Update single fieldexports.oxmysql:execute('UPDATE characters SET cash = ? WHERE id = ?', { newCash, charId})-- Update multiple fieldsexports.oxmysql:execute([[ UPDATE characters SET cash = ?, bank = ?, last_login = ? WHERE id = ?]], { cash, bank, os.time(), charId})-- Increment fieldexports.oxmysql:execute('UPDATE characters SET cash = cash + ? WHERE id = ?', { amount, charId})
DELETE Queries
-- Delete recordexports.oxmysql:execute('DELETE FROM vehicles WHERE id = ?', { vehicleId })-- Delete with conditionexports.oxmysql:execute('DELETE FROM logs WHERE timestamp < ?', { os.time() - (30 * 24 * 60 * 60) -- Older than 30 days})
CREATE TABLE vehicles ( id INT PRIMARY KEY AUTO_INCREMENT, owner INT NOT NULL, -- Character SID model VARCHAR(50) NOT NULL, plate VARCHAR(8) UNIQUE NOT NULL, garage VARCHAR(50) DEFAULT 'pillbox', state INT DEFAULT 0, -- 0=out, 1=garaged fuel INT DEFAULT 100, engine FLOAT DEFAULT 1000.0, body FLOAT DEFAULT 1000.0, mods TEXT, -- JSON customization created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, INDEX idx_owner (owner), INDEX idx_plate (plate));
Store dynamic, frequently-changing data in MongoDB:
-- Good fit for MongoDB-- Character stats-- Inventory items-- Player positions-- Logs and events-- Bad fit for MySQL-- Constantly updating data requires many UPDATE queries
2. Use MySQL for Relational Data
Use MySQL when you need complex relationships:
-- Good fit for MySQL-- Vehicle ownership with joins to characters-- Transaction history requiring aggregation-- Property relationships-- Use JOIN queriesSELECT v.*, c.first_name, c.last_nameFROM vehicles vJOIN characters c ON v.owner = c.idWHERE v.plate = ?
-- MySQL - Create indexesCREATE INDEX idx_owner ON vehicles(owner);CREATE INDEX idx_character ON inventory(character_id);
4. Sanitize User Input
Always use parameterized queries:
-- ✅ GOOD - Parameterized (MongoDB)Database.Game:findOne({ collection = 'characters', query = { SID = userInput }}, callback)-- ✅ GOOD - Parameterized (MySQL)exports.oxmysql:single('SELECT * FROM users WHERE id = ?', { userInput })-- ❌ BAD - SQL injection riskexports.oxmysql:single('SELECT * FROM users WHERE id = ' .. userInput)
5. Handle Errors
Database operations can fail. Always handle errors in callbacks:
Database.Game:findOne({ collection = 'characters', query = { SID = charId }}, function(success, character) if not success then Logger:Error('Database', 'Failed to fetch character', { charId = charId }) return end if not character then print('Character not found') return end -- Process character print('Found:', character.First, character.Last)end)
6. Batch Operations
When possible, batch database operations:
-- ❌ BAD - Multiple queries in loopfor i = 1, 100 do Database.Game:insertOne({ collection = 'logs', document = { action = 'test_' .. i } }, function() end)end-- ✅ GOOD - Single batch insertlocal documents = {}for i = 1, 100 do table.insert(documents, { action = 'test_' .. i })endDatabase.Game:insertMany({ collection = 'logs', documents = documents}, function(success, result) print('Inserted', #documents, 'documents')end)
7. Cache When Appropriate
Cache frequently accessed, rarely changing data:
-- Cache examplelocal itemCache = {}function GetItem(itemId, callback) if itemCache[itemId] then callback(itemCache[itemId]) return end Database.Game:findOne({ collection = 'items', query = { id = itemId } }, function(success, item) if success and item then itemCache[itemId] = item callback(item) else callback(nil) end end)end
-- 1. Use prepared statementsexports.oxmysql:prepare('SELECT * FROM vehicles WHERE owner = ?', { charId })-- 2. Add indexesCREATE INDEX idx_owner ON vehicles(owner);CREATE INDEX idx_compound ON vehicles(owner, state);-- 3. Optimize queries-- Use EXPLAIN to check query performanceEXPLAIN SELECT * FROM vehicles WHERE owner = 1;-- 4. Limit resultsSELECT * FROM logs WHERE player = ? ORDER BY timestamp DESC LIMIT 100;
When in doubt, use MongoDB for game data. It’s more flexible and handles nested/changing data better. Use MySQL when you specifically need relational queries or compatibility with other resources.