A hundred and twenty-five units players actually own.
Seventeen garages at Strawberry and a three-floor warehouse at the docks. Buy or rent a unit, set a PIN, store items and upgrade the capacity — with a directory tablet that walks you to the door.
Two locations, 125 units. Players buy or rent a unit, choose a PIN, store items, upgrade its capacity and manage ownership themselves.
Location
Displayed numbers
Unit IDs used by integrations
Strawberry garages
01–17
1–17
Warehouse, three floors
001–108
1001–1108
Displayed numbers are not the IDs. Warehouse unit 001 is ID 1001 and
unit 108 is ID 1108. Those IDs are what commands, exports, price overrides and
persistent ownership use. Garage IDs are unchanged, so garage 01 is simply 1.
What is in the resource
Both maps, already included: the Strawberry garages and the three-floor dock warehouse.
Keypads for buying, renting, PIN entry and rent renewal.
Management laptops for the door, the PIN, capacity upgrades and selling.
Animated storage crates that open into the inventory stash.
A warehouse directory tablet with filters and indoor 3D guidance to the unit.
Qbox, QBCore, ESX Legacy, standalone and custom framework adapters, plus seven inventory adapters.
Requirements
OneSync, game build 3095 or newer, a framework and an inventory. The database is optional.
ServerOneSync, and game build 3095 or newer.
FrameworkQbox, QBCore, ESX Legacy, standalone, or a custom adapter.
InventoryRequired for storing items, in every mode including standalone.
DatabaseOptional: oxmysql, mysql-async or ghmattimysql. Ownership falls back to JSON when none is running.
Included inventory adapters
ox_inventory, qb-inventory, qs-inventory,
tgiann-inventory, codem-inventory, origen_inventory,
core_inventory and custom. Use a version that exposes the stash API
expected by bridge/inventory.lua.
An included adapter is not a promise about every version. Inventory resources change
their stash API between releases, and an adapter being listed here does not claim that every
version of that inventory works with every framework.
What has actually been tested
Qbox and QBCore have been tested in game. ESX and standalone have passed offline integration
checks, and the ESX check also runs against the upstream ESX Legacy xPlayer
implementation. Your own server's framework modifications and inventory still need an
installation test.
Standalone does not include an inventory. It provides identity, ownership, payments,
PIN access and map interaction without a framework, and carries its own cash and bank wallet
saved per licence in resource KVP. Setting framework = 'standalone' does not
make a framework-dependent inventory standalone-compatible — use an inventory that supports
your setup, or implement the editable custom inventory bridge.
Installation
Add the pool increases, start it after your framework, then connect with a freshly launched client.
Extract 9labs_storageunits into your resources, and keep that folder name.
Start your framework, database, inventory and optional target resource before this one.
Add the four pool increases from storage_pools.cfg to server.cfg, without duplicating entries your server already sets.
Add the resource to server.cfg.
Configure config.lua, then start the resource and connect with a freshly launched FiveM client.
ensure 9labs_storageunits
Do not load the warehouse twice. Both maps ship with this resource. Disable any
separate 9labs_warehouse_storage or 9labs_warehouse_storage_NEW map
resource, and keep both native map manifests and all YTYP registrations intact.
No manual SQL import is needed. The table ninelabs_storage_units is created
automatically when a supported database resource is running; otherwise ownership is written to
data/units.json.
Two common setups
ESXframework = 'esx' with inventory = 'ox_inventory', and es_extended plus the configured inventory and database already started.
Standaloneframework = 'standalone' with a compatible inventory configured separately. Starting balances live in standalone.startingCash and standalone.startingBank.
Fully restart FiveM before connecting after the first installation or a map update. A
hot resource restart does not validate map streaming on a first connection.
Player controls
Four things a player touches: the keypad, the laptop, the crate and the directory tablet.
KeypadBuy or rent, choose a PIN, unlock the unit, open and close the door, and renew rent. Used from the corridor side only, never through the wall from inside the unit.
LaptopOpen and close the door, change the PIN, upgrade capacity, renew rent, or sell a purchased unit. After a sale to the city the door stays open until the seller walks out. Anyone stuck inside a closed unit without access gets an OPEN DOOR button here.
CrateApproach it while the door is open (or from inside a closed unit with crate.requireOpenDoor = false). Opening plays the character and lid animation and opens the inventory.
Directory tabletAt the warehouse entrance: filter, search and start indoor guidance.
A correct PIN starts a session of pin.sessionMinutes for that unit. Closing the unit
blocks new stash access. PIN entry accepts keyboard digits, the numpad, Backspace, Delete and
Enter, or clicks on the onscreen keys; Escape closes the screen.
Sharing a unit by PIN
With the default pin.shared = true, anyone who enters the correct PIN gets a session
and can open and close the door, open the crate and use the stash, so friends can share one
unit. Managing the unit stays with the owner: changing the PIN, capacity upgrades, paying rent
and selling check ownership as well as the session. A PIN change immediately ends every open
session on that unit, including shared ones.
Giving out the PIN gives out the unit's contents. This is the intended behaviour. For
owner-only access, set pin.shared = false: other players are then refused at the
keypad even with the correct PIN, and only the owner can open the door, crate and stash.
Wrong PINs lock the player out per character after pin.attempts tries, and every
repeated lockout doubles pin.lockoutSeconds, up to one hour.
The default interaction is the built-in E prompt. When a target system is selected
in the config, that is used instead.
Warehouse directory
The entrance tablet lists all 108 units and walks you to the one you pick.
Open the tablet at the warehouse entrance.
Filter by floor, by availability, or by your own units — or type a three-digit number.
Select a unit and choose GUIDE ME THERE.
Follow the 3D arrows through the corridors and stairs.
Press X to cancel guidance at any point.
Arrow colour, visible distance, the cancel key and the guidance timeout are set under
warehouse.navigation. The cancel key is a real FiveM key binding, so a player can
rebind it in their own settings.
Buying, renting and reclaim
Purchases and rentals are enabled independently, and both can be reclaimed after inactivity.
Purchases and rentals
Purchased units carry no recurring rent. Rentals pay once on acquisition and then renew, and
cannot be sold to the city or to another player. Turning off new rentals still lets current
renters renew, and with both acquisition options disabled existing owners keep their access.
Player-to-player sales also need economy.buy = true; with purchases disabled, offers
are refused and the laptop does not show the option.
Per-unit prices override the default purchase price by unit ID:
The unit stays usable for graceDays. When the grace period ends, the stash is cleared and ownership is released.
'lock'
Door and storage access are blocked at once, with no grace period. Ownership and contents are kept until the owner pays.
graceDays only delays a 'reclaim'. In 'lock' mode the unit is
never reclaimed for unpaid rent, but the inactivity rule below still applies to it.
Inactivity
Inactivity applies to purchases and rentals alike, defaults to seven offline days, and can be
switched off. Character login and logout counts as activity — visiting the unit is not
required. Online owners are exempt from an inactivity reclaim, but still owe rental payments.
A reclaim that cannot clear the stash does not take the unit. If clearing fails,
ownership is retained and the attempt is retried after five minutes, rather than releasing a
unit whose contents are still inside it.
Only the ox_inventory adapter and the custom adapter confirm that the
stash is actually empty. The qb-inventory, qs-inventory,
tgiann-inventory, codem-inventory, origen_inventory and
core_inventory adapters report success as soon as their clear export returns without
an error, so with those inventories a clear that silently does nothing still releases the unit.
Configuration reference
All normal setup lives in config.lua.
Setting
Purpose
locale
en, de or bs; translations are editable in locales/
framework
auto, qbx, qb, esx, standalone, custom
inventory
auto, an included inventory name, or custom
interaction
9labs_ui, ox_lib, ox_target, qb-target, custom
interactKey / interactKeyLabel
Prompt key as a control ID, and the label shown for it; default 38, E
interactDistance
Reach in metres from a keypad or laptop screen; default 1.8
targetIcon
Icon for the ox_target and qb-target options
prompt.title / prompt.scale
Title and size of the built-in 9labs_ui prompt; scale 0.75–1.5
notify
9labs_ui, ox_lib, native, custom
display
Keypad and laptop screen colours: accent, free and taken
standalone.startingCash / standalone.startingBank
Starting balances of a new standalone wallet
debug
Print the framework, inventory and database driver when the resource starts
economy.buy / economy.rentable
Enable purchases and new rentals independently
economy.buyPrice / economy.price[id]
Default purchase price, and per-unit overrides
economy.accounts
Payment accounts, bank and/or cash, in the order the screens offer them. The first entry also receives city buyback and player-sale payouts, and a buyer accepting a player offer always pays from it. Default { 'bank', 'cash' }
economy.currency
Currency symbol shown on screens and in messages
economy.rent
Rental price, period length, advance payments, grace period and expiry action
economy.sellToState
City buyback share of the purchase and upgrades paid
economy.sellToPlayer
Enable player sales, and set price and offer limits. Also requires economy.buy = true
inactivity.enabled / inactivity.days
Reclaim units after this many offline days
tiers
Storage slots, weight in grams and upgrade prices. Keep tier IDs stable
pin
PIN length, attempts, lockout and session duration
pin.shared
Default true: anyone with the PIN can open the door, crate and stash, while only the owner manages the unit. false limits access to the owner
doors.rate
Native opening and closing speed; default 0.5. Try values in game with storagedoor <rate>
doors.autoCloseSeconds
Automatic door closing; 0 disables it
doors.closeOnStart
Close doors when the resource starts
crate.visuals / crate.maxProps
Display inventory contents inside crates
crate.itemProps
Props used for items shown in the crate; weapons use their own world model
crate.ownerOnly
Default true: the lid opens only for a player with a PIN session. false lets anyone at an open unit move the lid; the stash still needs a session
crate.requireOpenDoor
Default true: the crate and stash work only with the door open. false also allows them with the door closed, for players standing inside the unit
crate.animHeadingOffset
Degrees added to the character's heading at the crate; 180 if it opens the crate with its back to it
Screen camera field of view, how much of the view the screen fills, blend time in milliseconds, and a fixed distance in metres (0 calculates it from the screen size)
camera.keypadFill
Overrides fill for keypads; lower values move the keypad camera farther away
camera.focus
Default true: keeps the open screen in focus when a graphics pack adds depth of field. A config without the key behaves as true
screens
Screen resolution as a width in pixels (1024 to 4096) for keypad, laptop and tablet; the height follows the screen shape
Arrow colour, visible distance, cancel key and timeout
adminAce
Admin permission; default 9labs.storage.admin
Keep tier IDs and unit IDs stable. Tier IDs persist on owned units, and unit IDs are
what ownership, price overrides and every export refer to.
Framework and inventory
auto picks the first one it finds. Choose explicitly when more than one is installed.
The auto framework order is qbx_core, qb-core,
es_extended, then standalone. Inventory auto-selection follows the order listed
under Requirements.
Switching framework on a live server is a migration, not a setting change. The
framework choice does not rewrite stored owner identities, so plan a data migration before
changing one in production, and back up first.
Standalone wallet
Standalone keeps its own cash and bank balance per licence in resource KVP, separate from any
framework economy. Starting balances apply when a wallet is first created. Do not rename the
resource or delete its KVP data during an update, or those wallets are gone.
Commands and permissions
Administration runs from the server console or an ACE-authorized principal.
Command
Access
Purpose
storage <unitId> open
Console / admin ACE
Open a unit
storage <unitId> close
Console / admin ACE
Close a unit
storage <unitId> free
Console / admin ACE
Clear the stash and reclaim ownership
storage or storage info
Console / admin ACE
Print the active inventory and database driver, then owners, tiers and rent dates
storagecredits <playerId> <amount> [cash|bank]
Console / admin ACE
Add standalone wallet funds; defaults to bank
storagestatus
Console only
Version, framework, inventory, database, unit counts and external manifest scripts
storagewaypoint
Player
Cancel warehouse guidance; default key X
For an existing admin principal:
add_ace group.admin 9labs.storage.admin allow
Commands take the integration ID, not the displayed number — this opens warehouse unit 108:
storage 1108 open
Client diagnostic commands
These run in a player's F8 console and have no permission check. They print to that player's
own console and only change settings in that player's game, never on the server or for anyone
else. Use them while setting up, then copy the values you settle on into config.lua.
Command
Purpose
storagedoor [rate]
Print the state of every door within 40 m. A number sets the door speed in this game only, to try values for doors.rate
storagedoor trace [unitId]
Record the native door motor of the nearest or given door for 120 seconds while you open and close it
storageanim [offset]
Print the crate animation heading offset. A number sets it in this game only, to find crate.animHeadingOffset
storagedui [apply]
Print the state of nearby keypad and laptop screens; apply re-applies their textures
storagekey
Print the nearest keypad, laptop, crate or directory, its distance and the interaction state, for a prompt that does not appear
Server exports
Nine exports, all server-side. Those that act on a unit take its numeric unit ID.
GetUnits, GetUnitOwner, IsUnitOwner and
GetStashId are covered here. GetDoorOpen, SetDoorOpen,
GetCrateOpen, SetCrateOpen and RecordPlayerActivity are
under Doors, crates and activity.
Mutation exports are trusted integration functions. They do not enforce player
ownership, distance or PIN checks. Never expose them through an unrestricted client event —
player interactions already go through the protected handlers.
GetUnits
local units = exports['9labs_storageunits']:GetUnits()for key, unit in pairs(units) do local id = tonumber(key) print(id, unit.label, unit.door.coords.x, unit.door.coords.y, unit.door.coords.z)end
Returns a detached geometry table for all 125 units, with no owner details and no PINs. It is
JSON-copied, so keys may be strings and positions arrive as { x, y, z } tables.
Warehouse entries carry site = 'warehouse' and level of
0, 1 or 2. Treat the geometry as read-only: changing the
returned table moves nothing.
GetUnitOwner
local owner = exports['9labs_storageunits']:GetUnitOwner(1001)if owner then print(owner.name, owner.tier, owner.paidUntil)end
Returns nil for an unowned or invalid unit, otherwise:
Field
Type
Meaning
owner
string
Internal framework-prefixed character identity, not a player server ID
name
string
Stored owner display name
tier
string
Storage tier ID
paidUntil
number
Rental expiry in Unix seconds; 0 for purchases
PINs are never returned by this export.
IsUnitOwner
local ownsUnit = exports['9labs_storageunits']:IsUnitOwner(playerId, 1001)
Returns a boolean. playerId is an online player's numeric server ID. It checks
ownership only: it does not establish a PIN session, and it does not tell you whether a rental
is paid up.
GetStashId
local stashId = exports['9labs_storageunits']:GetStashId(1001)-- 9ls_storage_1001
Returns the persistent inventory identifier, or nil for an invalid unit. Garage
unit 1 uses 9ls_storage_01. This export neither registers nor opens the inventory,
and holding the identifier does not grant permission to access it.
Doors, crates and activity
The other five exports, three of which change state. Apply your own access rules before calling them.
GetDoorOpen / SetDoorOpen
local isOpen = exports['9labs_storageunits']:GetDoorOpen(1001)local changed = exports['9labs_storageunits']:SetDoorOpen(1001, true)
GetDoorOpen(id) returns the requested boolean state, or nil for an
invalid ID. It reports the server state, not whether the physical animation has finished.
SetDoorOpen(id, open) returns true when the state changes, and
false for an invalid ID or type, an unchanged state, or the 700 ms toggle cooldown.
Opening honours the configured automatic closing, and closing also closes an open crate. State
changes synchronise to clients and are applied when the map streams in.
GetCrateOpen / SetCrateOpen
local isOpen = exports['9labs_storageunits']:GetCrateOpen(1001)local changed = exports['9labs_storageunits']:SetCrateOpen(1001, false)
GetCrateOpen(id) returns the requested lid state, or nil for an
invalid ID. SetCrateOpen(id, open) returns true on a change, or
false for an invalid ID or type, an unchanged state, or the 3.2-second crate
cooldown.
It controls the synced lid and nothing else. It does not animate a player, does not
open their inventory, and does not check whether the unit door is open.
Call this after your custom character system finishes loading a character, once the configured
identity adapter returns the correct character identity. It updates inactivity tracking and
returns no value. Qbox, QBCore, ESX and standalone load and drop flows are handled
automatically.
Never manufacture owner keys or write ownership records from a client. Custom
integrations can invalidate a cached player identity through the editable bridge.
Custom bridges
The editable files are config.lua, bridge/*.lua and locales/*.lua.
bridge/server.luaSet framework = 'custom' and fill in the CUSTOM block: stable character identity, display name, balance, debit, payout and optional admin checks.
bridge/inventory.luaSet inventory = 'custom' and implement registration, opening, item listing and clearing. Weights are in grams.
bridge/client.luaSet interaction and/or notify to custom for prompts, target zones or notifications.
Money hooks must return a boolean and actually apply the amount, and clearing must
return true only once the stored items have really been removed. Returning true
on a failed operation is how a unit gets released with items still in it.
Guarding a custom inventory's open endpoint
If your inventory has a separate open endpoint, call
StorageInventory.canOpen(source, unitId) inside the inventory bridge before
opening. It checks that the unit door is open, that the player is within 2.5 m of the crate, and
that the player holds a PIN session on a unit that is not locked for unpaid rent. It does not
check ownership: with pin.shared = true, anyone who entered the correct PIN passes.
That is an internal server bridge function, not an export. It is not available from
another resource. The ox_inventory integration registers an open hook for
9ls_storage_* stashes; any other inventory integration has to preserve equivalent
access checks itself.
Built-in prompts and notifications work without a separate UI resource.
Updates, backups and troubleshooting
Ownership and inventory items are stored separately. Both need backing up.
What to preserve on an update
Your configuration, editable bridges and locales.
The ownership database, or data/units.json when no database is running.
Inventory storage, which lives in your inventory resource, not this one.
The standalone resource KVP database.
Do not rename the resource, and do not delete its KVP data when updating standalone wallets.
Door and lid states are runtime state, not persisted ownership data.
Changing framework or inventory provider is a migration. Back up before you do it — it
is not a config-only conversion.
Getting a support report
Run storagestatus from the server console and include its output. External scripts
added to this resource's manifest execute inside it, and the profiler may charge their cost to
this resource — so preserve the delivered manifest while diagnosing a problem.
Common problems
A stash will not openCheck the selected inventory, the startup order, the PIN session, rental status and door state. With no supported inventory, item storage is unavailable.
Missing or duplicated geometryCheck the pool settings and any separate warehouse copy, then test with a freshly launched game client.
A screen looks blurryUsually depth of field from a graphics pack such as NVE. Keep camera.focus = true; you can also raise the width in screens.
RPC timeouts under an anticheatAllow the resource and its request and reply events.