A practical guide to how to add a weapon shop to your qbcore fivem server, with implementation decisions, validation steps, and security considerations for a production-minded project.
Introduction
Stellar AI can help generate and revise the project files for this workflow. Describe your framework, existing dependencies, and one testable feature, then bring back errors for a focused revision. You remain responsible for running the tests and managing deployment in your own development environment.
Architecture overview
Design the weapon shop as a small set of responsibilities split across client and server: UI client code handles input and displays prices; server code validates purchases, modifies player data, logs transactions, and persists inventory changes. Never trust client data. The server is authoritative for money, inventory, permissions, and logging.
A simple architecture:
- Client UI: shows shop catalog, local animations, triggers server callbacks/events.
- Server API: callbacks to check affordability and permissions; events to perform purchases and log actions.
- Persistence layer: writes purchases to player inventories (ox_inventory / qb-inventory / custom DB) and logs to a transactions table.
- Optional: admin panel and webhooks for audit logs.
Data model and database considerations
Keep the weapon catalog in a centrally maintained Lua table or DB table that the server loads on startup. Store only canonical identifiers (e.g., "weapon_pistol", item names) and server-side price/requirement metadata. Avoid letting clients choose arbitrary identifiers.
Example catalog structure (server-side):
local WeaponCatalog = {
pistol = {hash = "weapon_pistol", price = 2500, ammo = 50, job = nil},
smg = {hash = "weapon_smg", price = 7500, ammo = 120, job = "police"}
}
Persistence options:
- Use your server inventory system (qb-inventory, ox_inventory) to add items. This ensures consistency with existing inventory UIs.
- Log transactions in a dedicated SQL table for audits and troubleshooting: player identifier, weapon id, price, timestamp, transaction id.
- If using MySQL, wrap money removal and inventory addition in a deterministic server-side sequence and log failures.
Server-side security and validation
Security checklist (enforced server-side only):
- Validate weapon identifier against the trusted catalog.
- Check player existence and load latest player data (money, job, whitelist flags).
- Verify payment (Remove money or deduct bank balance) and verify the function returns true before granting item.
- Enforce job or whitelist restrictions on the server; do not rely on client checks.
- Sanitize and validate metadata (ammo counts, attachments) and reject unexpected fields.
- Log every successful and failed attempt for auditing.
All client-initiated operations should call server callbacks or events. Use QBCore.Functions.CreateCallback for pre-purchase checks, and RegisterNetEvent for finalization with server-side validation of the same checks.
QBCore implementation example
Below is a compact, practical pattern using QBCore conventions. Adapt function names to match your QBCore version and inventory resource.
-- server/main.lua (server-side)
local QBCore = exports['qb-core']:GetCoreObject()
local WeaponCatalog = {
pistol = {item = "weapon_pistol", price = 2500, ammo = 50},
smg = {item = "weapon_smg", price = 7500, ammo = 120}
}
QBCore.Functions.CreateCallback('weaponshop:canBuy', function(source, cb, weaponKey)
local src = source
local Player = QBCore.Functions.GetPlayer(src)
if not Player then return cb(false, 'no_player') end
local entry = WeaponCatalog[weaponKey]
if not entry then return cb(false, 'invalid_weapon') end
local balance = Player.PlayerData.money['cash'] or 0
if balance < entry.price then return cb(false, 'insufficient_funds') end
-- Example job check (optional)
if entry.job and Player.PlayerData.job.name ~= entry.job then
return cb(false, 'job_restricted')
end
cb(true, entry.price)
end)
RegisterNetEvent('weaponshop:buy', function(weaponKey)
local src = source
local Player = QBCore.Functions.GetPlayer(src)
if not Player then return end
local entry = WeaponCatalog[weaponKey]
if not entry then
TriggerClientEvent('QBCore:Notify', src, 'Invalid weapon', 'error')
return
end
-- re-check server-side
if (Player.PlayerData.money['cash'] or 0) < entry.price then
TriggerClientEvent('QBCore:Notify', src, 'Not enough money', 'error')
return
end
-- remove money, then add item atomically in code flow
if not Player.Functions.RemoveMoney('cash', entry.price) then
TriggerClientEvent('QBCore:Notify', src, 'Payment failed', 'error')
return
end
Player.Functions.AddItem(entry.item, 1, false, { ammo = entry.ammo })
-- log to DB or external logger here
TriggerClientEvent('QBCore:Notify', src, 'Purchase successful', 'success')
end)
Client-side usage (invoke callback then event):
-- client/main.lua (client-side)
local QBCore = exports['qb-core']:GetCoreObject()
function AttemptBuy(weaponKey)
QBCore.Functions.TriggerCallback('weaponshop:canBuy', function(success, info)
if not success then
QBCore.Functions.Notify(info, 'error')
return
end
-- Show confirmation UI to the player then:
TriggerServerEvent('weaponshop:buy', weaponKey)
end, weaponKey)
end
Notes: adjust Player.Functions.AddItem/AddWeapon to your inventory or weapon resource. Many servers use qb-inventory so adding an item there keeps UI consistent.
Logging and atomicity
Ensure operations that affect money and inventory are performed together in the server flow. If your persistence uses asynchronous DB calls, perform server-side state changes first, then persist the log; if persistence fails, reconciliate via a periodic audit that checks player inventories vs transaction logs.
Integrating with ox_lib / ox_inventory and ESX
ox_inventory and ox_lib provide performance and UX benefits. Integration notes:
- Use the inventory's AddItem export instead of directly modifying DB tables.
- ox_inventory will often handle attachment metadata; pass { ammo = X } as item metadata when adding a weapon entry.
- If you use ESX, replace QBCore player interactions with ESX.GetPlayerFromId and xPlayer.removeMoney/addInventoryItem. Maintain the same server-side validation flow.
Example integration pattern: check funds via server callback, call xPlayer.removeMoney / Player.Functions.RemoveMoney, then call the inventory export to add the item. Always check return values from exports and revert or log if failures occur.
Client UI and safety considerations
Client responsibilities:
- Render the catalog and prices fetched from the server or embedded in the client as read-only. Prefer fetching the catalog from the server at UI open to reflect dynamic pricing.
- Only call server callbacks or events to request checks/purchase. Do not perform any client-side money subtraction or inventory modifications locally.
- Provide clear error messages from server callback results.
Example client flow:
- Player opens shop -> client requests catalog and current player limits (server callback).
- Client displays UI and confirmation prompt.
- On confirm, client triggers server event to finalize purchase; server validates and responds via notification.
Testing and debugging
Strategy for robust testing:
- Unit test server callbacks: simulate GetPlayer returning mock data to ensure all validation branches behave as expected.
- Integration test: simulate networked purchase from client to server and assert database/inventory state changes and logs.
- Edge cases: insufficient funds, malformed identifiers, DB failures, duplicate requests, concurrent purchases (race conditions).
- Penetration checks: attempt to call the buy event with forged weapon keys and confirm server rejects them.
Stellar AI can help generate and revise the project files for this workflow. Describe your framework, existing dependencies, and one testable feature, then bring back errors for a focused revision. You remain responsible for running the tests and managing deployment in your own development environment.
Roblox-specific note
If you maintain a similar weapon shop concept in Roblox, preserve server authority using RemoteEvents/RemoteFunctions carefully: the server must own the final checks and DataStore writes. Handle DataStore failures by retrying with exponential backoff, and consider queueing writes to avoid hitting limits. For RemoteEvents, validate all incoming parameters and do not trust client-sent prices or identifiers.
Deployment and maintenance
Deployment checklist before going live:
- Run load tests or simulated purchases to validate DB and inventory throughput.
- Ensure all logging (successful and failed attempts) is persisted for audits.
- Apply rate-limiting on purchase attempts to avoid abuse via rapid repeated events.
- Document required configuration (weapon catalog, prices, job restrictions) in a single server file for easy updates.
Post-deployment: monitor server logs for anomalies and run daily reconciliation jobs comparing inventory tables with transaction logs. If you automate backups and versioned deployments, include rollback steps and database migration scripts.
Practical checklist (quick reference)
| Task | Server-side | Status |
|---|---|---|
| Validate weapon identifier | Compare incoming key against WeaponCatalog | Required |
| Verify funds & remove | Get player, remove money, confirm success | Required |
| Add inventory entry | Call Player.Functions.AddItem or inventory export | Required |
| Log transaction | Insert into transactions table | Strongly recommended |
| Restrict by job/whitelist | Server-side job check | Optional |
| Rate limiting | Throttle purchase attempts per player | Recommended |
Maintenance tips
Keep the weapon catalog and pricing in source control. Announce shop changes to players in downtime windows. Implement feature flags for experimental weapons so you can turn them off without a full deploy. Periodically review logs for abuse patterns and adjust rate limits or add further anti-cheat checks.