Development
Complete guide to developing resources for QBCore framework.
This guide covers everything you need to know about developing custom resources for QBCore framework.
Development Environment Setup
Prerequisites
- Code Editor: VS Code with Lua extensions recommended
 - Git: For version control
 - Database Tool: HeidiSQL, phpMyAdmin, or similar
 - QBCore Server: Running development server
 
Recommended VS Code Extensions
{
  "recommendations": [
    "sumneko.lua",
    "actboy168.lua-debug",
    "keyring.lua",
    "koihik.vscode-lua-format",
    "trixnz.vscode-lua"
  ]
}Development Server Setup
Create a separate development server configuration:
# server-dev.cfg
set sv_hostname "QBCore Development Server"
set sv_maxclients 4
sv_licenseKey "your_license_key"
# Developer permissions
add_ace group.admin command allow
add_ace group.admin resource allow
add_principal identifier.steam:your_steam_id group.admin
# Fast restart for development
sv_scriptHookAllowed 1
set developer_mode true
Resource Structure
Standard QBCore Resource Structure
qb-resourcename/
├── client/
│   ├── main.lua
│   ├── events.lua
│   └── utils.lua
├── server/
│   ├── main.lua
│   ├── events.lua
│   └── callbacks.lua
├── shared/
│   ├── config.lua
│   ├── items.lua
│   └── locale.lua
├── html/                   # For NUI resources
│   ├── index.html
│   ├── style.css
│   └── script.js
├── database/               # Database files
│   └── qb-resourcename.sql
├── locales/               # Translation files
│   ├── en.lua
│   ├── es.lua
│   └── fr.lua
├── fxmanifest.lua
└── README.mdfxmanifest.lua Template
fx_version 'cerulean'
game 'gta5'
 
author 'Your Name <[email protected]>'
description 'QBCore Resource Description'
version '1.0.0'
repository 'https://github.com/yourusername/qb-resourcename'
 
shared_scripts {
    '@qb-core/shared/locale.lua',
    'locales/en.lua',
    'shared/*.lua'
}
 
client_scripts {
    'client/*.lua'
}
 
server_scripts {
    '@oxmysql/lib/MySQL.lua',
    'server/*.lua'
}
 
ui_page 'html/index.html'  -- For NUI resources
 
files {
    'html/index.html',
    'html/style.css',
    'html/script.js'
}
 
lua54 'yes'
 
dependencies {
    'qb-core',
    'oxmysql'
}
 
provide 'qb-resourcename'  -- Optional: for resource replacementCore Integration
Getting QBCore Object
-- Client-side
local QBCore = exports['qb-core']:GetCoreObject()
 
-- Server-side
local QBCore = exports['qb-core']:GetCoreObject()
 
-- Alternative method (deprecated but still works)
QBCore = nil
CreateThread(function()
    while QBCore == nil do
        TriggerEvent('QBCore:GetObject', function(obj) QBCore = obj end)
        Wait(200)
    end
end)Player Data Management
Getting Player Data
-- Server-side
local Player = QBCore.Functions.GetPlayer(source)
if Player then
    local playerData = Player.PlayerData
    local citizenId = Player.PlayerData.citizenid
    local job = Player.PlayerData.job
    local money = Player.PlayerData.money
end
 
-- Client-side
local PlayerData = QBCore.Functions.GetPlayerData()
if PlayerData then
    local job = PlayerData.job
    local money = PlayerData.money
endPlayer Events
-- Client-side: Listen for player data updates
RegisterNetEvent('QBCore:Client:OnPlayerLoaded', function()
    PlayerData = QBCore.Functions.GetPlayerData()
    -- Initialize your resource after player loads
end)
 
RegisterNetEvent('QBCore:Client:OnJobUpdate', function(JobInfo)
    PlayerData.job = JobInfo
    -- Handle job updates
end)
 
RegisterNetEvent('QBCore:Client:OnMoneyChange', function(moneyType, amount, operation)
    -- Handle money changes
end)
 
-- Server-side: Player management
AddEventHandler('QBCore:Server:OnPlayerLoaded', function(Player)
    -- Handle player loading
end)
 
AddEventHandler('playerDropped', function()
    local src = source
    local Player = QBCore.Functions.GetPlayer(src)
    if Player then
        -- Handle player disconnect
    end
end)Database Integration
Using oxmysql
-- SELECT query
MySQL.Async.fetchAll('SELECT * FROM players WHERE job = ?', {jobName}, function(result)
    if result[1] then
        -- Handle results
    end
end)
 
-- SELECT single row
MySQL.Async.fetchSingle('SELECT * FROM players WHERE citizenid = ?', {citizenId}, function(result)
    if result then
        -- Handle single result
    end
end)
 
-- INSERT query
MySQL.Async.execute('INSERT INTO my_table (citizenid, data) VALUES (?, ?)', {
    citizenId,
    json.encode(data)
}, function(affectedRows)
    if affectedRows > 0 then
        -- Success
    end
end)
 
-- UPDATE query
MySQL.Async.execute('UPDATE players SET money = ? WHERE citizenid = ?', {
    json.encode(money),
    citizenId
}, function(affectedRows)
    -- Handle update
end)Modern oxmysql (Promise-based)
-- Using promises (recommended)
local result = MySQL.query.await('SELECT * FROM players WHERE job = ?', {jobName})
if result[1] then
    -- Handle results
end
 
-- With error handling
local success, result = pcall(MySQL.query.await, 'SELECT * FROM players WHERE citizenid = ?', {citizenId})
if success and result[1] then
    -- Handle success
else
    print('Database query failed')
endItem System
Creating Custom Items
Add items to qb-core/shared/items.lua:
QBShared.Items = {
    -- Existing items...
    
    ['my_custom_item'] = {
        name = 'my_custom_item',
        label = 'My Custom Item',
        weight = 500,
        type = 'item',
        image = 'my_custom_item.png',
        unique = false,
        useable = true,
        shouldClose = true,
        combinable = nil,
        description = 'This is my custom item description'
    },
    
    ['weapon_custom'] = {
        name = 'weapon_custom',
        label = 'Custom Weapon',
        weight = 2000,
        type = 'weapon',
        ammotype = 'AMMO_PISTOL',
        image = 'weapon_custom.png',
        unique = true,
        useable = false,
        description = 'A custom weapon'
    }
}Item Usage Events
-- Server-side: Register useable item
QBCore.Functions.CreateUseableItem('my_custom_item', function(source, item)
    local Player = QBCore.Functions.GetPlayer(source)
    if Player then
        -- Item usage logic
        TriggerClientEvent('qb-myresource:client:useItem', source, item)
    end
end)
 
-- Client-side: Handle item usage
RegisterNetEvent('qb-myresource:client:useItem', function(item)
    -- Client-side item effects
    QBCore.Functions.Notify('You used ' .. item.label, 'success')
end)Inventory Management
-- Server-side inventory functions
local Player = QBCore.Functions.GetPlayer(source)
 
-- Add item
Player.Functions.AddItem('my_item', 1, false, {quality = 100})
 
-- Remove item
Player.Functions.RemoveItem('my_item', 1)
 
-- Get item
local item = Player.Functions.GetItemByName('my_item')
if item then
    print('Player has ' .. item.amount .. ' of ' .. item.label)
end
 
-- Check if player has item
local hasItem = Player.Functions.GetItemByName('my_item') ~= nilJob System
Creating Custom Jobs
Add jobs to qb-core/shared/jobs.lua:
QBShared.Jobs = {
    -- Existing jobs...
    
    ['mechanic'] = {
        label = 'Mechanic',
        defaultDuty = true,
        offDutyPay = false,
        grades = {
            ['0'] = {
                name = 'Trainee',
                payment = 50
            },
            ['1'] = {
                name = 'Mechanic',
                payment = 75
            },
            ['2'] = {
                name = 'Expert Mechanic',
                payment = 100
            },
            ['3'] = {
                name = 'Shop Supervisor',
                payment = 125
            },
            ['4'] = {
                name = 'Shop Owner',
                isboss = true,
                payment = 150
            },
        },
    }
}Job Management Functions
-- Server-side: Job management
local Player = QBCore.Functions.GetPlayer(source)
 
-- Set player job
Player.Functions.SetJob('mechanic', 2)
 
-- Check job permissions
if Player.PlayerData.job.name == 'police' and Player.PlayerData.job.grade.level >= 3 then
    -- Allow police captain+ actions
end
 
-- Check if player is boss
if Player.PlayerData.job.isboss then
    -- Boss-only actions
end
 
-- Client-side: Job checking
local PlayerData = QBCore.Functions.GetPlayerData()
if PlayerData.job.name == 'mechanic' then
    -- Mechanic-specific functionality
endVehicle System
Vehicle Management
-- Server-side: Vehicle functions
local Player = QBCore.Functions.GetPlayer(source)
 
-- Get player vehicles
local vehicles = MySQL.query.await('SELECT * FROM player_vehicles WHERE citizenid = ?', {Player.PlayerData.citizenid})
 
-- Add vehicle to player
local vehicleData = {
    citizenid = Player.PlayerData.citizenid,
    vehicle = 'adder',
    hash = GetHashKey('adder'),
    mods = json.encode({}),
    plate = 'ABC123',
    garage = 'pillboxgarage',
    fuel = 100,
    engine = 1000.0,
    body = 1000.0
}
 
MySQL.insert.await('INSERT INTO player_vehicles (citizenid, vehicle, hash, mods, plate, garage, fuel, engine, body) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)', {
    vehicleData.citizenid,
    vehicleData.vehicle,
    vehicleData.hash,
    vehicleData.mods,
    vehicleData.plate,
    vehicleData.garage,
    vehicleData.fuel,
    vehicleData.engine,
    vehicleData.body
})Vehicle Keys Integration
-- Give keys to player
TriggerEvent('qb-vehiclekeys:server:GiveVehicleKeys', source, plate)
 
-- Remove keys from player
TriggerEvent('qb-vehiclekeys:server:RemoveVehicleKeys', source, plate)
 
-- Client-side: Check if player has keys
local hasKeys = exports['qb-vehiclekeys']:HasKeys(plate)UI Development (NUI)
Basic NUI Setup
<!-- html/index.html -->
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>QBCore Resource UI</title>
    <link rel="stylesheet" href="style.css">
    <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
</head>
<body>
    <div id="container" style="display: none;">
        <div id="header">
            <h1>My QBCore Resource</h1>
            <button id="close-btn">×</button>
        </div>
        <div id="content">
            <!-- Your UI content here -->
        </div>
    </div>
    <script src="script.js"></script>
</body>
</html>/* html/style.css */
* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}
 
body {
    font-family: 'Arial', sans-serif;
    background: transparent;
    color: white;
}
 
#container {
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    width: 600px;
    height: 400px;
    background: rgba(0, 0, 0, 0.9);
    border-radius: 10px;
    border: 1px solid #333;
}
 
#header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 20px;
    border-bottom: 1px solid #333;
}
 
#close-btn {
    background: #ff4757;
    color: white;
    border: none;
    border-radius: 50%;
    width: 30px;
    height: 30px;
    cursor: pointer;
    font-size: 16px;
}
 
#content {
    padding: 20px;
}// html/script.js
$(document).ready(function() {
    // Hide UI on ESC key
    document.onkeyup = function(data) {
        if (data.which == 27) {
            closeUI();
        }
    }
    
    // Close button click
    $('#close-btn').click(function() {
        closeUI();
    });
    
    // Listen for messages from Lua
    window.addEventListener('message', function(event) {
        const data = event.data;
        
        switch(data.action) {
            case 'open':
                openUI(data.data);
                break;
            case 'close':
                closeUI();
                break;
            case 'updateData':
                updateUI(data.data);
                break;
        }
    });
});
 
function openUI(data) {
    $('#container').fadeIn(300);
    $.post('https://qb-myresource/uiLoaded', JSON.stringify({}));
}
 
function closeUI() {
    $('#container').fadeOut(300);
    $.post('https://qb-myresource/closeUI', JSON.stringify({}));
}
 
function updateUI(data) {
    // Update UI with new data
}Lua NUI Integration
-- Client-side NUI management
local isUIOpen = false
 
-- Open UI
function OpenUI(data)
    if isUIOpen then return end
    
    isUIOpen = true
    SetNuiFocus(true, true)
    SendNUIMessage({
        action = 'open',
        data = data
    })
end
 
-- Close UI
function CloseUI()
    if not isUIOpen then return end
    
    isUIOpen = false
    SetNuiFocus(false, false)
    SendNUIMessage({
        action = 'close'
    })
end
 
-- NUI Callbacks
RegisterNUICallback('uiLoaded', function(data, cb)
    -- UI has loaded
    cb('ok')
end)
 
RegisterNUICallback('closeUI', function(data, cb)
    CloseUI()
    cb('ok')
end)
 
-- Export functions for other resources
exports('OpenUI', OpenUI)
exports('CloseUI', CloseUI)Event System
Custom Events
-- Server-side events
RegisterNetEvent('qb-myresource:server:doSomething', function(data)
    local src = source
    local Player = QBCore.Functions.GetPlayer(src)
    
    if not Player then return end
    
    -- Validate data
    if not data or not data.value then
        return
    end
    
    -- Process request
    local success = processRequest(Player, data)
    
    -- Send response
    TriggerClientEvent('qb-myresource:client:requestResult', src, success)
end)
 
-- Client-side events
RegisterNetEvent('qb-myresource:client:requestResult', function(success)
    if success then
        QBCore.Functions.Notify('Request successful!', 'success')
    else
        QBCore.Functions.Notify('Request failed!', 'error')
    end
end)Callbacks
-- Server-side callback
QBCore.Functions.CreateCallback('qb-myresource:server:getData', function(source, cb, playerId)
    local Player = QBCore.Functions.GetPlayer(playerId)
    if Player then
        cb(Player.PlayerData)
    else
        cb(false)
    end
end)
 
-- Client-side callback usage
QBCore.Functions.TriggerCallback('qb-myresource:server:getData', function(playerData)
    if playerData then
        -- Use player data
    end
end, GetPlayerServerId(PlayerId()))Commands
Creating Commands
-- Server-side command
QBCore.Commands.Add('mycommand', 'Command description', {{name = 'target', help = 'Target player ID'}}, true, function(source, args)
    local Player = QBCore.Functions.GetPlayer(source)
    if not Player then return end
    
    local targetId = tonumber(args[1])
    if not targetId then
        TriggerClientEvent('QBCore:Notify', source, 'Invalid player ID', 'error')
        return
    end
    
    local TargetPlayer = QBCore.Functions.GetPlayer(targetId)
    if not TargetPlayer then
        TriggerClientEvent('QBCore:Notify', source, 'Player not found', 'error')
        return
    end
    
    -- Command logic here
    TriggerClientEvent('QBCore:Notify', source, 'Command executed successfully', 'success')
end, 'admin') -- Permission level
 
-- Client-side command
RegisterCommand('clientcommand', function(source, args, rawCommand)
    local PlayerData = QBCore.Functions.GetPlayerData()
    if PlayerData.job.name == 'police' then
        -- Police-only client command
    end
end, false)Configuration Management
config.lua Template
Config = {}
 
-- General settings
Config.Debug = false
Config.Locale = GetConvar('qb_locale', 'en')
 
-- Feature toggles
Config.EnableFeatureA = true
Config.EnableFeatureB = false
 
-- Timing settings
Config.Cooldowns = {
    action1 = 5000,  -- 5 seconds
    action2 = 30000, -- 30 seconds
}
 
-- Job permissions
Config.JobPermissions = {
    mechanic = {
        repair = 0,      -- Grade 0+
        advanced = 2,    -- Grade 2+
        boss = 4         -- Grade 4+
    },
    police = {
        arrest = 0,
        impound = 1,
        commander = 3
    }
}
 
-- Locations
Config.Locations = {
    mechanic_shop = {
        coords = vector3(123.45, 678.90, 12.34),
        heading = 90.0,
        radius = 2.0,
        blip = {
            sprite = 446,
            color = 2,
            scale = 0.8,
            name = "Mechanic Shop"
        }
    }
}
 
-- Items and pricing
Config.Items = {
    repair_kit = {
        item = 'repair_kit',
        price = 500,
        requiredJob = 'mechanic'
    }
}
 
-- Notifications
Config.Notifications = {
    success_repair = 'Vehicle repaired successfully',
    insufficient_funds = 'You don\'t have enough money',
    wrong_job = 'You don\'t have the required job'
}Testing & Debugging
Debug Functions
-- Debug utility
local function DebugPrint(...)
    if Config.Debug then
        print('^3[QB-MYRESOURCE]^7', ...)
    end
end
 
-- Server-side debugging
local function LogAction(player, action, data)
    if Config.Debug then
        print(string.format('^3[QB-MYRESOURCE]^7 Player: %s (%s) | Action: %s | Data: %s', 
            player.PlayerData.name, 
            player.PlayerData.citizenid, 
            action, 
            json.encode(data)
        ))
    end
end
 
-- Client-side debugging
local function DrawDebugText(text, x, y)
    if Config.Debug then
        SetTextFont(0)
        SetTextProportional(1)
        SetTextScale(0.0, 0.35)
        SetTextDropshadow(0, 0, 0, 0, 255)
        SetTextEdge(1, 0, 0, 0, 255)
        SetTextDropShadow()
        SetTextOutline()
        SetTextEntry("STRING")
        AddTextComponentString(text)
        DrawText(x, y)
    end
endTesting Checklist
- Resource starts without errors
 - Database connections work
 - Player events trigger correctly
 - Items can be used/given/removed
 - UI opens/closes properly
 - Commands execute with proper permissions
 - No console errors or warnings
 - Memory usage is reasonable
 - Performance is acceptable
 
Best Practices
Code Organization
- Separate Concerns: Keep client, server, and shared code separate
 - Use Callbacks: For data requests between client and server
 - Validate Everything: Never trust client input
 - Handle Errors: Use pcall for database operations
 - Performance: Avoid unnecessary loops and timers
 
Security
- Server Validation: Always validate on the server
 - Permission Checks: Verify job/permissions before actions
 - Rate Limiting: Prevent spam/abuse
 - Secure Events: Use source validation
 
Performance
- Efficient Queries: Use proper database indexes
 - Caching: Cache frequently accessed data
 - Resource Cleanup: Clean up on resource stop
 - Minimal UI: Keep NUI lightweight
 
This comprehensive development guide provides everything needed to create professional QBCore resources following best practices and framework conventions.