9LABS Storage Units 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.
Introduction
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.
LaptopOpen and close the door, change the PIN, upgrade capacity, renew rent, or sell a purchased unit.
CrateApproach it while the door is open. Opening plays the character and lid animation and opens the inventory.
Directory tabletAt the warehouse entrance: filter, search and start indoor guidance.
The owner must hold a valid PIN session to manage a unit from the laptop, and 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.
Knowing someone else's PIN does not grant access. Ownership is checked as well as the
PIN.
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.
Per-unit prices override the default purchase price by unit ID:
price = {
[1] = 20000, -- Strawberry garage 01
[1001] = 30000, -- warehouse unit 001
},
When rent runs out
onExpire | What happens after the grace period |
'reclaim' | Clears the stash and releases ownership. |
'lock' | Keeps ownership and contents, but blocks storage access until payment. |
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 the inventory refuses,
ownership is retained and the attempt is retried after five minutes, rather than releasing a
unit whose contents are still inside it.
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 |
notify | 9labs_ui, ox_lib, native, custom |
economy.buy / economy.rentable | Enable purchases and new rentals independently |
economy.buyPrice / economy.price[id] | Default purchase price, and per-unit overrides |
economy.accounts | Allowed payment accounts: cash, bank |
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 |
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 |
doors.rate | Native opening and closing speed; default 0.5 |
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 |
camera.keypadFill | Lower values move the keypad camera farther away |
warehouse.name / warehouse.blip | Directory title and warehouse blip |
warehouse.navigation | 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 info | Console / admin ACE | List 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
Server exports
Every export is server-side and takes a numeric unit ID.
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
Three more exports that 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.
RecordPlayerActivity
exports['9labs_storageunits']:RecordPlayerActivity(playerId)
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 verifies ownership and PIN session, door state, rental access and player proximity.
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.
RPC timeouts under an anticheatAllow the resource and its request and reply events.
9labs_storageunits:rpc:request
9labs_storageunits:rpc:reply