FiveM & QBCore · Practical guide

QBCore Real Estate: Build a Secure, Server-Authoritative Housing System

Step-by-step guide to designing and shipping a QBCore real estate resource. Covers architecture, destination-labelled files (fxmanifest, client, server, config, SQL), server-side.

Stellar AI · Updated 8 September 2026 · 5 min read

A practical, secure plan to create a QBCore real estate system that supports property browsing, purchase, ownership persistence, and basic entry/lock mechanics. This guide includes complete destination-labelled files, installation steps, validation checks, and a brief Roblox mapping for cross-platform builders.

Why server-authoritative housing matters

Real estate systems touch money, permissions, and persistent state. For stability and security, all authoritative actions — purchases, ownership changes, and money transfers — must run on the server. The client only requests actions and displays UI. This guide shows a minimal, production-minded QBCore resource with safe database writes, qb-target support, and clear extension points for inventory/locks or rental systems.

Implementation plan (short)

  • Define a SQL schema for properties and ownership.
  • Create resource files: fxmanifest.lua, config.lua, server/main.lua, client/main.lua.
  • Expose target zones (qb-target) for viewing and buying.
  • Validate purchases server-side: check player, balance, and atomic DB write.
  • Test flow: browse -> request buy -> server validates and persists -> client receives confirmation.

Assumptions & dependencies

  • Server uses QBCore (v2+ pattern): exports['qb-core']:GetCoreObject().
  • MySQL is available via oxmysql (exports.oxmysql:execute) or adjust to your DB layer.
  • qb-target or a similar targeting library is present for interaction zones.
  • Resource name will be qb-realestate. Adjust references if you rename it.

Destination-labelled files

Below are the minimal complete files to get started. Place them in a folder named qb-realestate in your resources directory.

// fxmanifest.lua
fx_version 'cerulean'
game 'gta5'

author 'YourName'
description 'QBCore Real Estate - minimal safe implementation'
version '1.0.0'

shared_scripts {
  'config.lua'
}

server_scripts {
  '@oxmysql/lib/MySQL.lua',
  'server/main.lua'
}

client_scripts {
  'client/main.lua'
}

dependencies {
  'qb-core'
}
// config.lua
Config = {}
Config.PropertyTable = 'qb_realestate_properties' -- SQL table
Config.OwnersTable = 'qb_realestate_owners'     -- SQL table
Config.TargetResource = 'qb-target' -- optional, used for interactions

-- Example property list fallback (also useful for initial DB seed)
Config.Properties = {
  { id = 1, label = 'Vinewood Terrace #1', price = 150000, coords = vector3( -267.0, -969.0, 31.2 ) },
}
// server/main.lua
local QBCore = exports['qb-core']:GetCoreObject()

-- Server: handle purchase requests and persistence
RegisterNetEvent('qb-realestate:server:BuyProperty', function(propertyId)
  local src = source
  local Player = QBCore.Functions.GetPlayer(src)
  if not Player then return end

  -- Validate property exists
  local property = nil
  for _,p in pairs(Config.Properties) do if p.id == propertyId then property = p break end end
  if not property then
    TriggerClientEvent('QBCore:Notify', src, 'Property not found', 'error')
    return
  end

  local price = property.price
  -- Check money and remove it server-side
  if Player.Functions.RemoveMoney('bank', price) then
    -- Persist ownership safely using parameterised query
    exports.oxmysql:insert('INSERT INTO '..Config.OwnersTable..' (property_id, owner) VALUES (?, ?)', { propertyId, Player.PlayerData.citizenid }, function(insertId)
      TriggerClientEvent('qb-realestate:client:OnBought', src, propertyId)
      print(('qb-realestate: %s bought property %s'):format(Player.PlayerData.citizenid, propertyId))
    end)
  else
    TriggerClientEvent('QBCore:Notify', src, 'Insufficient funds', 'error')
  end
end)

-- Simple server RPC to fetch owned properties
QBCore.Functions.CreateCallback('qb-realestate:server:GetOwned', function(source, cb)
  local Player = QBCore.Functions.GetPlayer(source)
  if not Player then cb({}) return end
  exports.oxmysql:execute('SELECT property_id FROM '..Config.OwnersTable..' WHERE owner = ?', { Player.PlayerData.citizenid }, function(result)
    local owned = {}
    for _,r in ipairs(result) do owned[r.property_id] = true end
    cb(owned)
  end)
end)
// client/main.lua
local QBCore = exports['qb-core']:GetCoreObject()

-- Register target interactions (if qb-target is present)
CreateThread(function()
  -- Add target for each property
  for _,p in pairs(Config.Properties) do
    if GetResourceState(Config.TargetResource) == 'started' then
      exports[Config.TargetResource]:AddBoxZone('re_prop_'..p.id, p.coords, 2, 2, {
        name = 're_prop_'..p.id,
        heading = 0,
        debugPoly = false
      }, {
        options = {
          {
            icon = 'fas fa-home',
            label = 'View / Buy '..p.label,
            action = function()
              QBCore.Functions.TriggerCallback('qb-realestate:server:GetOwned', function(owned)
                if owned[p.id] then
                  QBCore.Functions.Notify('You already own this property')
                else
                  -- open a simple confirmation UI or use default input
                  TriggerServerEvent('qb-realestate:server:BuyProperty', p.id)
                end
              end)
            end
          }
        },
        distance = 2.5
      })
    end
  end
end)

-- Receive confirmation
RegisterNetEvent('qb-realestate:client:OnBought', function(propertyId)
  QBCore.Functions.Notify('Purchase complete! Property ID: '..propertyId, 'success')
end)

SQL schema (simple)

-- sql/schema.sql
CREATE TABLE IF NOT EXISTS qb_realestate_properties (
  id INT PRIMARY KEY,
  label VARCHAR(255),
  price BIGINT,
  coords TEXT -- JSON or encoded vector
);

CREATE TABLE IF NOT EXISTS qb_realestate_owners (
  id INT AUTO_INCREMENT PRIMARY KEY,
  property_id INT,
  owner VARCHAR(64),
  purchased_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

Where every file belongs & installation

  • Place fxmanifest.lua, config.lua, server/main.lua, client/main.lua in resources/qb-realestate.
  • Place sql/schema.sql in your database migrations folder and run it with your MySQL tool.
  • Add ensure qb-realestate to server.cfg after qb-core and oxmysql.

Validation, testing & security considerations

  • Always validate player existence server-side: QBCore.Functions.GetPlayer(source).
  • Never trust client-side price or ownership flags — use server-side Config.Properties or DB lookups.
  • Use parameterised queries (oxmysql handles placeholders) to avoid SQL injection.
  • Make money removal atomic before the DB write; consider transactional behavior if your DB supports it or compensate on failure.
  • Rate-limit buy attempts server-side if you see abuse patterns.

Quick checklist before launch

StepActionFile/Tool
1Seed propertiessql/schema.sql / config.lua
2Start resourceserver.cfg
3Test buy flowclient/server logs
4Verify DB rowsMySQL
5Attempt failure cases (insufficient funds, duplicate buy)QA

Roblox mapping: server-authoritative equivalent

If you also build a Roblox housing flow, keep the same rules: server validates currency and persists ownership to DataStore. Below is a safe ServerScript example using RemoteEvent & DataStoreService. The client should only send a propertyId request; the server checks currency, writes ownership, and uses pcall for DataStore safety.

// ServerScript (Roblox Lua / Luau)
local DataStoreService = game:GetService('DataStoreService')
local PropertiesStore = DataStoreService:GetDataStore('PropertiesStore')
local BuyEvent = game.ReplicatedStorage:WaitForChild('BuyPropertyEvent')

local function onBuy(player, propertyId, price)
  -- validate server-side: check leaderstats or server currency
  local success, owned = pcall(function()
    return PropertiesStore:GetAsync(player.UserId)
  end)
  if not success then
    BuyEvent:FireClient(player, 'error', 'DataStore error')
    return
  end

  -- check funds (example uses leaderstats currency)
  local leaderstats = player:FindFirstChild('leaderstats')
  local cash = leaderstats and leaderstats:FindFirstChild('Cash')
  if not cash or cash.Value < price then
    BuyEvent:FireClient(player, 'error', 'Insufficient funds')
    return
  end

  -- deduct and persist
  cash.Value = cash.Value - price
  local newOwned = owned or {}
  table.insert(newOwned, propertyId)
  local ok, err = pcall(function()
    PropertiesStore:SetAsync(player.UserId, newOwned)
  end)
  if ok then
    BuyEvent:FireClient(player, 'success', propertyId)
  else
    -- rollback on failure
    cash.Value = cash.Value + price
    BuyEvent:FireClient(player, 'error', 'Failed to save ownership')
  end
end

BuyEvent.OnServerEvent:Connect(onBuy)

Next steps and iteration

Additions you can make next: keyholder permissions, rental/lease timers, in-house storage, customizable interiors, and integration with your existing phone/garage systems. If you want a generator for property files or an interactive planner, try the Stellar AI app for scripting assistance: Stellar AI app. For more background on building flows and iteration patterns, see the guide on our blog: Stellar AI blog. When you're ready to prototype alternative designs or generate full resource files from a plain-English plan, open the Stellar AI app.

Follow the validation checklist above, keep money and DB writes on the server, and iterate with small, tested changes. That approach keeps your real estate system robust and extensible.

Build your next system with Stellar AI

Describe one feature, get organized project files, then bring back your errors to keep improving. Start free with no card required. Test generated code in a private development environment before release.

Create your first script free →