A practical guide to how to add a car dealership to your qbcore fivem server, with implementation decisions, validation steps, and security considerations for a production-minded project.
Overview
Adding a car dealership to a QBCore FiveM server is a multi-layer task: it touches client UI, server-side validation, persistent storage, and anticheat/logging. This guide focuses on a secure, maintainable architecture that never trusts client input and uses QBCore patterns (and optional ox_lib/ox_target integrations) to create a robust purchase-and-ownership flow. For deployment and analytics integration, consider tools such as the Stellar AI app for team collaboration and monitoring — see the Stellar AI app and projects list for quick setup: Stellar AI app.
Architecture and Components
Core components
- Client UI: dealership menus, preview camera, purchase confirmation (purely visual; no trust)
- Server controller: request handling, validation, funds management, vehicle creation record
- Database: persistent vehicle ownership table and transaction log
- Anti-cheat and audit: rate limiting, anomaly detection, logs
Typical data flow
- Client opens dealership UI and selects a model (client-side only).
- Client requests purchase by calling a server RPC/event with chosen model id.
- Server validates model against a maintained whitelist, checks price, and verifies funds.
- Server applies transaction (atomic where possible), inserts ownership record, and responds with success/failure.
- Client receives confirmation and spawns owned vehicle only after server approves and returns spawn token/plate.
Implementation Choices
QBCore integration
Use the QBCore core object on the server: local QBCore = exports['qb-core']:GetCoreObject(). Implement server callbacks or server events that perform all checks. Avoid placing pricing or model whitelist in client files; client may cache read-only copies for UI, but server must hold authoritative config.
Optional libraries
- ox_lib for menus and notifications (client-side UX).
- ox_target for interaction points (approach dealership sign to open catalog).
- oxmysql or MySQL Async for robust DB operations and transaction management.
Server-Side Validation and Security
Never trust the client. All financial and ownership decisions must be done on the server. Do not accept client-supplied price, plate, or ownership flags. Maintain a server-side vehicle catalog with model, display name, and price and validate purchases against that catalog.
Key validation steps
- Confirm model exists in server whitelist and retrieve authoritative price.
- Check that the requesting player exists (QBCore.Functions.GetPlayer(source)) and that they have sufficient funds in the correct wallet type.
- Use database transactions where available; if insertion fails, refund immediately and log the incident.
- Rate-limit purchases per player to prevent automated abuse.
- Log purchase attempts including mismatched price, invalid model, or DB errors for later review.
Example server flow (Lua, QBCore)
local QBCore = exports['qb-core']:GetCoreObject()
local vehicleCatalog = {
['t20'] = { price = 1_500_000, name = 'T20' },
['zentorno'] = { price = 2_000_000, name = 'Zentorno' },
-- server-authoritative list
}
QBCore.Functions.CreateCallback('qb-dealer:server:purchase', function(source, cb, model)
local src = source
local Player = QBCore.Functions.GetPlayer(src)
if not Player then return cb(false, 'no_player') end
local catalogItem = vehicleCatalog[model]
if not catalogItem then
-- invalid model attempted
return cb(false, 'invalid_model')
end
local price = catalogItem.price
if not Player.Functions.RemoveMoney('bank', price) then
return cb(false, 'insufficient_funds')
end
-- generate plate and ownership metadata server-side
local plate = 'DLR' .. tostring(math.random(1000,9999))
-- insert into owned_vehicles with a safe DB call
exports.oxmysql:insert('INSERT INTO owned_vehicles (owner, plate, model, metadata) VALUES (?, ?, ?, ?)', {
Player.PlayerData.citizenid,
plate,
model,
json.encode({ state = 'stored', purchasePrice = price })
}, function(insertId)
if not insertId then
-- DB error: refund and log
Player.Functions.AddMoney('bank', price)
-- log warning to server logs
return cb(false, 'db_error')
end
-- success: return plate and model, server-authoritative
return cb(true, { plate = plate, model = model })
end)
end)
Database Schema and Transactions
Keep a minimal and indexed ownership table. Use transactions when possible to ensure atomicity between money removal and ownership creation.
| Table | Key Columns | Notes |
|---|---|---|
| owned_vehicles | id (pk), owner (citizenid), plate, model, metadata (json), created_at | index on owner and plate for fast lookups |
| dealer_transactions | id, owner, model, price, status, error_code, created_at | transaction history for audits and refunds |
Best practices
- Wrap multi-step DB modifications (debit + insert) in transactions if your driver supports them.
- Retain a transaction log with error_code to enable automatic reconciliation scripts.
- Sanitize data and use prepared statements or parameterized queries (oxmysql handles parameters).
UI & Interaction Patterns
The client UI should provide previews and selection UX only. Use server callbacks to confirm availability and final price before performing a purchase.
Approach patterns
- Use ox_target to trigger a "Open Catalog" client interaction when the player approaches a dealer NPC or showroom.
- On selection, call a server callback to get the latest price and availability; show a final confirmation dialog client-side.
- On final confirm, call the purchase server callback — do not pass sensitive values like price or plate from client side.
Testing and QA
The testing phase should include unit, integration, and abuse testing. Test both normal and failure paths: DB downtime, insufficient funds, and malformed client messages. Use the following checklist to validate behavior.
| Test | Purpose | Expected Result |
|---|---|---|
| Purchase success | Standard buy flow | Funds removed, owned_vehicles entry created, client receives plate |
| Insufficient funds | Ensure server denies and no DB insert | No DB entry, no funds removed |
| Invalid model | Client submits model not in server list | Purchase rejected, logged |
| DB failure during insert | Simulate DB timeout/error | Funds refunded, alert logged |
| Rate limit abuse | Repeated rapid requests | Requests throttled with warning and logs |
Deployment and Maintenance
Deploy the dealership as a versioned resource with migrations for DB schema changes. Keep config in a separate server-side file and support remote-reload of catalogs if your workflow requires live catalog updates.
Operational tips
- Run weekly automated backups of DB and validate restores.
- Monitor transaction failure rates and set alerts for consecutive DB errors.
- Maintain an audit trail (dealer_transactions) that includes error_code and stack traces when available.
- Use CI to lint Lua, run unit tests, and verify that server-side callbacks cover all edge cases.
- For collaboration and issue tracking, integrating a project workspace such as the Stellar AI app can centralize logs and tasks: Stellar AI app. For editorial and tutorial updates, review the team blog page: Stellar AI blog.
Roblox (Luau) Considerations — Brief Notes
If you implement a similar dealership pattern on Roblox, the same server-authority principle applies. Use RemoteEvents/RemoteFunctions carefully: all purchase validation must occur in a server script that checks player currency in a server-side datastore. Do not trust client-sent prices or item IDs.
Roblox best practices
- Perform DataStore saves with retry/backoff and handle failure by queueing reconciliation or notifying the player.
- Use server-only modules for catalog config; clients only request previews. Provide limited time tokens for spawns only after server confirms purchase.
- Gracefully handle DataStore throttling/failure: inform the player, and implement a retry or manual support flow.
Monitoring, Logging, and Anti-Cheat
Logging is essential. Track suspicious purchase attempts, mismatched prices, and repeated failures. Integrate server logs with your monitoring platform or use a centralized workspace to triage issues. Consider:
- Request rate and pattern analysis per player
- Automated alerts for DB insert failures above a threshold
- Manual review workflows for refunds or disputes