A practical guide to how to add a crafting system to your qbcore fivem server, with implementation decisions, validation steps, and security considerations for a production-minded project.
Overview
This guide walks through adding a robust, server-authoritative crafting system to a QBCore-based FiveM server and outlines parallel concerns for Roblox/Luau. You will learn architectural patterns, item and recipe modeling, validation and anti-cheat measures, integration options (ESX/ox_lib considerations), how to test safely, and how to deploy and maintain the system. If you want to prototype or coordinate tasks with designers and developers, consider using the Stellar AI collaboration tool: https://trystellarai.com/app.
Design and Architecture
Crafting systems are stateful: they change player inventories, consume items, produce items, and may trigger cooldowns or skill checks. The most important architectural principle is server authority. The server must be the only component that confirms recipe success, updates inventory, and persists results.
Core components:
- Item database: canonical definitions for item IDs, stack size, metadata, and crafting flags.
- Recipe registry: a server-side table of recipes mapping ingredients to results, cost, and optional metadata (time, skill checks, chance).
- Server validation layer: checks availability, concurrency locks, cooldowns, and anti-duplication safeguards.
- Client UI: displays recipes and submits craft requests, but never performs final validation.
- Persistence/storage: inventory database or player items storage (QBCore SQL, ESX DB, Roblox DataStore).
Choice considerations: QBCore, ESX, ox_lib
QBCore provides utilities like QBCore.Functions.GetPlayer and inventory helpers that simplify server-side work. ESX has its own player object patterns. ox_lib is a client-side helper library (menus, contexts). When using ox_lib for the client UI, ensure it only communicates with the server for authoritative operations. Architect so that the server can operate independently of the client UI resource.
Data Model and Recipe Management
Model recipes as immutable server-side records. A minimal recipe entry:
{
id = "stone_pickaxe",
ingredients = { {item = "wood", amount = 2}, {item = "stone", amount = 3} },
result = { item = "stone_pickaxe", amount = 1 },
craftingTime = 5000, -- milliseconds
chance = 1.0 -- 0.0-1.0 optional
}
Store items and recipes in a single shared Lua table on the server and expose read-only callbacks to clients for UI rendering. Avoid sending mutable recipe objects to the client; send only read-only metadata and localized strings.
Server Validation and Security
Never trust client inputs. All craft requests must be validated by the server using these checks:
- Existence: recipe ID exists in the server-side registry.
- Inventory verification: the server checks the player's inventory for required quantities using canonical inventory APIs (QBCore.Functions.GetPlayer, ESX.GetPlayerFromId).
- Concurrency: lock player inventory (or use transaction semantics) to prevent race conditions or double-spend.
- Cooldowns and rate limits: server-enforced per-player cooldown or craft queue to prevent farm exploits.
- Randomization and fairness: compute any chance/hit rate server-side using a secure RNG; do not rely on client RNG.
- Logging and auditing: log craft events for anomaly detection (item ID, player ID, timestamp).
Example anti-cheat pattern: when a craft request arrives, immediately acquire a server-side lock (e.g., a per-player boolean busy flag). If locked, reject. After validation and applying inventory changes atomically, release the lock. Use database transactions where possible for persistence.
Implementation Patterns
Below are concise implementation examples showing server-side validation for FiveM (QBCore) and for Roblox (Luau). The goal is to demonstrate correct authority flow.
QBCore (FiveM) - Server-side event
-- server.lua (FiveM, QBCore)
RegisterNetEvent('mycraft:requestCraft', function(recipeId)
local src = source
local Player = QBCore.Functions.GetPlayer(src)
if not Player then return end
-- simple per-player lock
if Player.metadata.isCrafting then
TriggerClientEvent('mycraft:craftResult', src, false, "busy")
return
end
Player.metadata.isCrafting = true
local recipe = Recipes[recipeId]
if not recipe then
Player.metadata.isCrafting = false
TriggerClientEvent('mycraft:craftResult', src, false, "invalid_recipe")
return
end
-- Validate inventory
for _, ing in ipairs(recipe.ingredients) do
if Player.Functions.GetItemByName(ing.item) == nil or Player.Functions.GetItemByName(ing.item).amount < ing.amount then
Player.metadata.isCrafting = false
TriggerClientEvent('mycraft:craftResult', src, false, "missing_ingredients")
return
end
end
-- Remove ingredients atomically then add result
for _, ing in ipairs(recipe.ingredients) do
Player.Functions.RemoveItem(ing.item, ing.amount)
end
-- Optionally delay for craftingTime, but state must remain server-authoritative
Citizen.SetTimeout(recipe.craftingTime, function()
Player.Functions.AddItem(recipe.result.item, recipe.result.amount)
Player.metadata.isCrafting = false
TriggerClientEvent('mycraft:craftResult', src, true, "success")
end)
end)
Key points: all inventory checks and changes happen server-side. The client only requests craft and receives the result.
Roblox (Luau) - Server-authoritative RemoteEvent
-- ServerScript in ServerScriptService
local RemoteEvent = game.ReplicatedStorage:WaitForChild("CraftRequest")
local DataStoreService = game:GetService("DataStoreService")
local playerInventoryStore = DataStoreService:GetDataStore("PlayerInventory")
RemoteEvent.OnServerEvent:Connect(function(player, recipeId)
-- Validate input types
if type(recipeId) ~= "string" then
RemoteEvent:FireClient(player, false, "invalid_input")
return
end
local recipe = Recipes[recipeId]
if not recipe then
RemoteEvent:FireClient(player, false, "invalid_recipe")
return
end
-- Obtain player's inventory server-side (cache or DataStore)
local ok, inventory = pcall(function()
return playerInventoryStore:GetAsync(player.UserId)
end)
if not ok then
RemoteEvent:FireClient(player, false, "datastore_error")
return
end
-- Validate ingredients
for _, ing in ipairs(recipe.ingredients) do
if (inventory[ing.item] or 0) < ing.amount then
RemoteEvent:FireClient(player, false, "missing_ingredients")
return
end
end
-- Deduct and write back with pcall + UpdateAsync pattern and retry
local success = false
local attempts = 0
while not success and attempts < 5 do
attempts = attempts + 1
local updateOk, newInv = pcall(function()
return playerInventoryStore:UpdateAsync(tostring(player.UserId), function(old)
old = old or inventory
for _, ing in ipairs(recipe.ingredients) do
old[ing.item] = (old[ing.item] or 0) - ing.amount
end
old[recipe.result.item] = (old[recipe.result.item] or 0) + recipe.result.amount
return old
end)
end)
if updateOk then
success = true
else
wait(2 ^ attempts * 0.1) -- exponential backoff
end
end
if success then
RemoteEvent:FireClient(player, true, "success")
else
RemoteEvent:FireClient(player, false, "save_failed")
end
end)
UI, Feedback, and Crafting Stations
Client UI should be purely presentational: show recipes, amounts, and enable the "craft" button which sends the request. If using ox_lib for contextual menus or progress UI, ensure progress bars are cosmetic: treat them as indicators of server progress, not as authoritative timers. For visible feedback to the player, use server push events (TriggerClientEvent/RemoteEvent:FireClient) when the server changes player state.
For stationary crafting stations (workbenches), spawn them server-side and check proximity on the server or validate a token the client sends that proves the player is near a station (e.g., server tracks position or uses zone flags). Never accept a craft simply because the client says "I am at bench X".
Testing and Quality Assurance
Testing must verify race conditions, persistence failures, and exploits. Recommended tests:
- Unit test recipe validation logic (server-only functions).
- Simulate concurrent craft requests for same player to ensure locks prevent double-spend.
- Simulate disconnects during crafting and verify inventory integrity.
- Test datastore/database failures and recovery flows (for Roblox DataStore or SQL reconnects).
- Run load tests for automated crafting farms and implement rate-limits accordingly.
Track suspicious patterns with logs and create automated alerts for sudden inventory creation spikes. Publish a concise bug report template and use it during testing cycles; if you use collaborative tooling, you can centralize artifact tracking with tools like https://trystellarai.com/app. For design reads and implementation notes consider writing developer posts on your team blog: https://trystellarai.com/blog.
Deployment and Maintenance
Roll out changes carefully. Recommended deployment approach:
- Feature-flag new recipes and crafting logic on the server to enable quick rollback.
- Deploy server-side code first. Clients should gracefully accept server changes (e.g., missing recipe IDs ignored).
- Monitor logs for unexpected item creation and increased datastore errors.
- Use database migrations for any persistent schema changes; back up inventory tables prior to large updates.
Maintenance checklist (short version):
| Task | Frequency | Responsible |
|---|---|---|
| Audit recipe integrity and balance | Monthly | Game Designer |
| Inspect server logs for anomalies | Daily | SRE / DevOps |
| Backup inventories and database | Daily | DB Admin |
| Security review of RPC/Remote events | Quarterly | Security Engineer |
Checklist: Practical Steps to Implement
- Define canonical item IDs and server-side recipe registry.
- Create server-side API for: listRecipes (read-only), requestCraft (server validates and performs changes), craftStatus (optional polling or push notifications).
- Implement per-player locking and rate-limiting.
- Use database transactions or UpdateAsync/persistent atomic patterns to avoid dupes.
- Log craft events and create detection for abnormal creation patterns.
- Gracefully handle persistence failures: report to client, retry with exponential backoff where safe, and avoid silent loss of items.
- Test edge cases: disconnects mid-craft, simultaneous crafts, exploit simulations.
Testing Examples and Verification
Verify the following during QA:
- Try to craft using forged client packets that reference nonexistent items; server should reject.
- Queue multiple craft requests concurrently; server should serialize or reject redundant requests.
- Simulate database downtime and ensure the system fails safely (no item duplication and informative client errors).
- Check that crafting stations and proximity checks are validated server-side.