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
| Step | Action | File/Tool |
|---|---|---|
| 1 | Seed properties | sql/schema.sql / config.lua |
| 2 | Start resource | server.cfg |
| 3 | Test buy flow | client/server logs |
| 4 | Verify DB rows | MySQL |
| 5 | Attempt 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.