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.

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.
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.

  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.

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 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.

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
notify9labs_ui, ox_lib, native, custom
economy.buy / economy.rentableEnable purchases and new rentals independently
economy.buyPrice / economy.price[id]Default purchase price, and per-unit overrides
economy.accountsAllowed payment accounts: cash, bank
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
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
doors.rateNative opening and closing speed; default 0.5
doors.autoCloseSecondsAutomatic door closing; 0 disables it
doors.closeOnStartClose doors when the resource starts
crate.visuals / crate.maxPropsDisplay inventory contents inside crates
camera.keypadFillLower values move the keypad camera farther away
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 infoConsole / admin ACEList 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

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:

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

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
Stuck on something this page does not cover? Ask in Discord. Support