Skip to Content
QBCore docs – powered by Nextra 4
ResourcesQB-Houses - Advanced Housing System

QB-Houses - Advanced Housing System

The qb-houses resource provides an advanced housing system for QBCore servers, featuring property ownership, interior customization, real estate markets, and comprehensive property management.

Overview

QB-Houses creates a realistic property system with buying, selling, renting, decorating, and managing residential properties. Players can own multiple properties, customize interiors, and participate in the real estate market.

Key Features

  • Property Ownership: Buy, sell, and manage properties
  • Interior Decoration: Furniture placement and customization
  • Real Estate Market: Dynamic property pricing
  • Rental System: Property leasing and tenant management
  • Security Systems: Locks, alarms, and access control
  • Utilities Management: Electricity, water, and maintenance
  • Property Types: Houses, apartments, mansions, and commercial spaces
  • Mortgage System: Property financing and payments

Installation

Prerequisites

  • QBCore Framework
  • qb-target (for interaction system)
  • qb-menu (for housing menus)
  • qb-inventory (for storage systems)
  • qb-interior (for interior management)

Installation Steps

  1. Download the Resource
cd resources/[qb] git clone https://github.com/qbcore-framework/qb-houses.git
  1. Database Setup
-- Housing system tables CREATE TABLE IF NOT EXISTS `player_houses` ( `id` int(11) NOT NULL AUTO_INCREMENT, `house` varchar(50) NOT NULL, `identifier` varchar(50) DEFAULT NULL, `citizenid` varchar(50) DEFAULT NULL, `keyholders` longtext DEFAULT NULL, `decorations` longtext DEFAULT NULL, `stash` longtext DEFAULT NULL, `outfit` longtext DEFAULT NULL, `logout` longtext DEFAULT NULL, `mortgage` int(11) DEFAULT 0, `last_payment` timestamp DEFAULT current_timestamp(), PRIMARY KEY (`id`), UNIQUE KEY `house` (`house`) ); CREATE TABLE IF NOT EXISTS `house_plants` ( `id` int(11) NOT NULL AUTO_INCREMENT, `building` varchar(50) DEFAULT NULL, `stage` int(11) DEFAULT 0, `sort` varchar(50) DEFAULT NULL, `coords` longtext DEFAULT NULL, `plantid` varchar(50) DEFAULT NULL, PRIMARY KEY (`id`) ); CREATE TABLE IF NOT EXISTS `property_sales` ( `id` int(11) NOT NULL AUTO_INCREMENT, `property_id` varchar(50) DEFAULT NULL, `seller_citizenid` varchar(50) DEFAULT NULL, `buyer_citizenid` varchar(50) DEFAULT NULL, `sale_price` int(11) DEFAULT 0, `commission` int(11) DEFAULT 0, `sale_date` timestamp DEFAULT current_timestamp(), PRIMARY KEY (`id`) );
  1. Add Job Configuration to qb-core/shared/jobs.lua
['realestate'] = { label = 'Real Estate', defaultDuty = true, offDutyPay = false, grades = { ['0'] = { name = 'Real Estate Agent', payment = 75 }, ['1'] = { name = 'Senior Agent', payment = 100 }, ['2'] = { name = 'Broker', payment = 125, isboss = true }, ['3'] = { name = 'Property Manager', payment = 150, isboss = true } } }
  1. Add Items to qb-core/shared/items.lua
-- Housing Items ['house_key'] = { ['name'] = 'house_key', ['label'] = 'House Key', ['weight'] = 10, ['type'] = 'item', ['image'] = 'house_key.png', ['unique'] = true, ['useable'] = true, ['shouldClose'] = true, ['combinable'] = nil, ['description'] = 'Key to access property' }, ['furniture_chair'] = { ['name'] = 'furniture_chair', ['label'] = 'Chair', ['weight'] = 2000, ['type'] = 'item', ['image'] = 'furniture_chair.png', ['unique'] = false, ['useable'] = true, ['shouldClose'] = true, ['combinable'] = nil, ['description'] = 'Comfortable seating furniture' }, ['furniture_table'] = { ['name'] = 'furniture_table', ['label'] = 'Table', ['weight'] = 3000, ['type'] = 'item', ['image'] = 'furniture_table.png', ['unique'] = false, ['useable'] = true, ['shouldClose'] = true, ['combinable'] = nil, ['description'] = 'Wooden dining table' }
  1. Add to server.cfg
ensure qb-houses

Configuration

Basic Configuration

Config = {} -- General Settings Config.UseTarget = true Config.MortgageEnabled = true Config.RentalSystem = true Config.PropertyTax = 0.02 -- 2% monthly property tax -- Property Categories Config.PropertyTypes = { ["low_end"] = { label = "Starter Homes", price_range = {50000, 150000}, monthly_tax = 500, storage_slots = 50, decoration_limit = 25 }, ["mid_tier"] = { label = "Family Homes", price_range = {150000, 400000}, monthly_tax = 1200, storage_slots = 75, decoration_limit = 50 }, ["high_end"] = { label = "Luxury Properties", price_range = {400000, 1000000}, monthly_tax = 3000, storage_slots = 100, decoration_limit = 100 }, ["mansion"] = { label = "Mansions", price_range = {1000000, 5000000}, monthly_tax = 8000, storage_slots = 200, decoration_limit = 200 } } -- Default House Locations Config.Houses = { ["house1"] = { coords = {x = -174.35, y = 497.69, z = 137.64, h = 92.4}, price = 200000, type = "mid_tier", interior = "modern_apartment", garage = { coords = {x = -180.35, y = 502.69, z = 135.64, h = 92.4}, size = 2 } }, ["house2"] = { coords = {x = -681.85, y = 596.66, z = 145.38, h = 225.5}, price = 350000, type = "mid_tier", interior = "house_hi_end_v2", garage = { coords = {x = -685.85, y = 590.66, z = 143.38, h = 225.5}, size = 3 } }, -- Add more houses as needed } -- Interior Types Config.Interiors = { ["modern_apartment"] = { label = "Modern Apartment", spawn = {x = -174.35, y = 497.69, z = 137.64, h = 92.4}, stash = {x = -168.35, y = 487.69, z = 137.64}, logout = {x = -172.35, y = 493.69, z = 137.64}, storage_slots = 50, price = 25000 }, ["house_hi_end_v2"] = { label = "High-End House", spawn = {x = -681.85, y = 596.66, z = 145.38, h = 225.5}, stash = {x = -670.85, y = 588.66, z = 145.38}, logout = {x = -675.85, y = 592.66, z = 145.38}, storage_slots = 75, price = 50000 } } -- Furniture Categories Config.Furniture = { ["seating"] = { label = "Seating", items = { ["furniture_chair"] = { label = "Chair", model = "p_armchair_01_s", price = 500, width = 1.2, length = 1.2 }, ["furniture_sofa"] = { label = "Sofa", model = "p_sofa_01_s", price = 1500, width = 2.5, length = 1.0 } } }, ["tables"] = { label = "Tables", items = { ["furniture_table"] = { label = "Dining Table", model = "p_dining_table_01_s", price = 800, width = 2.0, length = 1.2 }, ["furniture_coffee_table"] = { label = "Coffee Table", model = "p_coffee_table_01_s", price = 400, width = 1.5, length = 0.8 } } }, ["storage"] = { label = "Storage", items = { ["furniture_wardrobe"] = { label = "Wardrobe", model = "p_wardrobe_01_s", price = 1200, width = 1.5, length = 0.8, storage = true } } } }

Mortgage System

-- Mortgage Configuration Config.MortgageSystem = { enabled = true, down_payment_min = 0.10, -- 10% minimum down payment interest_rates = { excellent_credit = 0.035, -- 3.5% APR good_credit = 0.045, -- 4.5% APR fair_credit = 0.055, -- 5.5% APR poor_credit = 0.075 -- 7.5% APR }, term_options = {15, 20, 25, 30}, -- Years payment_frequency = 30, -- Days between payments late_fee = 0.05, -- 5% late fee foreclosure_days = 90 -- Days before foreclosure } -- Real Estate Market Config.MarketSystem = { price_fluctuation = true, max_price_change = 0.15, -- 15% max change per month market_factors = { location_demand = 0.3, property_condition = 0.25, local_economy = 0.25, seasonal_trends = 0.2 }, commission_rates = { seller = 0.06, -- 6% seller commission buyer = 0.03 -- 3% buyer commission } }

API Reference

Client Exports

EnterHouse

Enter a house interior.

exports['qb-houses']:EnterHouse(houseId, spawn) -- Parameters: -- houseId: House identifier -- spawn: Spawn coordinates (optional)

ExitHouse

Exit house interior.

exports['qb-houses']:ExitHouse(exitCoords) -- Parameters: -- exitCoords: Exit coordinates (optional)

PlaceFurniture

Place furniture item in house.

local success = exports['qb-houses']:PlaceFurniture(furnitureType, coords, rotation) -- Parameters: -- furnitureType: Type of furniture to place -- coords: Placement coordinates -- rotation: Furniture rotation

Server Exports

CreateHouse

Create a new house.

local houseId = exports['qb-houses']:CreateHouse(houseData) -- Parameters: -- houseData: House configuration data

SetHouseOwner

Set house ownership.

exports['qb-houses']:SetHouseOwner(houseId, citizenId) -- Parameters: -- houseId: House identifier -- citizenId: New owner citizen ID

AddKeyHolder

Add keyholder to property.

local success = exports['qb-houses']:AddKeyHolder(houseId, citizenId) -- Parameters: -- houseId: House identifier -- citizenId: Keyholder citizen ID

Events

Client Events

-- House entered RegisterNetEvent('qb-houses:client:houseEntered', function(houseId) -- Handle house entry end) -- Furniture placed RegisterNetEvent('qb-houses:client:furniturePlaced', function(furnitureData) -- Handle furniture placement end) -- House purchased RegisterNetEvent('qb-houses:client:housePurchased', function(houseData) -- Handle house purchase end)

Server Events

-- House ownership changed RegisterNetEvent('qb-houses:server:ownershipChanged', function(houseId, oldOwner, newOwner) -- Handle ownership change end) -- Mortgage payment due RegisterNetEvent('qb-houses:server:mortgagePaymentDue', function(citizenId, amount) -- Handle mortgage payment end) -- Property tax due RegisterNetEvent('qb-houses:server:propertyTaxDue', function(houseId, amount) -- Handle property tax end)

Usage Examples

House Purchase System

-- Client-side house purchasing RegisterNetEvent('qb-houses:client:purchaseHouse', function(houseId) local houseData = Config.Houses[houseId] if not houseData then QBCore.Functions.Notify("House not found", "error") return end -- Check if house is available QBCore.Functions.TriggerCallback('qb-houses:server:isHouseAvailable', function(isAvailable) if not isAvailable then QBCore.Functions.Notify("This property is not available", "error") return end -- Create purchase options menu local purchaseMenu = { { header = "Purchase Property", txt = "Price: $" .. houseData.price, isMenuHeader = true }, { header = "Cash Purchase", txt = "Pay full amount upfront", params = { event = "qb-houses:client:processPurchase", args = { houseId = houseId, method = "cash" } } }, { header = "Mortgage Purchase", txt = "Finance with monthly payments", params = { event = "qb-houses:client:showMortgageOptions", args = { houseId = houseId } } }, { header = "Schedule Viewing", txt = "Tour the property first", params = { event = "qb-houses:client:scheduleViewing", args = { houseId = houseId } } } } exports['qb-menu']:openMenu(purchaseMenu) end, houseId) end) -- Process cash purchase RegisterNetEvent('qb-houses:client:processPurchase', function(data) local houseData = Config.Houses[data.houseId] QBCore.Functions.TriggerCallback('qb-houses:server:canAffordHouse', function(canAfford) if canAfford then TriggerServerEvent('qb-houses:server:buyHouse', data.houseId, data.method) else QBCore.Functions.Notify("Insufficient funds", "error") end end, houseData.price) end) -- Server-side house purchase RegisterNetEvent('qb-houses:server:buyHouse', function(houseId, method) local src = source local player = QBCore.Functions.GetPlayer(src) local houseData = Config.Houses[houseId] if not player or not houseData then return end -- Check if house is still available local existingOwner = MySQL.scalar.await('SELECT citizenid FROM player_houses WHERE house = ?', {houseId}) if existingOwner then TriggerClientEvent('QBCore:Notify', src, 'Property no longer available', 'error') return end if method == "cash" then if player.Functions.RemoveMoney('bank', houseData.price, 'house-purchase') then -- Create house ownership record MySQL.insert('INSERT INTO player_houses (house, identifier, citizenid, keyholders, decorations, stash, outfit, logout) VALUES (?, ?, ?, ?, ?, ?, ?, ?)', { houseId, player.PlayerData.license, player.PlayerData.citizenid, json.encode({}), json.encode({}), json.encode({}), json.encode({}), json.encode({}) })\n \n -- Give house key\n player.Functions.AddItem('house_key', 1, false, {\n house = houseId,\n label = houseData.label or \"House Key\"\n })\n \n TriggerClientEvent('QBCore:Notify', src, 'House purchased successfully!', 'success')\n TriggerClientEvent('qb-houses:client:housePurchased', src, {house = houseId, method = method})\n \n -- Log the sale\n MySQL.insert('INSERT INTO property_sales (property_id, buyer_citizenid, sale_price, sale_date) VALUES (?, ?, ?, NOW())', {\n houseId,\n player.PlayerData.citizenid,\n houseData.price\n })\n else\n TriggerClientEvent('QBCore:Notify', src, 'Payment failed', 'error')\n end\n end\nend)\n```\n\n### Interior Decoration System\n\n```lua\n-- Furniture placement system\nlocal decoratingMode = false\nlocal selectedFurniture = nil\nlocal furniturePreview = nil\n\nRegisterNetEvent('qb-houses:client:enterDecoratingMode', function(houseId)\n decoratingMode = true\n \n QBCore.Functions.Notify(\"Decorating mode enabled. Use mouse to place furniture\", \"info\")\n \n -- Create decorating menu\n local decorMenu = {\n {\n header = \"Furniture Catalog\",\n isMenuHeader = true\n }\n }\n \n for category, categoryData in pairs(Config.Furniture) do\n table.insert(decorMenu, {\n header = categoryData.label,\n txt = \"Browse \" .. categoryData.label:lower(),\n params = {\n event = \"qb-houses:client:browseFurniture\",\n args = {\n category = category,\n houseId = houseId\n }\n }\n })\n end\n \n table.insert(decorMenu, {\n header = \"Exit Decorating\",\n txt = \"Return to normal mode\",\n params = {\n event = \"qb-houses:client:exitDecoratingMode\"\n }\n })\n \n exports['qb-menu']:openMenu(decorMenu)\n \n -- Start decoration thread\n CreateThread(function()\n while decoratingMode do\n Wait(1)\n \n if selectedFurniture then\n -- Handle furniture placement\n local hit, coords, entity = GetCursorPosition()\n \n if hit then\n -- Update preview position\n if furniturePreview then\n SetEntityCoords(furniturePreview, coords.x, coords.y, coords.z)\n end\n \n -- Place furniture on click\n if IsControlJustPressed(0, 24) then -- Left mouse button\n PlaceFurnitureAtPosition(selectedFurniture, coords, houseId)\n selectedFurniture = nil\n \n if furniturePreview then\n DeleteEntity(furniturePreview)\n furniturePreview = nil\n end\n end\n end\n \n -- Cancel placement\n if IsControlJustPressed(0, 25) then -- Right mouse button\n selectedFurniture = nil\n \n if furniturePreview then\n DeleteEntity(furniturePreview)\n furniturePreview = nil\n end\n end\n end\n end\n end)\nend)\n\nfunction PlaceFurnitureAtPosition(furnitureType, coords, houseId)\n local furnitureData = GetFurnitureData(furnitureType)\n if not furnitureData then return end\n \n -- Check if player owns furniture item\n QBCore.Functions.TriggerCallback('qb-houses:server:hasFurnitureItem', function(hasItem)\n if hasItem then\n -- Place furniture\n TriggerServerEvent('qb-houses:server:placeFurniture', houseId, furnitureType, coords, 0.0)\n else\n QBCore.Functions.Notify(\"You don't have this furniture item\", \"error\")\n end\n end, furnitureType)\nend\n\n-- Server-side furniture placement\nRegisterNetEvent('qb-houses:server:placeFurniture', function(houseId, furnitureType, coords, rotation)\n local src = source\n local player = QBCore.Functions.GetPlayer(src)\n \n if not player then return end\n \n -- Verify house ownership\n local houseOwnership = MySQL.query.await('SELECT * FROM player_houses WHERE house = ? AND citizenid = ?', {\n houseId, player.PlayerData.citizenid\n })\n \n if not houseOwnership[1] then\n TriggerClientEvent('QBCore:Notify', src, 'You don\\'t own this property', 'error')\n return\n end\n \n -- Remove furniture item from inventory\n if player.Functions.RemoveItem(furnitureType, 1) then\n -- Get current decorations\n local currentDecorations = json.decode(houseOwnership[1].decorations) or {}\n \n -- Add new furniture\n local furnitureId = GenerateUniqueId()\n currentDecorations[furnitureId] = {\n type = furnitureType,\n coords = coords,\n rotation = rotation,\n placed_date = os.date(\"%Y-%m-%d %H:%M:%S\")\n }\n \n -- Update database\n MySQL.update('UPDATE player_houses SET decorations = ? WHERE house = ?', {\n json.encode(currentDecorations),\n houseId\n })\n \n TriggerClientEvent('QBCore:Notify', src, 'Furniture placed successfully!', 'success')\n TriggerClientEvent('qb-houses:client:spawnFurniture', src, furnitureId, currentDecorations[furnitureId])\n else\n TriggerClientEvent('QBCore:Notify', src, 'You don\\'t have this furniture item', 'error')\n end\nend)\n```\n\n### Rental Management System\n\n```lua\n-- Property rental system\nRegisterNetEvent('qb-houses:client:manageRentals', function(houseId)\n QBCore.Functions.TriggerCallback('qb-houses:server:getRentalInfo', function(rentalData)\n local rentalMenu = {\n {\n header = \"Rental Management\",\n txt = \"Property: \" .. houseId,\n isMenuHeader = true\n }\n }\n \n if rentalData.current_tenant then\n table.insert(rentalMenu, {\n header = \"Current Tenant\",\n txt = rentalData.tenant_name .. \" - $\" .. rentalData.monthly_rent .. \"/month\",\n params = {\n event = \"qb-houses:client:manageTenant\",\n args = {\n houseId = houseId,\n tenantId = rentalData.current_tenant\n }\n }\n })\n else\n table.insert(rentalMenu, {\n header = \"List for Rent\",\n txt = \"Set rental price and list property\",\n params = {\n event = \"qb-houses:client:listForRent\",\n args = {\n houseId = houseId\n }\n }\n })\n end\n \n table.insert(rentalMenu, {\n header = \"Rental History\",\n txt = \"View past tenants and payments\",\n params = {\n event = \"qb-houses:client:viewRentalHistory\",\n args = {\n houseId = houseId\n }\n }\n })\n \n exports['qb-menu']:openMenu(rentalMenu)\n \n end, houseId)\nend)\n\n-- List property for rent\nRegisterNetEvent('qb-houses:client:listForRent', function(data)\n local input = exports['qb-input']:ShowInput({\n header = \"List Property for Rent\",\n submitText = \"List Property\",\n inputs = {\n {\n text = \"Monthly Rent ($)\",\n name = \"rent_amount\",\n type = \"number\",\n isRequired = true\n },\n {\n text = \"Security Deposit ($)\",\n name = \"security_deposit\",\n type = \"number\",\n isRequired = true\n },\n {\n text = \"Lease Term (months)\",\n name = \"lease_term\",\n type = \"number\",\n isRequired = true\n }\n }\n })\n \n if input then\n TriggerServerEvent('qb-houses:server:listPropertyForRent', data.houseId, {\n rent_amount = tonumber(input.rent_amount),\n security_deposit = tonumber(input.security_deposit),\n lease_term = tonumber(input.lease_term)\n })\n end\nend)\n```\n\n## Troubleshooting\n\n### Common Issues\n\n#### Interior Loading Problems\n```lua\n-- Check interior existence\nfunction ValidateInterior(interiorType)\n local interiorData = Config.Interiors[interiorType]\n if not interiorData then\n print(\"Interior type not found:\", interiorType)\n return false\n end\n return true\nend\n```\n\n#### Furniture Placement Issues\n- Verify furniture model existence in game files\n- Check collision detection for placement validation\n- Ensure proper coordinate transformation for interiors\n\n#### Ownership Sync Problems\n```lua\n-- Debug house ownership\nRegisterCommand('checkhouse', function(source, args)\n local houseId = args[1]\n if houseId then\n local ownership = MySQL.query.await('SELECT * FROM player_houses WHERE house = ?', {houseId})\n if ownership[1] then\n print(\"House owner:\", ownership[1].citizenid)\n print(\"Keyholders:\", ownership[1].keyholders)\n else\n print(\"House not owned:\", houseId)\n end\n end\nend)\n```\n\n### Debug Commands\n\n```lua\n-- Give house key\n/givehousekey [player_id] [house_id]\n\n-- Set house owner\n/sethouseowner [house_id] [citizen_id]\n\n-- Add furniture item\n/givefurniture [player_id] [furniture_type] [amount]\n\n-- Check property value\n/propertyvalue [house_id]\n```\n\n<Callout type=\"info\">\n Regular maintenance of property databases and monitoring of the real estate market helps maintain server economy balance.\n</Callout>
Last updated on