Skip to Content
QBCore docs – powered by Nextra 4
Advanced🔧 Custom Resource Development

🔧 Custom Resource Development

Learn how to create custom QBCore resources from the ground up, following best practices and professional development patterns.

Getting Started

Custom resources extend QBCore’s functionality by adding new features, jobs, systems, or gameplay mechanics. This guide covers the complete development lifecycle.

Resource Types

QBCore supports several types of custom resources:

  • Job Resources - Custom employment systems
  • Activity Resources - Gameplay activities and mini-games
  • Utility Resources - Helper systems and tools
  • Integration Resources - Third-party system connections

Basic Resource Structure

Minimum Required Files

Every QBCore resource needs this basic structure:

your-resource/ ├── fxmanifest.lua # Resource manifest ├── server/ │ └── main.lua # Server-side logic ├── client/ │ └── main.lua # Client-side logic ├── shared/ │ └── config.lua # Shared configuration └── README.md # Documentation

Advanced Resource Structure

For complex resources, use this extended structure:

your-resource/ ├── fxmanifest.lua ├── server/ │ ├── main.lua │ ├── events.lua # Server events │ ├── functions.lua # Server functions │ ├── callbacks.lua # Server callbacks │ └── commands.lua # Server commands ├── client/ │ ├── main.lua │ ├── events.lua # Client events │ ├── functions.lua # Client functions │ ├── menu.lua # Menu systems │ └── ui.lua # UI interactions ├── shared/ │ ├── config.lua # Configuration │ ├── locale.lua # Localization │ └── utils.lua # Shared utilities ├── html/ # UI files │ ├── index.html │ ├── style.css │ └── script.js ├── locales/ # Language files │ ├── en.lua │ └── es.lua └── sql/ # Database files └── install.sql

Creating the Manifest

The fxmanifest.lua file defines your resource:

fx_version 'cerulean' game 'gta5' author 'Your Name' description 'Custom QBCore Resource' version '1.0.0' -- Dependencies dependencies { 'qb-core', 'qb-target', -- If using target system 'qb-menu' -- If using menu system } -- Shared files shared_scripts { 'shared/config.lua', 'shared/locale.lua' } -- Client files client_scripts { 'client/main.lua', 'client/events.lua', 'client/functions.lua' } -- Server files server_scripts { 'server/main.lua', 'server/events.lua', 'server/functions.lua' } -- UI files (if needed) ui_page 'html/index.html' files { 'html/index.html', 'html/style.css', 'html/script.js' } -- Exports exports { 'GetResourceData', 'IsPlayerEligible' } server_exports { 'CreateCustomJob', 'ProcessPayment' } lua54 'yes'

Basic Implementation

1. Server-Side Setup

-- server/main.lua local QBCore = exports['qb-core']:GetCoreObject() -- Resource initialization CreateThread(function() print('^2[' .. GetCurrentResourceName() .. ']^7 Resource started successfully') -- Initialize database tables if needed InitializeDatabase() -- Load existing data LoadResourceData() end) -- Core functions function InitializeDatabase() -- Create tables if they don't exist MySQL.query([[ CREATE TABLE IF NOT EXISTS custom_resource_data ( id INT AUTO_INCREMENT PRIMARY KEY, citizenid VARCHAR(50) NOT NULL, data LONGTEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ) ]]) end function LoadResourceData() -- Load any persistent data your resource needs local result = MySQL.query.await('SELECT * FROM custom_resource_data') for i = 1, #result do -- Process loaded data ProcessLoadedData(result[i]) end end -- Player joined handler RegisterNetEvent('QBCore:Client:OnPlayerLoaded', function() local src = source local player = QBCore.Functions.GetPlayer(src) if player then -- Initialize player data for your resource InitializePlayerData(src, player) end end) function InitializePlayerData(source, player) -- Setup player-specific data TriggerClientEvent('your-resource:client:initialize', source, { citizenid = player.PlayerData.citizenid, permissions = GetPlayerPermissions(player), settings = GetPlayerSettings(player.PlayerData.citizenid) }) end

2. Client-Side Setup

-- client/main.lua local QBCore = exports['qb-core']:GetCoreObject() local PlayerData = QBCore.Functions.GetPlayerData() -- Resource initialization RegisterNetEvent('QBCore:Client:OnPlayerLoaded', function() PlayerData = QBCore.Functions.GetPlayerData() InitializeClientSide() end) RegisterNetEvent('QBCore:Client:OnPlayerUnload', function() PlayerData = {} CleanupClientSide() end) function InitializeClientSide() -- Setup client-side functionality CreateThread(function() while PlayerData do -- Main client loop Wait(1000) -- Check conditions, update UI, etc. UpdateClientState() end end) end function CleanupClientSide() -- Clean up client-side resources -- Remove blips, NPCs, objects, etc. end -- Resource-specific initialization RegisterNetEvent('your-resource:client:initialize', function(data) print('Initializing custom resource for player:', data.citizenid) -- Setup player-specific client data SetupPlayerInterface(data) CreateResourceBlips() SpawnResourceNPCs() end)

3. Configuration System

-- shared/config.lua Config = {} -- General Settings Config.Debug = false Config.Locale = 'en' -- Feature Toggles Config.EnableNotifications = true Config.EnableBlips = true Config.EnableNPCs = true -- Locations Config.Locations = { main_office = { coords = vector3(100.0, 200.0, 25.0), heading = 90.0, blip = { sprite = 280, color = 2, scale = 0.8, label = "Custom Resource Office" } } } -- Job Configuration Config.Jobs = { allowed_jobs = {"police", "ambulance", "mechanic"}, payment_range = {min = 500, max = 1500}, experience_multiplier = 1.0 } -- Items and Rewards Config.Items = { required_items = {"id_card", "phone"}, reward_items = { {item = "money", amount = {min = 100, max = 500}}, {item = "custom_item", amount = 1} } } -- Timing Configuration Config.Timers = { cooldown_time = 300, -- 5 minutes process_time = 60, -- 1 minute reset_time = 86400 -- 24 hours }

Event System Integration

Server Events

-- server/events.lua -- Custom resource events RegisterNetEvent('your-resource:server:processAction', function(actionType, data) local src = source local player = QBCore.Functions.GetPlayer(src) if not player then return end -- Validate the action if not ValidateAction(player, actionType, data) then TriggerClientEvent('QBCore:Notify', src, 'Action not allowed', 'error') return end -- Process the action local result = ProcessPlayerAction(player, actionType, data) if result.success then -- Update player data UpdatePlayerProgress(player, result.progress) -- Send success response TriggerClientEvent('your-resource:client:actionComplete', src, result) TriggerClientEvent('QBCore:Notify', src, result.message, 'success') else TriggerClientEvent('QBCore:Notify', src, result.error, 'error') end end) -- Integration with other resources RegisterNetEvent('qb-phone:server:sendMessage', function(phoneNumber, message) -- Handle phone integration if message:find("#customresource") then ProcessPhoneCommand(source, message) end end) function ValidateAction(player, actionType, data) -- Check player permissions if not HasRequiredJob(player) then return false end -- Check cooldowns if IsPlayerOnCooldown(player.PlayerData.citizenid, actionType) then return false end -- Check required items if not HasRequiredItems(player, actionType) then return false end return true end

Client Events

-- client/events.lua -- Handle server responses RegisterNetEvent('your-resource:client:actionComplete', function(result) -- Update UI UpdateProgressBar(result.progress) -- Play effects if result.effects then PlayEffects(result.effects) end -- Update local data UpdateLocalPlayerData(result.playerData) end) -- Integration events RegisterNetEvent('your-resource:client:receivePhoneMessage', function(sender, message) -- Handle phone notifications TriggerEvent('qb-phone:client:addMessage', { sender = sender, message = message, time = os.date('%H:%M') }) end) RegisterNetEvent('your-resource:client:showNotification', function(data) -- Custom notification system if Config.EnableNotifications then ShowCustomNotification(data.title, data.message, data.type) end end)

Database Integration

Setting Up Tables

-- sql/install.sql CREATE TABLE IF NOT EXISTS `custom_resource_players` ( `id` INT(11) NOT NULL AUTO_INCREMENT, `citizenid` VARCHAR(50) NOT NULL, `level` INT(11) DEFAULT 1, `experience` INT(11) DEFAULT 0, `settings` LONGTEXT DEFAULT '{}', `statistics` LONGTEXT DEFAULT '{}', `last_active` TIMESTAMP DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `citizenid` (`citizenid`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE IF NOT EXISTS `custom_resource_actions` ( `id` INT(11) NOT NULL AUTO_INCREMENT, `citizenid` VARCHAR(50) NOT NULL, `action_type` VARCHAR(50) NOT NULL, `action_data` LONGTEXT DEFAULT '{}', `timestamp` TIMESTAMP DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `citizenid` (`citizenid`), KEY `action_type` (`action_type`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

Database Functions

-- server/functions.lua function GetPlayerResourceData(citizenid) local result = MySQL.query.await('SELECT * FROM custom_resource_players WHERE citizenid = ?', {citizenid}) if result[1] then return { level = result[1].level, experience = result[1].experience, settings = json.decode(result[1].settings), statistics = json.decode(result[1].statistics) } end return CreateDefaultPlayerData(citizenid) end function UpdatePlayerResourceData(citizenid, data) MySQL.insert('INSERT INTO custom_resource_players (citizenid, level, experience, settings, statistics) VALUES (?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE level = VALUES(level), experience = VALUES(experience), settings = VALUES(settings), statistics = VALUES(statistics), last_active = CURRENT_TIMESTAMP', { citizenid, data.level, data.experience, json.encode(data.settings), json.encode(data.statistics) }) end function LogPlayerAction(citizenid, actionType, actionData) MySQL.insert('INSERT INTO custom_resource_actions (citizenid, action_type, action_data) VALUES (?, ?, ?)', { citizenid, actionType, json.encode(actionData) }) end

Testing Your Resource

Development Testing

-- Add debug commands for testing if Config.Debug then RegisterCommand('test-resource', function(source, args) local player = QBCore.Functions.GetPlayer(source) if player then -- Test your resource functionality TriggerEvent('your-resource:server:processAction', 'test', { type = args[1] or 'default' }) end end, false) RegisterCommand('reset-resource-data', function(source, args) local player = QBCore.Functions.GetPlayer(source) if player then ResetPlayerData(player.PlayerData.citizenid) TriggerClientEvent('QBCore:Notify', source, 'Resource data reset', 'info') end end, false) end

Quality Assurance Checklist

  • Performance Testing: Resource uses minimal server resources
  • Error Handling: All functions have proper error handling
  • Database Validation: All database operations are validated
  • Client-Server Sync: Data synchronization works correctly
  • Integration Testing: Works with other QBCore resources
  • Security Validation: No exploitable endpoints
  • Documentation: Code is well-documented

Best Practices

Code Organization

  1. Modular Structure: Split functionality into logical files
  2. Clear Naming: Use descriptive function and variable names
  3. Error Handling: Always handle potential errors gracefully
  4. Performance: Optimize loops and database queries
  5. Security: Validate all user inputs and server events

Resource Integration

  1. Event Naming: Use consistent event naming patterns
  2. Data Validation: Always validate data before processing
  3. Cleanup: Properly clean up resources on player disconnect
  4. Configuration: Make features configurable when possible
  5. Backwards Compatibility: Consider version compatibility

Remember to thoroughly test your custom resource in a development environment before deploying to production servers.

Common Patterns

Interaction Systems

-- Using qb-target for interactions exports['qb-target']:AddBoxZone("custom-resource-interaction", vector3(100.0, 200.0, 25.0), 2.0, 2.0, { name = "custom-resource-interaction", heading = 90.0, debugPoly = Config.Debug, minZ = 24.0, maxZ = 26.0, }, { options = { { type = "client", event = "your-resource:client:openMenu", icon = "fas fa-hand", label = "Open Custom Menu", canInteract = function() return HasRequiredJob() end, }, }, distance = 2.5 })
-- Using qb-menu for interfaces RegisterNetEvent('your-resource:client:openMenu', function() local menuOptions = { { header = "Custom Resource Menu", isMenuHeader = true, }, { header = "Option 1", txt = "Description of option 1", params = { event = "your-resource:client:option1", args = {type = "option1"} } }, { header = "Option 2", txt = "Description of option 2", params = { event = "your-resource:client:option2", args = {type = "option2"} } } } exports['qb-menu']:openMenu(menuOptions) end)

This guide provides a solid foundation for creating professional QBCore custom resources. Continue with specific implementation details based on your resource’s requirements.

Last updated on