A practical guide to how to add a fishing job to your qbcore fivem server, with implementation decisions, validation steps, and security considerations for a production-minded project.
Overview
Adding a robust fishing job to a QBCore FiveM server means more than drawing bait and spawning fish. You need a clear architecture split between client visuals and server authority, secure server-side validation for rewards and inventory changes, a clean resource layout, and a plan for testing, deployment, and maintenance. This guide walks through architecture choices, concrete implementation patterns, security considerations (never trust client input), integration options (ox_lib, ox_inventory, ESX equivalents), and practical deployment steps.
Architecture and responsibilities
Design the fishing job as a modular resource that separates responsibilities:
- Client: UI, animations, local minigame, input and camera effects, hit detection for local visuals. The client may request actions but does not grant rewards directly.
- Server: authoritative checks, issuing items/money, storing sessions/tokens, database writes (selling, processing), cooldowns and rate-limits, anti-cheat decisions.
- Database: fish tables for prices, player job state (if persistent), migration scripts for items and job records.
- Optional integration: ox_lib for menus, ox_inventory for item handling or QBCore inventory API, reusable job entries in qb-core shared files if you want job appearing in job lists.
Resource structure and fxmanifest
A clear file layout keeps the resource maintainable. Example minimal layout:
qb-fishing/
fxmanifest.lua
client/
main.lua
ui.lua
server/
main.lua
handlers.lua
config.lua
sql/
migrations.sql
Example fxmanifest snippet:
fx_version 'cerulean'
game 'gta5'
author 'YourName'
description 'QBCore fishing job'
version '1.0.0'
shared_script 'config.lua'
client_scripts {
'client/main.lua',
'client/ui.lua'
}
server_scripts {
'@oxmysql/lib/MySQL.lua', -- if using oxmysql
'server/main.lua',
'server/handlers.lua'
}
Server validation, tokens and security
Never trust client-provided coordinates, counts, or timestamps. A reliable pattern is a short-lived server-issued token that authorizes a fishing session. This prevents clients from spoofing remote actions because only the server knows valid tokens and expiration times.
Pattern:
- Client enters fishing zone and sends a request to start a session: TriggerServerEvent('qb-fishing:server:requestSession', zoneId).
- Server checks job permissions, cooldowns, and optionally that the player is allowed to fish now. If OK, server generates a token and stores it in memory keyed by source with an expiration timestamp.
- Client can request catches using the token: TriggerServerEvent('qb-fishing:server:attemptCatch', token).
- Server validates the token and expiration server-side. Only after validation does the server add items/money to the player and optionally update database records.
This pattern avoids trusting any client-submitted coordinates and centralizes reward decisions on the server. Implement rate-limiting and anti-spam checks (per-minute catch caps) and log suspicious behavior for monitoring.
Server-side token example (Lua)
local QBCore = exports['qb-core']:GetCoreObject()
local sessions = {} -- [source] = {token=xxxx, expires=os.time()+60}
RegisterNetEvent('qb-fishing:server:requestSession', function(zoneId)
local src = source
local Player = QBCore.Functions.GetPlayer(src)
if not Player then return end
-- Permission checks
if Player.PlayerData.job.name == 'police' then return end -- example
local token = tostring(math.random(100000,999999))
sessions[src] = { token = token, expires = os.time() + 60, zone = zoneId }
TriggerClientEvent('qb-fishing:client:sessionStarted', src, token)
end)
RegisterNetEvent('qb-fishing:server:attemptCatch', function(token)
local src = source
local Player = QBCore.Functions.GetPlayer(src)
local s = sessions[src]
if not s or s.token ~= token or os.time() > s.expires then
-- invalid or expired
TriggerClientEvent('qb-fishing:client:sessionDenied', src)
return
end
-- Rate limiting example
if Player.PlayerData.metadata.fishCooldown and (os.time() - Player.PlayerData.metadata.fishCooldown) < 2 then
return
end
Player.Functions.AddItem('fish_small', 1)
Player.Functions.RemoveMoney('cash', 5) -- example processing cost or fees if required
Player.Functions.UpdateMetaData('fishCooldown', os.time())
end)
Item economy, processing and DB schema
Decide whether fish are raw items that need processing or direct sell items. Example flow:
- Catch raw fish (items: fish_small, fish_medium, fish_large).
- Take to processing location to transform raw fish to filet or canned goods (server processes inventory changes).
- Sell processed goods at a vendor NPC or sellpoint. Transactions occur server-side.
SQL migration sample (for oxmysql/ghmattimysql):
-- sql/migrations.sql
INSERT INTO items (name, label, weight, rare, usable, description) VALUES
('fish_small', 'Small Fish', 200, 0, 0, 'A small fish.'),
('fish_medium', 'Medium Fish', 350, 0, 0, 'A medium fish.'),
('fish_large', 'Large Fish', 600, 1, 0, 'A large fish.');
Implementation choices: minigames, animations, and integrations
Choices to match playstyle:
- Minigame: Simple timed progress bar (client) vs skill-based minigame (mini-UI). Keep minigame on client for smooth UX but require a server-issued session token to accept rewards.
- Inventory: Use QBCore.Functions for default inventory operations. If using ox_inventory, adapt AddItem/RemoveItem calls to that API. Do all item granting on server.
- Context menus and interaction: ox_lib context menus provide polished interaction points for starting/stopping job actions and selling. They are client components only for UI—authorization still runs server-side.
- Animations and synchronisation: Trigger local animations for visual feedback. Avoid synchronizing server position; let client animate, and validate actions via token-checks and cooldowns.
Code patterns: robust server handlers
Keep server code modular and maintain authority. Use dedicated handler functions, centralize economy changes, and emit logs on important operations:
local function giveFish(player, fishType, amount)
-- additional validations: inventory space, daily caps, etc.
player.Functions.AddItem(fishType, amount)
-- log in server logs for audits
print(('[fishing] %s received %s x%d'):format(player.PlayerData.citizenid, fishType, amount))
end
-- call giveFish after token validation (see previous token snippet)
Testing and QA
Testing should cover edge cases where clients try to bypass flows. Use these steps:
- Unit test server handlers locally with multiple simulated sources to check session expiration and cleanup.
- Playtest with two-player scenarios: one normal, one attempting to spam catch events to verify rate-limits and anti-cheat triggers.
- Test DB migrations on a staging server with a backup plan for rolling back in production.
- Verify inventory boundaries: item stacking, weight caps, and sellpoint behavior with zero inventory cases.
- Simulate missing dependencies (oxmysql down, inventory unavailable) and confirm the server handles failures gracefully and sends descriptive errors to admins/logs.
Deployment and maintenance
Deployment checklist:
| Step | What | Responsible |
|---|---|---|
| Install resource | Place qb-fishing in resources, add start qb-fishing to server.cfg | Dev/Ops |
| Run migrations | Apply sql/migrations.sql using your DB tool (oxmysql/ghmattimysql) | DB Admin |
| Configure | Edit config.lua for zones, prices, job names | Developer |
| Monitor | Check logs for token errors, failed DB calls, and suspicious activity | Ops |
Use a deployment or monitoring tool to track resource health. If you use a managed platform for resource deployment and logs, integrate server logs and alerts. For continuous improvement, keep configuration separated from code and use feature flags to enable/disable new game mechanics without redeploying code.
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.
Maintenance: versioning, backups, and cleanup
Maintenance tasks include:
- Keep resource versioned and tag releases. Document config changes.
- Back up databases before applying migrations or changing item definitions.
- Add server-side garbage collection for expired tokens/sessions. Example: periodic loop that clears sessions table and writes audit logs.
- Rotate logs and keep an incident response plan in place for exploits or item dupes.
- Periodically review and update permissions for jobs and salary settings in qb-core shared definitions to align with economy balance.
If you want a hosted interface for deployments and rapid rollback capabilities consider integrating your deployment pipeline with staging and production rollouts; you can connect and monitor via a platform such as https://trystellarai.com/app for centralized operations.
Roblox/Luau considerations (if cross-platform inspiration)
If you adapt patterns for Roblox: always treat the server as authoritative. Use RemoteEvents/RemoteFunctions only to request actions; have the server validate RemoteEvent calls and handle DataStore writes with retry/backoff and failure handling. Avoid putting any item granting logic on the client. Implement server-side cooldowns and store session state on the server. Handle DataStore failures by queuing and retrying writes and informing players of transient issues.
Checklist: minimum requirements before launch
- Server-side session token system implemented and tested.
- Rate limits and cooldowns on catches to prevent rapid farming.
- Inventory operations only on server: AddItem/RemoveItem controlled by server handlers.
- SQL migrations applied and tested on staging.
- Error handling for DB downtime and graceful failures.
- Logging for suspicious behavior and audit trails for item grants.
- Deployment plan with rollback and backups.
- Automated cleanup of expired sessions and stale data.