Runs on: server. This file stays editable when the resource is escrow-protected. Restart the resource after editing.
Server-only file: players never receive it, so it is the right place for passwords and hidden settings.
config/config.server.lua — database, money and hooks.
Database tables
Config.DB = {
licenses = 'aps_licenses', cooldowns = 'aps_license_cooldowns',
documents = 'aps_documents', insurance = 'aps_vehicle_insurance',
bills = 'aps_bills', gameScores = 'aps_game_scores', businesses = 'aps_businesses',
ownedVehicles = 'auto', -- owned_vehicles (ESX) / player_vehicles (QBCore, Qbox)
vehicles = 'auto', -- `vehicles` table (ESX) / shared vehicles (QBCore, Qbox)
}
vehicles to a table name (columns name, model, price, category) to read the catalog from the database on any framework.
Owned vehicle columns
'auto' uses the framework’s default columns. Set a table if your garage script names them differently.
Config.OwnedVehiclesColumns = 'auto'
-- ESX { owner = 'owner', plate = 'plate', vehicle = 'vehicle', type = 'type', stored = 'stored' }
-- QBCore { owner = 'citizenid', plate = 'plate', vehicle = 'vehicle', stored = 'state' }
-- vehicle: JSON props with a `model` field, or a plain model name
Security
Config.Security = {
callbackLimit = 40, -- requests a player may send …
callbackWindow = 5, -- … per 5 seconds (0 = no limit)
location = {
enabled = true, -- paid / state-changing actions only near a City Hall
distance = 10.0, -- metres from a Config.Locations entry
},
}
possible cheat menu). Every purchase, ownership, job and permission check runs on the server; the client only shows results.
location stops cheat menus from using City Hall from anywhere on the map: taking tests, ordering documents, buying insurance, paying bills and taxes, business actions, Job Center applications, collecting paychecks and the hidden console commands only work within distance of a City Hall. Viewing data, the phone app, mail and the Government Portal (which has its own access checks) work everywhere.
Opening the PC away from City Hall:
Config.Command.enabled = trueturns the location check off, since the command works anywhere.- From a server script (laptop item, MDT…), use
exports['aps_cityhall']:openCityHall(source). That PC session skips the check until the player closes it. - If you open it with the client export
OpenCityHall()away from City Hall, setlocation.enabled = false.
Money & payment
| Option | Default | Description |
|---|---|---|
Config.MoneyAccount | 'bank' | Account used for every fee: bank, cash, black_money (ESX) or crypto (QBCore). cash and money mean the same on every framework. |
Config.PaymentItem | nil | Item name that pays for theory tests instead of money (e.g. 'theory_voucher'). Uses Config.ItemCheckFn / Config.ItemRemoveFn (default: Config.Inventory), which you can adapt to any inventory. |
Config.FailRefundPct | 0.0 | Share of the test fee refunded on a failed test (0.5 = half). |
Config.BillsHistoryDays | 60 | Days of bill history shown to players. |
Config.NotifyExpiryDays | 3 | Days before expiry that insurance and documents show up in the notification area. |
Full View
Preview File Updated: 3.7.0 - 02.11.2026config/config.server.lua
Config = Config or {}
-- ─────────────────────────────────────────────
-- Database table names
-- ─────────────────────────────────────────────
Config.DB = {
licenses = 'aps_licenses',
cooldowns = 'aps_license_cooldowns',
documents = 'aps_documents',
insurance = 'aps_vehicle_insurance',
bills = 'aps_bills',
gameScores = 'aps_game_scores',
businesses = 'aps_businesses',
govLog = 'aps_gov_log',
mugshots = 'aps_mugshots',
citizens = 'aps_citizens', -- SSNs for frameworks without one
businessPermits = 'aps_business_permits',
businessVehicles = 'aps_business_vehicles',
businessEmployees = 'aps_business_employees',
businessInspections = 'aps_business_inspections',
taxes = 'aps_taxes',
businessTaxes = 'aps_business_taxes',
treasury = 'aps_treasury',
treasuryLog = 'aps_treasury_log',
scheduler = 'aps_scheduler',
suspensions = 'aps_license_suspensions',
penaltyPoints = 'aps_penalty_points',
paychecks = 'aps_paychecks',
paycheckLog = 'aps_paycheck_log',
resumes = 'aps_resumes',
resumeSettings = 'aps_resume_settings',
-- Framework vehicle tables (insurance + vehicle registry)
-- ownedVehicles 'auto' = owned_vehicles (ESX) / player_vehicles (QBCore, Qbox)
-- vehicles 'auto' = `vehicles` table (ESX) / shared vehicles (QBCore, Qbox)
-- or a table name with name, model, price, category columns
ownedVehicles = 'auto',
vehicles = 'auto',
}
-- ─────────────────────────────────────────────
-- Owned vehicles column mapping
-- 'auto' picks the framework default:
-- ESX owner='owner', plate, vehicle (JSON props), type, stored
-- QBCore owner='citizenid', plate, vehicle (model name), stored='state'
-- Or set your own (e.g. for a custom garage script):
-- Config.OwnedVehiclesColumns = {
-- owner = 'owner', -- identifier / citizenid column
-- plate = 'plate',
-- vehicle = 'vehicle', -- JSON props with `model`, or a model name
-- type = 'type', -- car / boat / airplane (optional)
-- stored = 'stored', -- 1 = in garage (optional)
-- }
-- ─────────────────────────────────────────────
Config.OwnedVehiclesColumns = 'auto'
-- ─────────────────────────────────────────────
-- Government Portal accounts (config/government.lua, access 'login' / 'both')
-- Server-only — players never receive this file.
-- username what staff type on the sign-in screen (case-insensitive)
-- password accounts with the password 'change-me' are disabled until you change it
-- role a role from Config.Government.roles
-- label name shown in the portal and the activity log
-- ─────────────────────────────────────────────
Config.GovernmentAccounts = {
{ username = 'mayor', password = 'change-me', role = 'mayor', label = 'Office of the Mayor' },
-- { username = 'clerk1', password = 'change-me-too', role = 'clerk', label = 'Front Desk' },
}
-- ─────────────────────────────────────────────
-- Security
-- callbackLimit / callbackWindow requests a player may send per window
-- (seconds). More are ignored and logged
-- once — normal use stays far below this.
-- 0 = no limit
-- ─────────────────────────────────────────────
Config.Security = {
callbackLimit = 40,
callbackWindow = 5,
-- Paid / state-changing actions (tests, documents, insurance, bills,
-- taxes, businesses, Job Center, paychecks …) only work within `distance`
-- of a Config.Locations entry, so a cheat menu can't use them from anywhere.
-- Skipped automatically when Config.Command.enabled = true, and for a PC
-- opened with the server export exports['aps_cityhall']:openCityHall(source).
-- Set enabled = false if you open the PC elsewhere with the client export.
location = {
enabled = true,
distance = 10.0,
},
}
-- ─────────────────────────────────────────────
-- Payment item
-- Set to an item name string to require that item
-- instead of deducting money. nil = use money only.
-- Example: Config.PaymentItem = 'theory_voucher'
-- ─────────────────────────────────────────────
Config.PaymentItem = nil
-- ─────────────────────────────────────────────
-- Fail refund percentage
-- Fraction of the test fee returned on a failed test.
-- 0.0 = no refund | 0.5 = 50% back | 1.0 = full refund
-- ─────────────────────────────────────────────
Config.FailRefundPct = 0.0
-- ─────────────────────────────────────────────
-- Money account used for all fees
-- 'bank' | 'cash' | 'black_money' (ESX) / 'crypto' (QBCore)
-- 'cash' and 'money' are treated the same on every framework.
-- ─────────────────────────────────────────────
Config.MoneyAccount = 'bank'
-- ─────────────────────────────────────────────
-- Hook: called server-side after a player earns a license
-- source player server id
-- licenseId string — e.g. 'car', 'pilot'
-- ─────────────────────────────────────────────
Config.OnLicenseEarned = function(source, licenseId)
-- TriggerClientEvent('aps_cityhall:licenseEarned', source, licenseId)
end
-- ─────────────────────────────────────────────
-- Hook: called server-side after a player orders a document
-- source player server id
-- docTypeId string — document type id
-- validityDays number — chosen validity in days
-- pricePaid number — amount deducted
-- ─────────────────────────────────────────────
Config.OnDocumentOrdered = function(source, docTypeId, validityDays, pricePaid)
-- TriggerClientEvent('aps_cityhall:documentOrdered', source, docTypeId)
end
-- ─────────────────────────────────────────────
-- Hook: called server-side after a player buys vehicle insurance
-- source player server id
-- plate string — vehicle plate
-- model string — vehicle model name
-- validityDays number — chosen validity in days
-- pricePaid number — amount deducted
-- ─────────────────────────────────────────────
Config.OnInsurancePurchased = function(source, plate, model, validityDays, pricePaid)
-- TriggerClientEvent('aps_cityhall:insurancePurchased', source, plate)
end
-- ─────────────────────────────────────────────
-- Bills history window
-- Number of days of bill history visible in the NUI.
-- ─────────────────────────────────────────────
Config.BillsHistoryDays = 60
-- ─────────────────────────────────────────────
-- Days before expiry to show renewal reminders
-- in the taskbar notification center.
-- Applies to vehicle insurance and documents.
-- ─────────────────────────────────────────────
Config.NotifyExpiryDays = 3
-- ─────────────────────────────────────────────
-- Hook: called server-side after a bill is issued to a player
-- source player server id
-- billId number — new row id in aps_bills
-- billType string — 'invoice' | 'ticket' | 'traffic-ticket' | 'receipt'
-- amount number — total amount
-- ─────────────────────────────────────────────
Config.OnBillIssued = function(source, billId, billType, amount)
-- TriggerClientEvent('aps_cityhall:billIssued', source, billId, billType, amount)
end
-- ─────────────────────────────────────────────
-- Hook: called server-side after a player pays a bill
-- source player server id
-- billId number — row id in aps_bills
-- billType string
-- amount number — amount paid
-- ─────────────────────────────────────────────
Config.OnBillPaid = function(source, billId, billType, amount)
-- TriggerClientEvent('aps_cityhall:billPaid', source, billId, billType, amount)
end
-- ─────────────────────────────────────────────
-- Config.PaymentItem inventory hooks
--
-- Only used when Config.PaymentItem is set to an item name.
-- The default uses Config.Inventory (config.compat.lua): ox_inventory,
-- qs-inventory or the framework's own item functions.
--
-- source — player server id
-- itemName — value of Config.PaymentItem
--
-- ItemCheckFn must return true if the player owns ≥ 1 of itemName.
-- ItemRemoveFn must remove exactly 1 of itemName from the player.
-- ─────────────────────────────────────────────
Config.ItemCheckFn = function(source, itemName)
return Bridge.GetItemCount(source, itemName) > 0
end
Config.ItemRemoveFn = function(source, itemName)
Bridge.RemoveItem(source, itemName, 1)
end
-- ─────────────────────────────────────────────
-- Custom phone (Config.Phone.system = 'custom')
--
-- Connect a phone City Hall doesn't support out of the box.
-- notify(source, title, text) a phone notification
-- mail(source, subject, body, sender) a mail to the player's phone
--
-- Example for okokPhone — check okokPhone's own docs for the exact export names:
-- notify = function(source, title, text)
-- TriggerClientEvent('okokPhone:client:notification', source, { title = title, description = text })
-- end,
-- ─────────────────────────────────────────────
Config.PhoneCustom = {
notify = function(source, title, text)
end,
mail = function(source, subject, body, sender)
end,
}
