Tutorial 2026-04-16

FiveM Door Lock System Development Guide

TDYSKY

TDYSKY

Founder & Lead Developer at Agency Scripts

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.

Share this article

Ready to upgrade your server?

Check out our premium FiveM scripts in the Agency Scripts store or join our Discord community for support and updates.