A practical guide to how to add a housing system to your qbcore fivem server, with implementation decisions, validation steps, and security considerations for a production-minded project.
Overview
Adding a housing system to a QBCore FiveM server is a multi-layered task that touches on database design, server-side validation, interior streaming, permissions, and long-term maintenance. This guide focuses on architecture and practical implementation choices for FiveM (QBCore/ESX) and also includes a short section covering Roblox patterns for teams building similar persistent housing systems. It assumes you have basic familiarity with creating resources (fxmanifest.lua), registering server events, and working with a MySQL backend (oxmysql or similar).
Architecture: server-first, authoritative design
The single most important principle is server authority. Never trust the client for ownership, purchase confirmation, or permission checks. Architect the system so that the server owns the canonical state (who owns which house, lock status, inventory associations, visited metadata). The client should only send intent requests (e.g., "attempt to buy house X" or "request interior for house X") and the server must validate each request against its database and permission rules before responding.
Typical components:
- Server resource: housing logic, database queries, validation, scheduled tasks (rent, eviction, backups)
- Client resource: UI, animations, interior streaming triggers, local door physics — only rendered after server confirmation
- Database: houses table, ownership history, interior metadata, persistent storage for furniture or inventory
- Optional integrations: inventory systems, job permissions, gang ownerships, qb-phone or qb-menu UIs, ox_target/ox_lib for interaction
For design and prototyping of layouts consider visual tools or remote design environments to capture coordinates and interior models. If you want a quick prototyping tool for visuals or collaboration, try the Stellar AI app for design workflows: https://trystellarai.com/app.
Data model and schema recommendations
A clear schema is critical for performance and maintainability. Use normalized tables where appropriate, and JSON columns for flexible metadata (furniture config, custom decorations) if your MySQL version supports it.
Core tables:
- houses — id (PK), name, model, price, coords (vector as separate x,y,z or JSON), interior_id, created_at
- house_owners — id, house_id (FK), owner_citizenid, purchased_at, access_level (owner, coowner, tenant)
- house_metadata — house_id, key, value (JSON) — for furniture states, decorations
- house_logs — action, actor, timestamp, details (audit trail for purchases/locks)
Indexing: add indexes on house_id, owner_citizenid. For common queries (fetch houses by owner), a composite index speeds up lookups.
-- Example minimal SQL (MySQL syntax)
CREATE TABLE houses (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(100),
model VARCHAR(50),
price BIGINT,
coord_x DOUBLE,
coord_y DOUBLE,
coord_z DOUBLE,
interior_id VARCHAR(50),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE house_owners (
id INT AUTO_INCREMENT PRIMARY KEY,
house_id INT,
owner_citizenid VARCHAR(64),
purchased_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
access_level VARCHAR(20),
INDEX (house_id),
INDEX (owner_citizenid)
);
Server-side validation and security best practices
Security is not an afterthought. Implement these validation and hardening steps:
- Always verify ownership server-side before awarding access or toggling locks. For example, when a client requests to open a door, check that the source player's citizenid matches an entry in house_owners with appropriate access.
- Use parameterized queries or the safe execution methods provided by your MySQL binding (oxmysql exports) to avoid SQL injection.
- Rate-limit potentially costly requests from the client (e.g., repeated purchase attempts). Implement server-side cooldowns or checks per player ID.
- Log sensitive actions (purchases, transfers, kicks) to a secure log with rotation. Keep an audit trail for debugging and abuse investigations.
- Validate any numeric values (prices, IDs) on the server — do not accept price or house id values directly from the client without re-fetching them from the DB.
- Use permissions/groups from QBCore or ESX to gate admin-only actions like force assignment, eviction, or global resets.
Example server validation rule: when processing a purchase, check that the house is not already owned in the database, ensure the player has enough money via server-side wallet APIs, then atomically insert or update owner records inside a single transaction (or use consistent checks to avoid race conditions).
Implementation choices and integrations
Decide early whether houses are static world objects with an exterior marker and separate interiors or entirely dynamic interiors streamed in. Common approaches:
- Static exterior + prebuilt interior IPL: faster and simpler for fewer interior types; use object streaming via interior maps or YMAPs.
- Dynamic instance interiors: spawn a private instance per owner (more complex but supports unique furniture per player).
- Hybrid: shared interior templates with per-house metadata for furniture positions and decorations.
Useful QBCore/ESX/ox_lib integrations:
- ox_target/ox_lib for interaction targeting on doors, safes, and fridges.
- qb-phone integration for notifications and remote management.
- inventory linking: store house-stored items under a container ID that maps to house_id.
For client UI and menu patterns, use server callbacks to fetch ownership and metadata. For example, qb-core-style callbacks should return only validated data and never rely on client-provided house IDs without server-side cross-checks.
If you need a fast prototyping or visual planning tool for interiors, consider external design apps — some developers use collaborative design tools like the Stellar AI app for rapid iteration: https://trystellarai.com/app.
Roblox/Luau notes: server authority, RemoteEvents, and DataStore failure handling
If you implement persistent housing on Roblox, the same server-first philosophy applies. Roblox servers should be authoritative: client requests for house actions must be validated on the server using RemoteEvents or RemoteFunctions. Never perform crucial state transitions only on the client.
DataStore considerations:
- Always wrap DataStore calls in pcall and implement retries with exponential backoff. DataStores can fail or return throttling errors; handle those gracefully and surface transient errors to the user with retry options.
- Prefer UpdateAsync for safe, atomic changes where possible. Use DataStore keys that are namespaced per-player or per-house to avoid conflicts.
- Implement a local server cache for frequently accessed house-lookups, backed by periodic persistence to the DataStore to reduce throttling pressure.
Example pattern for RemoteEvent handling: validate the player, verify ownership in server memory/cache, then perform DataStore writes with pcall and retry logic. If a DataStore write fails after retries, log the error, revert in-memory changes, and notify the player to try again later.
Testing, QA, and debugging strategies
Testing is essential before deploying to a live server. Focus on these areas:
- Unit tests for server validation functions — e.g., owner lookup, permission checks.
- Integration tests for DB transactions (purchase flow, transfer flow) using a staging database.
- Simulated multi-player sessions: stress test concurrent purchases and entry requests to reveal race conditions.
- Security tests: attempt to spoof client requests and ensure server-side checks reject malformed or unauthorized actions.
- Performance profiling: measure common queries, identify slow JOINs, and add indexes or caching where necessary.
Maintain verbose debug logs behind a toggle so you can replicate an issue with detailed traces without spamming production logs in normal operation.
Deployment, migrations, and maintenance
Production readiness checklist and ongoing maintenance tasks:
- Database migration strategy: include versioned SQL migrations, and test them on a staging DB before production.
- Backups: schedule regular DB backups and store backups off-site. Always test restores periodically.
- Rolling updates: when updating the housing resource, use a short maintenance window or staged restarts; persist transient state before restart.
- Monitor: track error rates from server logs and increase logging for new features to capture edge cases.
- User data portability: provide admin tools to transfer ownership or export house metadata if required by players or moderators.
Practical checklist
| Task | Priority | Notes |
|---|---|---|
| Server ownership verification | High | Implement atomic checks before granting access |
| Parameterized DB queries | High | Use oxmysql or equivalent execute APIs |
| Interior streaming strategy | Medium | Choose static IPLs or dynamic instances |
| Data backups & migrations | High | Versioned SQL and restore testing |
| Integration with inventory/phone | Medium | Map house storage to container IDs |
| Roblox DataStore retry logic | High (Roblox) | Wrap in pcall, backoff retries, UpdateAsync |
Example server-side snippets
Below are compact examples illustrating server validation patterns. Adapt these to your QBCore resource layout and MySQL binding.
-- QBCore server: validated purchase example (Lua for fxserver)
RegisterNetEvent('qb-housing:server:buyHouse', function(houseId)
local src = source
local Player = QBCore.Functions.GetPlayer(src)
if not Player then return end
-- server-only lookup: fetch house record
exports.oxmysql:execute("SELECT price FROM houses WHERE id = ?", {houseId}, function(result)
if not result or #result == 0 then
TriggerClientEvent('QBCore:Notify', src, 'House not available', 'error')
return
end
local price = result[1].price
-- check funds on server
if Player.Functions.RemoveMoney('bank', price) then
-- insert owner record safely
exports.oxmysql:execute("INSERT INTO house_owners (house_id, owner_citizenid, access_level) VALUES (?, ?, ?)",
{houseId, Player.PlayerData.citizenid, 'owner'}, function(insertResult)
TriggerClientEvent('QBCore:Notify', src, 'Purchase successful', 'success')
-- log and further server-side setup
end)
else
TriggerClientEvent('QBCore:Notify', src, 'Insufficient funds', 'error')
end
end)
end)
-- Roblox server Lua sample (Luau)
local Remote = game.ReplicatedStorage:WaitForChild("HouseRemote")
Remote.OnServerEvent:Connect(function(player, action, houseId)
-- validate action and player on server
if action == "EnterHouse" then
-- validate ownership via server cache or DataStore lookup
local owner = serverCache:GetOwner(houseId)
if owner == player.UserId then
-- allow teleport to interior; do not trust client data
TeleportPlayerToInterior(player, houseId)
else
Remote:FireClient(player, "AccessDenied")
end
end
end)
-- DataStore write with retry
local function SaveHouseData(key, data)
local attempts = 0
while attempts < 3 do
local success, result = pcall(function()
return DataStore:SetAsync(key, data)
end)
if success then return true end
attempts = attempts + 1
wait(2 ^ attempts) -- exponential backoff
end
warn("Failed to save house data:", key)
return false
end
Monitoring and rollback planning
Plan for rollbacks: if a new housing update introduces data model changes, provide scripts to downgrade or migrate back safely. Maintain backward compatibility when possible and ensure migration scripts are idempotent. Monitor for anomalies after deployment (e.g., spike in failed purchases) and have a staged rollback plan.