Why Door Locks Matter in Roleplay
Door lock systems are one of the most underrated yet essential components of any serious FiveM roleplay server. Without proper door management, players can walk into police armories, hospital staff rooms, and gang hideouts without restriction, completely breaking immersion. A well-implemented door lock system allows server owners to control access to every interior on the map, from government buildings to player-owned businesses. It creates natural boundaries that drive roleplay interactions like key exchanges, lockpicking scenarios, and coordinated breaches. In this guide, we will walk through building a complete door lock system using ox_doorlock as the foundation, then extend it with custom lockpicking mechanics, keycard authentication, door groups, and business-specific access control.
Setting Up ox_doorlock
The ox_doorlock resource is the most widely adopted door lock solution in the FiveM ecosystem, offering a clean API, built-in admin UI for placing doors, and support for both double doors and garage-style animated doors. Before writing any custom code, you need a solid foundation. Install ox_doorlock alongside ox_lib, which it depends on for UI components and utility functions. Your server.cfg needs to ensure the resources start in the correct order, with your database running first, then ox_lib, and finally ox_doorlock. Once running, the built-in admin command lets you walk up to any door in the game world and register it with a click.
-- server.cfg load order
ensure oxmysql
ensure ox_lib
ensure ox_doorlock
-- Grant admin access for door placement
add_ace group.admin command.doorlock allow
After registering doors through the admin interface, ox_doorlock stores each entry in the database with coordinates, heading, model hash, and lock state. You can query and manipulate these programmatically. The resource exposes both exports and events for toggling locks, checking state, and managing access. Understanding the data structure is critical before building custom features on top of it.
-- Checking if a specific door is locked
local doorId = 1
local isLocked = exports.ox_doorlock:getDoorState(doorId)
-- Toggle a door lock from server side
exports.ox_doorlock:setDoorState(doorId, not isLocked)
-- Listen for door state changes
AddEventHandler('ox_doorlock:stateChanged', function(id, state, source)
print(('Door %d changed to %s by player %s'):format(id, state and 'locked' or 'unlocked', source))
end)
Building a Lockpicking Mechanic
Lockpicking adds a gameplay layer that makes locked doors meaningful for criminal characters. Instead of doors being absolute barriers, they become skill-based challenges. The best approach is a minigame that requires timing and precision, making the outcome feel earned rather than random. We will use ox_lib's built-in skillcheck system to create a lockpicking flow that requires the player to have a lockpick item in their inventory, triggers an animation, and runs a multi-stage skillcheck. Failing the skillcheck should have consequences like breaking the lockpick or alerting nearby police through a dispatch event.
-- client/lockpicking.lua
local function AttemptLockpick(doorId)
local hasLockpick = exports.ox_inventory:Search('count', 'lockpick')
if hasLockpick < 1 then
lib.notify({ title = 'No Lockpick', type = 'error' })
return
end
-- Play lockpicking animation
local ped = PlayerPedId()
TaskPlayAnim(ped, 'anim@amb@clubhouse@tutorial@bkr_tut_ig3@', 'machinic_loop_mechandlenry', 8.0, -8.0, -1, 1, 0, false, false, false)
-- Multi-stage skillcheck: easy, medium, hard
local success = lib.skillCheck({'easy', 'medium', 'hard'}, {'w', 'a', 's', 'd'})
ClearPedTasks(ped)
if success then
TriggerServerEvent('doorlock:lockpick:success', doorId)
lib.notify({ title = 'Lock Picked', description = 'The door clicks open.', type = 'success' })
else
TriggerServerEvent('doorlock:lockpick:fail', doorId)
lib.notify({ title = 'Failed', description = 'The lockpick snapped.', type = 'error' })
end
end
On the server side, you need to validate the lockpicking attempt, remove the lockpick item on failure, unlock the door on success, and optionally dispatch a police alert. Rate limiting is important here to prevent spam attempts. Store a cooldown per player per door so they cannot immediately retry after a failure. The server handler should also verify that the player is actually near the door they claim to be picking, preventing remote exploitation.
-- server/lockpicking.lua
local cooldowns = {}
RegisterNetEvent('doorlock:lockpick:success', function(doorId)
local src = source
local playerCoords = GetEntityCoords(GetPlayerPed(src))
-- Verify proximity to door (anti-cheat)
local doorData = exports.ox_doorlock:getDoor(doorId)
if not doorData then return end
if #(playerCoords - doorData.coords) > 3.0 then return end
-- Check cooldown
local key = ('%s:%s'):format(src, doorId)
if cooldowns[key] and os.time() - cooldowns[key] < 30 then return end
exports.ox_doorlock:setDoorState(doorId, 0) -- Unlock
cooldowns[key] = os.time()
-- Auto-relock after 60 seconds
SetTimeout(60000, function()
exports.ox_doorlock:setDoorState(doorId, 1)
end)
end)
RegisterNetEvent('doorlock:lockpick:fail', function(doorId)
local src = source
exports.ox_inventory:RemoveItem(src, 'lockpick', 1)
-- Alert police dispatch
TriggerEvent('dispatch:alert', {
coords = GetEntityCoords(GetPlayerPed(src)),
message = 'Attempted break-in reported',
code = '10-31',
job = 'police'
})
end)
Implementing a Keycard System
Keycards provide a more structured access control mechanism compared to simple key items. They work well for corporate buildings, government facilities, and restricted areas where access should be tied to specific clearance levels. The keycard system assigns a security level from 1 to 5 to each door, and players must possess a keycard with an equal or higher clearance to unlock it. This creates a natural hierarchy where a Level 3 keycard opens all Level 1, 2, and 3 doors but cannot access Level 4 or 5 restricted areas. Keycards can be issued by job systems, found as loot, or crafted through a progression system.
-- shared/config.lua
Config = {}
Config.KeycardLevels = {
{ name = 'keycard_1', label = 'Green Keycard', level = 1 },
{ name = 'keycard_2', label = 'Blue Keycard', level = 2 },
{ name = 'keycard_3', label = 'Yellow Keycard', level = 3 },
{ name = 'keycard_4', label = 'Red Keycard', level = 4 },
{ name = 'keycard_5', label = 'Black Keycard', level = 5 },
}
Config.DoorSecurity = {
[10] = { level = 1, name = 'Office Lobby' },
[11] = { level = 2, name = 'Server Room' },
[12] = { level = 3, name = 'Executive Floor' },
[13] = { level = 4, name = 'Vault Anteroom' },
[14] = { level = 5, name = 'Main Vault' },
}
-- client/keycard.lua
local function TryKeycardAccess(doorId)
local security = Config.DoorSecurity[doorId]
if not security then return false end
for _, card in ipairs(Config.KeycardLevels) do
if card.level >= security.level then
local count = exports.ox_inventory:Search('count', card.name)
if count > 0 then
-- Play card swipe animation
lib.requestAnimDict('anim@heists@keycard@')
TaskPlayAnim(PlayerPedId(), 'anim@heists@keycard@', 'exit', 5.0, 1.0, -1, 16, 0, 0, 0, 0)
Wait(1200)
ClearPedTasks(PlayerPedId())
return true, card
end
end
end
return false
end
Door Groups and Business Access
Managing individual doors becomes unscalable when your server has hundreds of locked doors. Door groups solve this by letting you assign multiple doors to a named group and control access to the entire group at once. A police station might have 15 locked doors that should all be accessible by the police job. Instead of configuring each door individually, you assign them all to the "police_station" group and grant access based on job name. When an admin hires a new officer, they automatically gain access to every door in the group without any manual configuration.
-- server/door_groups.lua
local DoorGroups = {
police_station = {
doors = {1, 2, 3, 4, 5, 6, 7, 8},
access = {
{ type = 'job', name = 'police', minGrade = 0 },
{ type = 'job', name = 'sheriff', minGrade = 0 },
}
},
pillbox_hospital = {
doors = {20, 21, 22, 23, 24},
access = {
{ type = 'job', name = 'ambulance', minGrade = 0 },
{ type = 'job', name = 'doctor', minGrade = 2 },
}
},
vangelico = {
doors = {30, 31},
access = {
{ type = 'job', name = 'jeweler', minGrade = 0 },
{ type = 'item', name = 'vangelico_key' },
}
},
}
function HasGroupAccess(source, groupName)
local group = DoorGroups[groupName]
if not group then return false end
for _, rule in ipairs(group.access) do
if rule.type == 'job' then
local job = GetPlayerJob(source)
if job and job.name == rule.name and job.grade >= (rule.minGrade or 0) then
return true
end
elseif rule.type == 'item' then
local count = exports.ox_inventory:GetItem(source, rule.name, nil, true)
if count and count > 0 then return true end
end
end
return false
end
Business doors add another dimension by tying door access to ownership records. When a player purchases a business, they should automatically gain control over all doors associated with that property. The system needs to check business ownership in real time because properties can be sold or transferred. Implement a callback that queries your business table to verify the current owner, then cache the result with a short TTL to avoid hammering the database on every door interaction.
Syncing Door States Across Clients
Door synchronization is one of the trickier aspects of a door lock system. When one player unlocks a door, every nearby player needs to see it open. FiveM native door controls operate on a per-client basis, meaning each client independently manages door states. Without proper sync, one player sees a door open while another sees it closed. ox_doorlock handles basic state sync, but custom extensions need to be careful about state propagation. Use state bags or server-authoritative events to broadcast door changes to all players within a reasonable range. Avoid syncing to the entire server for every door toggle because that creates unnecessary network overhead on large servers with hundreds of doors.
-- server: broadcast door state to nearby players only
local function SyncDoorToNearby(doorId, state, coords, range)
range = range or 100.0
local players = GetPlayers()
for _, playerId in ipairs(players) do
local ped = GetPlayerPed(playerId)
if ped and DoesEntityExist(ped) then
local playerCoords = GetEntityCoords(ped)
if #(playerCoords - coords) <= range then
TriggerClientEvent('doorlock:sync', tonumber(playerId), doorId, state)
end
end
end
end
-- client: apply synced door state
RegisterNetEvent('doorlock:sync', function(doorId, state)
local door = exports.ox_doorlock:getDoor(doorId)
if door then
door.state = state
end
end)
Security Considerations and Anti-Exploit
Door lock systems are a common target for exploiters because bypassing a door often grants access to restricted items, weapons, or money. Every door toggle request must be validated server-side. Never trust client-sent door IDs without verifying the player's proximity and access rights. Implement logging for every door state change so admins can review suspicious patterns, like a player unlocking vault doors they should not have access to. Rate limit all door interactions to prevent brute-force attempts on the lockpicking system. Consider adding a server-side event logger that records the player's Steam ID, door ID, timestamp, and method of access for audit trails. These logs become invaluable when investigating exploits or resolving disputes between players about who accessed what and when.
Putting It All Together
A production-ready door lock system combines all these components into a unified framework. The entry point is a target system interaction, either ox_target or qb-target, that detects when a player looks at a registered door and presents contextual options based on their access level. If they have job access, show an unlock button. If they have a keycard, show a swipe option. If they have a lockpick and the door is flagged as pickable, show the lockpick option. Each path feeds into the appropriate handler, which validates on the server and syncs the result to nearby clients. Configuration should be data-driven through database entries or shared config files so server owners can add, remove, and modify door access without touching code. This modular architecture means you can swap out the lockpicking minigame, add new access methods like biometric scanners, or integrate with entirely different inventory systems without rewriting the core logic.