Nothing in this manual matched.

9LABS Storage Units · v1.4.0

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.

LocationDisplayed numbersUnit IDs used by integrations
Strawberry garages01–171–17
Warehouse, three floors001–1081001–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.

  1. Extract 9labs_storageunits into your resources, and keep that folder name.
  2. Start your framework, database, inventory and optional target resource before this one.
  3. Add the four pool increases from storage_pools.cfg to server.cfg, without duplicating entries your server already sets.
  4. Add the resource to server.cfg.
  5. 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.

  1. Open the tablet at the warehouse entrance.
  2. Filter by floor, by availability, or by your own units — or type a three-digit number.
  3. Select a unit and choose GUIDE ME THERE.
  4. Follow the 3D arrows through the corridors and stairs.
  5. 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:

price = {
    [1]    = 20000,   -- Strawberry garage 01
    [1001] = 30000,   -- warehouse unit 001
},

When rent runs out

onExpireWhat happens when the paid period ends
'reclaim'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.

SettingPurpose
localeen, de or bs; translations are editable in locales/
frameworkauto, qbx, qb, esx, standalone, custom
inventoryauto, an included inventory name, or custom
interaction9labs_ui, ox_lib, ox_target, qb-target, custom
interactKey / interactKeyLabelPrompt key as a control ID, and the label shown for it; default 38, E
interactDistanceReach in metres from a keypad or laptop screen; default 1.8
targetIconIcon for the ox_target and qb-target options
prompt.title / prompt.scaleTitle and size of the built-in 9labs_ui prompt; scale 0.75–1.5
notify9labs_ui, ox_lib, native, custom
displayKeypad and laptop screen colours: accent, free and taken
standalone.startingCash / standalone.startingBankStarting balances of a new standalone wallet
debugPrint the framework, inventory and database driver when the resource starts
economy.buy / economy.rentableEnable purchases and new rentals independently
economy.buyPrice / economy.price[id]Default purchase price, and per-unit overrides
economy.accountsPayment 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.currencyCurrency symbol shown on screens and in messages
economy.rentRental price, period length, advance payments, grace period and expiry action
economy.sellToStateCity buyback share of the purchase and upgrades paid
economy.sellToPlayerEnable player sales, and set price and offer limits. Also requires economy.buy = true
inactivity.enabled / inactivity.daysReclaim units after this many offline days
tiersStorage slots, weight in grams and upgrade prices. Keep tier IDs stable
pinPIN length, attempts, lockout and session duration
pin.sharedDefault 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.rateNative opening and closing speed; default 0.5. Try values in game with storagedoor <rate>
doors.autoCloseSecondsAutomatic door closing; 0 disables it
doors.closeOnStartClose doors when the resource starts
crate.visuals / crate.maxPropsDisplay inventory contents inside crates
crate.itemPropsProps used for items shown in the crate; weapons use their own world model
crate.ownerOnlyDefault 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.requireOpenDoorDefault 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.animHeadingOffsetDegrees added to the character's heading at the crate; 180 if it opens the crate with its back to it
camera.fov / camera.fill / camera.blend / camera.distanceScreen 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.keypadFillOverrides fill for keypads; lower values move the keypad camera farther away
camera.focusDefault true: keeps the open screen in focus when a graphics pack adds depth of field. A config without the key behaves as true
screensScreen resolution as a width in pixels (1024 to 4096) for keypad, laptop and tablet; the height follows the screen shape
blipStrawberry garage blip: enabled, sprite, colour, scale, coords
warehouse.name / warehouse.blipDirectory title and warehouse blip
warehouse.navigationArrow colour, visible distance, cancel key and timeout
adminAceAdmin 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.

CommandAccessPurpose
storage <unitId> openConsole / admin ACEOpen a unit
storage <unitId> closeConsole / admin ACEClose a unit
storage <unitId> freeConsole / admin ACEClear the stash and reclaim ownership
storage or storage infoConsole / admin ACEPrint the active inventory and database driver, then owners, tiers and rent dates
storagecredits <playerId> <amount> [cash|bank]Console / admin ACEAdd standalone wallet funds; defaults to bank
storagestatusConsole onlyVersion, framework, inventory, database, unit counts and external manifest scripts
storagewaypointPlayerCancel 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.

CommandPurpose
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
storagekeyPrint 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:

FieldTypeMeaning
ownerstringInternal framework-prefixed character identity, not a player server ID
namestringStored owner display name
tierstringStorage tier ID
paidUntilnumberRental 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.

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 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.
9labs_storageunits:rpc:request
9labs_storageunits:rpc:reply

Nothing in this manual matched.