Weapon Shop Architecture Overview
A weapon shop system on a FiveM roleplay server needs to balance accessibility for law-abiding citizens with realistic restrictions that create gameplay depth. The architecture splits into two distinct branches: legal shops that enforce weapon licensing, background checks, and purchase cooldowns, and illegal black market dealers that sell untraceable weapons at higher prices with no paperwork. Both systems share the same underlying inventory and transaction logic, but differ in their access requirements and the metadata attached to each weapon. Legal weapons get serial numbers linked to the buyer's citizen ID, making them traceable by police, while black market weapons have scratched serials that cannot be traced back to a specific player. This dual-track design creates natural roleplay tension because players must weigh the convenience and lower cost of legal purchases against the anonymity of the black market.
Database Schema for Weapons and Licenses
Your database needs to track weapon inventory per shop, individual weapon serial numbers, player licenses, and purchase history. The serial number system is critical because it bridges the weapon shop and the police investigation systems. Every legally purchased weapon gets a unique serial that police can look up during traffic stops or crime scene investigations. Design the schema to handle both shop stock management and individual weapon tracking:
CREATE TABLE IF NOT EXISTS weapon_shops (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(100) NOT NULL,
type ENUM('legal', 'blackmarket') DEFAULT 'legal',
coords_x FLOAT NOT NULL,
coords_y FLOAT NOT NULL,
coords_z FLOAT NOT NULL,
is_active BOOLEAN DEFAULT TRUE
);
CREATE TABLE IF NOT EXISTS weapon_inventory (
id INT AUTO_INCREMENT PRIMARY KEY,
shop_id INT NOT NULL,
weapon_name VARCHAR(50) NOT NULL,
label VARCHAR(100) NOT NULL,
price INT NOT NULL,
ammo_price INT DEFAULT 0,
category ENUM('handguns', 'smgs', 'rifles', 'shotguns', 'melee', 'throwables') NOT NULL,
license_required ENUM('none', 'basic', 'advanced', 'military') DEFAULT 'none',
stock INT DEFAULT -1,
INDEX idx_shop (shop_id),
FOREIGN KEY (shop_id) REFERENCES weapon_shops(id)
);
CREATE TABLE IF NOT EXISTS weapon_serials (
serial VARCHAR(20) PRIMARY KEY,
weapon_name VARCHAR(50) NOT NULL,
owner_citizenid VARCHAR(50) DEFAULT NULL,
is_scratched BOOLEAN DEFAULT FALSE,
purchased_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
shop_id INT DEFAULT NULL,
INDEX idx_owner (owner_citizenid)
);
CREATE TABLE IF NOT EXISTS weapon_licenses (
id INT AUTO_INCREMENT PRIMARY KEY,
citizenid VARCHAR(50) NOT NULL,
license_type ENUM('basic', 'advanced', 'military') NOT NULL,
issued_by VARCHAR(50) DEFAULT NULL,
issued_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
expires_at TIMESTAMP NULL,
revoked BOOLEAN DEFAULT FALSE,
UNIQUE KEY unique_license (citizenid, license_type)
);
The weapon_serials table tracks every weapon individually through its lifecycle. When a player buys a weapon legally, a serial is generated and linked to their citizen ID. If they scratch the serial using a black market service, the is_scratched flag flips to true and the owner_citizenid is nullified. The stock field in weapon_inventory uses -1 to indicate unlimited stock, while positive values enable limited supply mechanics that create scarcity and drive black market demand.
Legal Weapon Shop Implementation
Legal weapon shops are the primary way most players acquire firearms on a roleplay server. They enforce licensing requirements, apply purchase cooldowns to prevent bulk buying, and generate traceable serial numbers for every weapon sold. The shop UI should display weapons grouped by category with clear indicators showing which license tier each weapon requires. Players without the proper license see the weapon but cannot purchase it, encouraging them to roleplay the licensing process with government officials. Implement a cooldown system that prevents players from buying more than a set number of weapons per real-time day to prevent stockpiling for resale on the black market:
RegisterNetEvent('weaponshop:server:purchase', function(shopId, weaponName)
local src = source
local Player = QBCore.Functions.GetPlayer(src)
if not Player then return end
local citizenid = Player.PlayerData.citizenid
local item = GetShopItem(shopId, weaponName)
if not item then
TriggerClientEvent('QBCore:Notify', src, 'Item not available', 'error')
return
end
-- Check license requirement
if item.license_required ~= 'none' then
local hasLicense = HasValidLicense(citizenid, item.license_required)
if not hasLicense then
TriggerClientEvent('QBCore:Notify', src, 'You need a ' .. item.license_required .. ' weapons license', 'error')
return
end
end
-- Check purchase cooldown
local recentPurchases = GetRecentPurchaseCount(citizenid, 86400) -- last 24h
if recentPurchases >= Config.DailyPurchaseLimit then
TriggerClientEvent('QBCore:Notify', src, 'Daily purchase limit reached', 'error')
return
end
-- Check stock
if item.stock ~= -1 then
if item.stock <= 0 then
TriggerClientEvent('QBCore:Notify', src, 'Out of stock', 'error')
return
end
end
-- Check funds
if Player.PlayerData.money.bank < item.price then
TriggerClientEvent('QBCore:Notify', src, 'Insufficient funds', 'error')
return
end
-- Generate serial number
local serial = GenerateWeaponSerial()
-- Process purchase
Player.Functions.RemoveMoney('bank', item.price, 'weapon-purchase')
Player.Functions.AddItem(weaponName, 1, false, { serial = serial })
-- Record serial
MySQL.insert('INSERT INTO weapon_serials (serial, weapon_name, owner_citizenid, shop_id) VALUES (?, ?, ?, ?)',
{ serial, weaponName, citizenid, shopId })
-- Update stock
if item.stock ~= -1 then
MySQL.update('UPDATE weapon_inventory SET stock = stock - 1 WHERE shop_id = ? AND weapon_name = ?',
{ shopId, weaponName })
end
TriggerClientEvent('QBCore:Notify', src, 'Purchased ' .. item.label .. ' (S/N: ' .. serial .. ')', 'success')
end)
function GenerateWeaponSerial()
local chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
local serial = ''
for i = 1, 3 do
serial = serial .. chars:sub(math.random(#chars), math.random(#chars))
end
serial = serial .. '-'
for i = 1, 5 do
serial = serial .. tostring(math.random(0, 9))
end
return serial
end
Ammunition and Attachment System
Ammunition management adds a resource drain that keeps players engaged with the economy and prevents infinite ammo exploits. Instead of selling ammo as a generic item, tie it to specific caliber types that match weapon categories. A pistol uses 9mm rounds, an SMG might share 9mm or use .45 ACP, and rifles use 5.56 or 7.62 depending on the model. This caliber system creates a more realistic experience and gives shop owners flexibility in pricing different ammo types. Store ammo counts on the weapon metadata rather than as separate inventory items so that each weapon tracks its own loaded rounds. Attachments follow the same metadata pattern where each weapon item stores a list of installed components like scopes, suppressors, extended magazines, and flashlights:
Config.AmmoTypes = {
['ammo_9mm'] = { label = '9mm Rounds', price = 5, amount = 24, weapons = {'WEAPON_PISTOL', 'WEAPON_COMBATPISTOL', 'WEAPON_SMG'} },
['ammo_45acp'] = { label = '.45 ACP Rounds', price = 7, amount = 24, weapons = {'WEAPON_APPISTOL', 'WEAPON_MACHINEPISTOL'} },
['ammo_556'] = { label = '5.56 Rounds', price = 12, amount = 30, weapons = {'WEAPON_CARBINERIFLE', 'WEAPON_ASSAULTRIFLE'} },
['ammo_762'] = { label = '7.62 Rounds', price = 15, amount = 30, weapons = {'WEAPON_SNIPERRIFLE', 'WEAPON_MARKSMANRIFLE'} },
['ammo_12gauge'] = { label = '12ga Shells', price = 10, amount = 8, weapons = {'WEAPON_PUMPSHOTGUN', 'WEAPON_ASSAULTSHOTGUN'} },
}
Config.Attachments = {
['attachment_suppressor'] = {
label = 'Suppressor',
price = 8500,
component = 'COMPONENT_AT_PI_SUPP_02',
compatible = {'WEAPON_PISTOL', 'WEAPON_COMBATPISTOL', 'WEAPON_SMG', 'WEAPON_CARBINERIFLE'},
license_required = 'advanced',
},
['attachment_scope'] = {
label = 'Holographic Scope',
price = 3200,
component = 'COMPONENT_AT_SCOPE_MACRO_02',
compatible = {'WEAPON_SMG', 'WEAPON_CARBINERIFLE', 'WEAPON_ASSAULTRIFLE'},
license_required = 'basic',
},
['attachment_extmag'] = {
label = 'Extended Magazine',
price = 4500,
component = 'COMPONENT_CARBINERIFLE_CLIP_02',
compatible = {'WEAPON_CARBINERIFLE', 'WEAPON_ASSAULTRIFLE', 'WEAPON_SMG'},
license_required = 'basic',
},
['attachment_flashlight'] = {
label = 'Flashlight',
price = 1200,
component = 'COMPONENT_AT_AR_FLSH',
compatible = {'WEAPON_PISTOL', 'WEAPON_COMBATPISTOL', 'WEAPON_CARBINERIFLE', 'WEAPON_PUMPSHOTGUN'},
license_required = 'none',
},
}
When a player purchases an attachment, validate that their current weapon is in the compatible list, check their license tier, then add the component hash to the weapon's metadata table. On weapon equip, iterate through the stored components and apply them with GiveWeaponComponentToPed. This approach ensures attachments persist across sessions and cannot be duplicated by manipulating the client.
Black Market Mechanics
The black market is where the real roleplay depth lives. Unlike static legal shops, black market dealers should feel dangerous and exclusive. Implement rotating dealer locations that change every few hours so players need insider knowledge or contacts to find them. Require a reputation system where new players must build trust with the dealer through smaller purchases before gaining access to high-tier weapons. The black market sells weapons without serial numbers, restricted military-grade equipment that legal shops cannot stock, and services like serial number scratching that make legally purchased weapons untraceable. Price everything 2-3x higher than legal shops to reflect the risk premium and create genuine economic tension between the legal and illegal paths:
Config.BlackMarket = {
rotationInterval = 10800, -- 3 hours between location changes
locations = {
{ coords = vector3(89.98, -1810.87, 24.98), heading = 230.0, label = 'Underground Parking' },
{ coords = vector3(1394.27, 1141.48, 114.33), heading = 90.0, label = 'Desert Warehouse' },
{ coords = vector3(-58.21, 6443.31, 31.43), heading = 45.0, label = 'Paleto Docks' },
{ coords = vector3(981.45, -1812.66, 31.14), heading = 180.0, label = 'Industrial Zone' },
},
reputationTiers = {
[0] = { label = 'Unknown', items = {'WEAPON_KNIFE', 'WEAPON_BAT'} },
[1] = { label = 'Associate', items = {'WEAPON_PISTOL', 'WEAPON_MICROSMG', 'ammo_9mm'} },
[2] = { label = 'Trusted', items = {'WEAPON_SMG', 'WEAPON_PUMPSHOTGUN', 'ammo_45acp', 'ammo_12gauge', 'attachment_suppressor'} },
[3] = { label = 'Inner Circle', items = {'WEAPON_ASSAULTRIFLE', 'WEAPON_CARBINERIFLE', 'ammo_556', 'service_scratch_serial'} },
[4] = { label = 'Arms Dealer', items = {'WEAPON_SNIPERRIFLE', 'WEAPON_RPG', 'WEAPON_GRENADELAUNCHER', 'ammo_762', 'armor_heavy'} },
},
priceMultiplier = 2.5,
reputationGainPerPurchase = 0.15,
}
-- Serial scratching service
RegisterNetEvent('blackmarket:server:scratchSerial', function(weaponSlot)
local src = source
local Player = QBCore.Functions.GetPlayer(src)
if not Player then return end
local citizenid = Player.PlayerData.citizenid
local reputation = GetBlackMarketReputation(citizenid)
if reputation < 3 then
TriggerClientEvent('QBCore:Notify', src, 'You don\'t have enough reputation', 'error')
return
end
local item = Player.Functions.GetItemBySlot(weaponSlot)
if not item or not item.info or not item.info.serial then
TriggerClientEvent('QBCore:Notify', src, 'No weapon with serial found', 'error')
return
end
local cost = Config.ScratchSerialPrice -- e.g., 15000
if Player.PlayerData.money.cash < cost then
TriggerClientEvent('QBCore:Notify', src, 'Not enough cash', 'error')
return
end
Player.Functions.RemoveMoney('cash', cost, 'serial-scratch')
-- Update database
MySQL.update('UPDATE weapon_serials SET is_scratched = TRUE, owner_citizenid = NULL WHERE serial = ?',
{ item.info.serial })
-- Update item metadata
item.info.serial = 'SCRATCHED'
Player.Functions.SetInventoryItem(item.name, item.amount, item.info, weaponSlot)
TriggerClientEvent('QBCore:Notify', src, 'Serial number removed', 'success')
end)
Weapon Licensing System
The licensing system creates an important bureaucratic roleplay layer that connects weapon shops to government and law enforcement factions. Players should apply for a weapons license at city hall or a police station, undergo a background check that examines their criminal record, and potentially attend a shooting range test administered by an officer. Implement three license tiers: basic for handguns and shotguns, advanced for SMGs and rifles, and military for heavy weapons that are rarely issued to civilians. Licenses should have an expiration date, typically 30 real-time days, requiring renewal. Police officers need a command to revoke licenses when players are convicted of violent crimes, which immediately blocks them from purchasing at legal shops and creates demand for the black market. Store license status in the database and check it server-side on every purchase attempt, never trust client-side license checks because they can be bypassed trivially.
Client-Side Shop Interface
The weapon shop NUI should feel like a professional storefront with weapons displayed in categorized tabs. Show each weapon with its name, price, required license, current stock level, and a small preview image or icon. Include a separate section for ammunition where players select their weapon first, then see only compatible ammo types. The attachment section works similarly, showing only components compatible with the player's currently equipped weapon. Implement a shopping cart system that lets players queue multiple items and check out in a single transaction rather than buying each item individually. For the black market UI, use a darker aesthetic with a reputation progress bar showing how close the player is to the next tier. Display locked items as silhouettes with a reputation requirement label so players know what they are working toward. Both shop types should trigger a server callback on open to fetch real-time prices and stock rather than relying on cached config values, because admins may adjust prices dynamically based on server economy conditions.
Anti-Exploit and Police Integration
Weapon shops are a prime target for exploiters trying to duplicate expensive weapons or generate items without paying. Validate every purchase server-side by checking the player's money, license status, shop proximity, and purchase cooldown before creating any items. Log every weapon transaction with the buyer's identifiers, weapon serial, shop ID, and timestamp so administrators can trace the origin of any weapon on the server. Integrate with the police MDT system so officers can look up a weapon serial during investigations and see the registered owner, purchase date, and whether the serial has been scratched. Implement a weapons registry export that generates a report of all registered weapons for a specific citizen, useful for probation checks. Rate-limit purchase events to prevent rapid-fire requests that could bypass cooldown checks through race conditions. Send Discord webhook alerts when high-tier military weapons are sold on the black market so your moderation team can monitor for abuse. The combination of proper server-side validation, comprehensive logging, and police integration creates a weapon economy that feels realistic while remaining resistant to exploitation.