Add these lines to your server.cfg:
# IMPORTANT: Load ink_postgresql BEFORE resources that use it
ensure ink_postgresql
# Set PostgreSQL connection string
set postgresql_connection_string "postgresql://username:password@localhost:5432/database_name"
# Optional: Configure connection pool size
# set postgresql_connection_string "postgresql://username:password@localhost:5432/database_name?max=20&idle_timeout=30"
# Optional: Enable SSL connection
# set postgresql_connection_string "postgresql://username:password@localhost:5432/database_name?ssl=true"
# Then load your resources that depend on ink_postgresql
ensure your_gamemode
ensure your_resourceLoad Order is Critical: ink_postgresql must start before any resource that uses it.
Create a test resource to verify InkPG is working:
fxmanifest.lua:
fx_version 'cerulean'
game 'gta5'
server_scripts {
'server.lua'
}
dependency 'ink_postgresql'server.lua:
-- Use wrapper pattern for better performance
local InkPG = exports.ink_postgresql
-- IMPORTANT: Always wait for database to be ready
InkPG:ready(function()
print('[Test] Database connected!')
-- Create a test table
InkPG:execute([[
CREATE TABLE IF NOT EXISTS test_users (
id SERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
age INT,
created_at TIMESTAMP DEFAULT NOW()
)
]], {})
print('[Test] Test table created')
end)
-- Test command: /testdb
RegisterCommand('testdb', function(source, args)
-- Insert a user
local result = InkPG:insert(
'INSERT INTO test_users (name, age) VALUES ($1, $2)',
{'John Doe', 25}
)
print('Inserted user with ID:', result.insertId)
-- Query all users
local users = InkPG:query('SELECT * FROM test_users ORDER BY id DESC LIMIT 10', {})
print('Recent users:')
for _, user in ipairs(users) do
print(string.format(' ID: %d, Name: %s, Age: %d', user.id, user.name, user.age or 0))
end
-- Get single user
local user = InkPG:single('SELECT * FROM test_users WHERE id = $1', {result.insertId})
if user then
print('Found user:', user.name)
end
-- Update user
local updated = InkPG:update(
'UPDATE test_users SET age = $1 WHERE id = $2',
{30, result.insertId}
)
print('Updated rows:', updated.affectedRows)
-- Using named parameters
local results = InkPG:query(
'SELECT * FROM test_users WHERE name = :name AND age > :minAge',
{name = 'John Doe', minAge = 20}
)
print('Named param results:', #results)
end, false)
-- Test command: /testcallback
RegisterCommand('testcallback', function(source, args)
-- Using callbacks instead of promises
InkPG:query('SELECT COUNT(*) as total FROM test_users', {}, function(result)
if result and result[1] then
print('Total users (callback):', result[1].total)
end
end)
end, false)
-- Test command: /testjson
RegisterCommand('testjson', function(source, args)
-- Create table with JSONB column
InkPG:execute([[
CREATE TABLE IF NOT EXISTS test_players (
id SERIAL PRIMARY KEY,
identifier VARCHAR(255) NOT NULL,
data JSONB,
created_at TIMESTAMP DEFAULT NOW()
)
]], {})
-- Insert with JSON data
local playerData = json.encode({
inventory = {
{item = 'bread', count = 5},
{item = 'water', count = 3}
},
position = {x = 100.5, y = 200.3, z = 30.1},
job = 'police',
money = 5000
})
local result = InkPG:insert(
'INSERT INTO test_players (identifier, data) VALUES ($1, $2)',
{'license:abc123', playerData}
)
print('Inserted player ID:', result.insertId)
-- Query using JSONB operators
local cops = InkPG:query(
"SELECT * FROM test_players WHERE data->>'job' = $1",
{'police'}
)
print('Found', #cops, 'police officers')
-- Check if key exists in JSONB
local playersWithInventory = InkPG:query(
"SELECT * FROM test_players WHERE data ? 'inventory'",
{}
)
print('Players with inventory:', #playersWithInventory)
end, false)-- In your ESX resource
local InkPG = exports.ink_postgresql
-- Get player by identifier
function GetPlayerFromIdentifier(identifier)
local player = InkPG:single(
'SELECT * FROM users WHERE identifier = $1',
{identifier}
)
return player
end
-- Save player data
function SavePlayer(identifier, data)
local result = InkPG:update(
'UPDATE users SET accounts = $1, job = $2, position = $3 WHERE identifier = $4',
{json.encode(data.accounts), data.job, json.encode(data.position), identifier}
)
return result.affectedRows > 0
end
-- Get all online players
function GetOnlinePlayers()
return InkPG:query('SELECT * FROM users WHERE online = $1', {true})
end-- In your QB-Core resource
local InkPG = exports.ink_postgresql
-- Load player data
QBCore.Player.LoadPlayer = function(source, citizenid)
local player = InkPG:single(
'SELECT * FROM players WHERE citizenid = $1',
{citizenid}
)
if not player then return nil end
-- Parse JSON data
player.money = json.decode(player.money)
player.job = json.decode(player.job)
player.metadata = json.decode(player.metadata)
return player
end
-- Save player data
QBCore.Player.SavePlayer = function(source)
local Player = QBCore.Functions.GetPlayer(source)
InkPG:update([[
UPDATE players
SET money = $1, job = $2, position = $3, metadata = $4
WHERE citizenid = $5
]], {
json.encode(Player.PlayerData.money),
json.encode(Player.PlayerData.job),
json.encode(Player.PlayerData.position),
json.encode(Player.PlayerData.metadata),
Player.PlayerData.citizenid
})
endQuick PostgreSQL setup using Docker:
docker run --name fivem-postgres \
-e POSTGRES_USER=fivem \
-e POSTGRES_PASSWORD=changeme \
-e POSTGRES_DB=gameserver \
-p 5432:5432 \
-v postgres-data:/var/lib/postgresql/data \
-d postgres:15-alpineThen configure in server.cfg:
set postgresql_connection_string "postgresql://fivem:changeme@localhost:5432/gameserver"Test query performance:
RegisterCommand('perftest', function()
local startTime = os.clock()
-- Run 1000 queries
for i = 1, 1000 do
InkPG:single('SELECT $1 as num', {i})
end
local endTime = os.clock()
local duration = (endTime - startTime) * 1000
print(string.format('1000 queries completed in %.2fms (%.2fms avg)', duration, duration / 1000))
end, false)