Runs on: shared. This file stays editable when the resource is escrow-protected. Restart the resource after editing.
config/config.lua — the settings most servers change.
Locale
Config.Locale = 'en' -- loads locales/en.lua and locales/ui/en.json
Feature flags
Set any module tofalse to remove it completely: its server callbacks are not registered and its app disappears from the PC.
Config.Features = {
licenseCenter = true, -- Theory tests + license management
documentOrders = true, -- Document Office app
vehicleInsurance = true, -- Vehicle Insurance app
bills = true, -- Bills & Fines app + giveBill export
game = true, -- Mini-games (Paperwork Tower, Queue Rush, Typing Test) + leaderboards
vehicleRegistry = true, -- Vehicle Registry app (list of the player's vehicles)
businessRegister = true, -- Business Register app (register / browse companies)
government = true, -- Government Portal app for staff (config/government.lua)
taxes = true, -- Finances app: personal + business taxes, city treasury (config/taxes.lua)
penaltyPoints = true, -- penalty points on traffic tickets (Config.PenaltyPoints)
vin = true, -- VINs for owned vehicles (Config.VIN)
jobCenter = true, -- Job Center app: pick a job or apply (config/jobcenter.lua)
paychecks = false, -- salaries collected at City Hall; needs a framework edit (docs → Paychecks)
npc = true, -- Spawn the NPC (+ blip) at every Config.Locations entry
-- (set false if you only open City Hall via the export / event / command)
}
Locations
One clerk (and blip) is spawned per entry.Config.Locations = {
{ coords = vector4(-543.53, -207.07, 37.67, 206.34) }, -- Rockford Hills
{ coords = vector4(1690.12, 3580.81, 35.62, 210.0), blip = false }, -- no map blip
{ coords = vector4(-140.5, 6290.2, 31.5, 135.0), model = 'a_f_y_business_02', label = 'Paleto Town Hall' },
}
| Key | Type | Description |
|---|---|---|
coords | vector4 | Position and heading of the clerk. |
blip | boolean | false hides the map blip for this location. |
label | string | Optional blip name (default Config.Blip.label). |
model / scenario | string | Optional per-location override of Config.NPC. |
NPC & blip
Config.NPC = {
model = 's_m_m_dockwork_01',
scenario = 'WORLD_HUMAN_CLIPBOARD',
freeze = true,
invincible = true,
}
Config.Blip = { enabled = true, sprite = 545, color = 0, scale = 0.7, label = 'City Hall' }
Interaction
Config.Interaction = {
type = 'auto', -- 'auto' | 'ox_target' | 'qb-target' | 'proximity' (3D text + key)
distance = 2.0,
icon = 'fas fa-id-card',
key = 38, -- E (proximity mode)
}
'auto' uses ox_target, then qb-target, whichever is started. If the selected target isn’t running, the proximity prompt is used automatically.
Command
Optional chat command to open the PC from anywhere. While it’s enabled, the City Hall location check (Security) is off.Config.Command = {
enabled = false,
name = 'cityhall',
ace = nil, -- e.g. 'aps_cityhall.open' → add_ace group.admin aps_cityhall.open allow
}
Test settings
Config.Settings = {
questionsPerTest = 20, -- questions drawn from each license's pool
cooldownSeconds = 3600, -- wait after a failed test
notifyType = 'auto', -- 'auto' | 'esx' | 'qb' | 'ox_lib'
-- auto: ESX → esx, QBCore → qb, Qbox → ox_lib
}
Branding & desktop
Make the PC your own: names, screen size, available apps and the default look.Branding
Config.Branding = {
cityName = 'Los Santos', -- "City of …" on the built-in websites
orgName = 'City Hall',
osName = 'City Hall OS', -- Command Prompt, Control Panel
monitorLabel = 'CITY HALL', -- text on the monitor bezel ('' hides it)
computerName = 'CITYHALL-PC07',
}
Screen
Config.UI = {
width = 80, -- % of the game window
height = 80,
welcomeScreen = true, -- "Welcome" / "Logging off…" screens
monitor = true, -- bezel + stand around the screen
}
Set
width = 100, height = 100, monitor = false for a full-screen PC without the monitor frame.Desktop apps
Remove any built-in app from the PC. City Hall services are switched with feature flags instead.Config.DesktopApps = {
browser = true, explorer = true, writer = true, sheet = true, paint = true,
photos = true, media = true, cmd = true, calculator = true, notepad = true,
settings = true, -- Control Panel
}
Desktop defaults
The look a player gets until they change it themselves in the Control Panel. Their own choices are saved on their PC and win over these defaults. “Restore default settings” in the Control Panel returns to these values.| Key | Values | Default |
|---|---|---|
wallpaper | harmony · dusk · teal · graphite | harmony |
glassColor | sky · twilight · sea · leaf · sun · pumpkin · ruby · violet · slate · frost | sky |
transparency | boolean | true |
intensity | 0.0 – 1.0 | 0.6 |
crt | boolean — old-monitor scanlines | true |
iconSize | large · medium · small | large |
showGadgets | boolean | true |
screensaver | none · bubbles · blank | none |
screensaverMinutes | 1 – 10 | 3 |
clock24 | boolean | false |
singleClick | boolean | false |
avatar | flower · violet · person | flower |
Full View
Preview File Updated: 3.7.0 - 02.11.2026config/config.lua
Config = Config or {}
-- ─────────────────────────────────────────────
-- Locale
-- Language code: loads locales/<code>.lua (server and client messages)
-- and locales/ui/<code>.json (everything inside the PC).
-- Example: 'de' loads locales/de.lua and locales/ui/de.json.
-- Included: 'en' (English), 'de' (Deutsch).
-- Missing files or keys fall back to English. See docs → Localization.
-- ─────────────────────────────────────────────
Config.Locale = 'en'
-- ─────────────────────────────────────────────
-- Feature flags
-- Set any value to false to completely disable that module.
-- Disabled modules will not register their server callbacks,
-- will not appear as apps in the NUI, and (where applicable)
-- will not spawn the NPC or blip.
-- ─────────────────────────────────────────────
Config.Features = {
licenseCenter = true, -- Theory tests + license management
documentOrders = true, -- Document Office app
vehicleInsurance = true, -- Vehicle Insurance app
bills = true, -- Bills & Fines app + giveBill export
game = true, -- Mini-games (Paperwork Tower, Queue Rush, Typing Test) + leaderboards
vehicleRegistry = true, -- Vehicle Registry app (list of the player's vehicles)
businessRegister = true, -- Business Register app (register / browse companies)
government = true, -- Government Portal app for staff (config/government.lua)
taxes = true, -- Finances app: personal + business taxes, city treasury (config/taxes.lua)
penaltyPoints = true, -- penalty points on traffic tickets (Config.PenaltyPoints)
vin = true, -- VINs for owned vehicles (Config.VIN)
jobCenter = true, -- Job Center app: pick a job or apply (config/jobcenter.lua)
paychecks = false, -- salaries collected at City Hall; needs a framework edit (docs → Paychecks)
npc = true, -- Spawn the NPC (+ blip) at every Config.Locations entry
-- (set false if you only open City Hall via the export / event / command)
}
-- ─────────────────────────────────────────────
-- Locations
-- One NPC (and blip) is spawned per entry. Add as many as you like.
-- coords vector4(x, y, z, heading)
-- blip false to hide the map blip for this location (default true)
-- model / scenario optional per-location override of Config.NPC
-- ─────────────────────────────────────────────
Config.Locations = {
{ coords = vector4(-543.5352, -207.0693, 37.6651, 206.3393) }, -- Rockford Hills
-- { coords = vector4(1690.12, 3580.81, 35.62, 210.0), blip = true }, -- Sandy Shores (example)
}
-- ─────────────────────────────────────────────
-- NPC defaults (used for every location)
-- ─────────────────────────────────────────────
Config.NPC = {
model = 's_m_m_dockwork_01',
scenario = 'WORLD_HUMAN_CLIPBOARD',
freeze = true,
invincible = true,
}
-- ─────────────────────────────────────────────
-- Map blip
-- ─────────────────────────────────────────────
Config.Blip = {
enabled = true,
sprite = 545,
color = 0,
scale = 0.7,
label = 'City Hall',
}
-- ─────────────────────────────────────────────
-- Chat command to open the City Hall PC from anywhere
-- (e.g. for admins, or a laptop item via your own script).
-- ace nil = everyone may use it; or an ace like 'aps_cityhall.open'
-- (add_ace group.admin aps_cityhall.open allow)
-- Other ways to open it:
-- client: exports['aps_cityhall']:OpenCityHall()
-- client: TriggerEvent('aps_cityhall:open')
-- server: TriggerClientEvent('aps_cityhall:open', source)
-- ─────────────────────────────────────────────
Config.Command = {
enabled = false,
name = 'cityhall',
ace = nil,
}
-- ─────────────────────────────────────────────
-- Interaction
-- type 'auto' — ox_target → qb-target → proximity (first one started)
-- 'ox_target' | 'qb-target' — that target (proximity if not started)
-- 'proximity' — 3D text + E key press
-- distance interaction radius in units
-- icon target icon class (FontAwesome)
-- key FiveM control index used in proximity mode (38 = E)
-- ─────────────────────────────────────────────
Config.Interaction = {
type = 'auto',
distance = 2.0,
icon = 'fas fa-id-card',
key = 38,
}
-- ─────────────────────────────────────────────
-- Branding — names shown inside the PC
-- cityName used on the built-in websites ("City of …")
-- orgName the organisation running the PC
-- osName operating system name (Command Prompt, Control Panel)
-- monitorLabel text on the monitor's bezel ('' to hide)
-- computerName shown in Control Panel → System
-- ─────────────────────────────────────────────
Config.Branding = {
cityName = 'Los Santos',
orgName = 'City Hall',
osName = 'City Hall OS',
monitorLabel = 'CITY HALL',
computerName = 'CITYHALL-PC07',
}
-- ─────────────────────────────────────────────
-- Screen
-- width / height size of the PC screen in % of the game window
-- welcomeScreen show the "Welcome" / "Logging off…" screens
-- monitor draw the monitor bezel + stand around the screen
-- ─────────────────────────────────────────────
Config.UI = {
width = 80,
height = 80,
welcomeScreen = true,
monitor = true,
}
-- ─────────────────────────────────────────────
-- Desktop apps — set false to remove an app from the PC.
-- (City Hall services are switched with Config.Features above.)
-- ─────────────────────────────────────────────
Config.DesktopApps = {
browser = true, -- Internet (built-in city websites)
explorer = true, -- File Explorer
writer = true, -- Writer (word processor)
sheet = true, -- Spreadsheet
paint = true, -- Paint
photos = true, -- Photo Viewer
media = true, -- Media Player (see Config.MediaStreams)
cmd = true, -- Command Prompt
calculator = true, -- Calculator
notepad = true, -- Notepad
settings = true, -- Control Panel
}
-- ─────────────────────────────────────────────
-- Desktop defaults — the look a player gets before changing anything
-- in the Control Panel (their own changes are saved on their PC).
-- wallpaper 'harmony' | 'dusk' | 'teal' | 'graphite'
-- glassColor 'sky' | 'twilight' | 'sea' | 'leaf' | 'sun' | 'pumpkin'
-- | 'ruby' | 'violet' | 'slate' | 'frost'
-- transparency true / false intensity 0.0 – 1.0
-- crt old-monitor scanline effect
-- iconSize 'large' | 'medium' | 'small'
-- showGadgets clock + calendar on the desktop
-- screensaver 'none' | 'bubbles' | 'blank' screensaverMinutes 1–10
-- clock24 24-hour clock singleClick open icons with one click
-- avatar 'flower' | 'violet' | 'person'
-- ─────────────────────────────────────────────
Config.DesktopDefaults = {
wallpaper = 'harmony',
glassColor = 'sky',
transparency = true,
intensity = 0.6,
crt = true,
iconSize = 'large',
showGadgets = true,
screensaver = 'none',
screensaverMinutes = 3,
clock24 = false,
singleClick = false,
avatar = 'flower',
}
-- ─────────────────────────────────────────────
-- Global test settings
-- ─────────────────────────────────────────────
Config.Settings = {
questionsPerTest = 20, -- questions randomly picked per test session
cooldownSeconds = 3600, -- retry cooldown after a failed test (seconds)
notifyType = 'auto', -- 'auto' (vms_notifyv2 / okokNotify when started, else framework default)
-- | 'esx' | 'qb' | 'ox_lib' | 'vms_notifyv2' | 'okokNotify'
}
-- ─────────────────────────────────────────────
-- Mini-games
-- leaderboardSize entries shown in each game's Top list
-- games anti-cheat rules per game. A submitted score is
-- rejected when it's impossible for the round length:
-- minSecondsPerPoint round must last at least score × this
-- minDuration round must last at least this many seconds
-- maxScore highest score accepted
-- roundSeconds (typing) length of a round; the server then
-- requires roundSeconds - 5 unless minDuration is set
-- ─────────────────────────────────────────────
Config.Game = {
leaderboardSize = 10,
games = {
tower = { minSecondsPerPoint = 0.25 }, -- Paperwork Tower (height)
snake = { minSecondsPerPoint = 0.20 }, -- Queue Rush (tickets collected)
typing = { roundSeconds = 60, maxScore = 250 }, -- Typing Test (words per minute)
},
}
-- ─────────────────────────────────────────────
-- Business Register
-- fee registration fee taken from Config.MoneyAccount (0 = free)
-- maxPerPlayer active businesses one player may own
-- categories choices shown in the registration form
-- ─────────────────────────────────────────────
Config.Business = {
fee = 5000,
maxPerPlayer = 3,
categories = {
'Retail', 'Food & Drink', 'Automotive', 'Services',
'Entertainment', 'Real Estate', 'Transport', 'Other',
},
-- ── Permits ─────────────────────────────────
-- Licenses a business can hold. Other scripts check them with
-- exports['aps_cityhall']:hasBusinessPermit / playerHasPermit.
-- label / description shown in the Business Register
-- categories business categories that may apply (nil = all)
-- review true = the Government Portal must approve the
-- application (and renewals); false = granted on payment
-- requiresLicenses the owner must hold these City Hall licenses
-- (ids from config/licenses.lua)
-- options validity choices { label, days, price }
-- renewing while valid adds the days on top
permits = {
alcohol = {
label = 'Alcohol License',
description = 'Sell and serve alcoholic drinks.',
categories = { 'Food & Drink', 'Retail', 'Entertainment' },
review = true,
options = {
{ label = '30 Days', days = 30, price = 5000 },
{ label = '1 Year', days = 365, price = 45000 },
},
},
tobacco = {
label = 'Tobacco License',
description = 'Sell cigarettes, cigars and other tobacco products.',
categories = { 'Retail', 'Food & Drink' },
review = false,
options = {
{ label = '30 Days', days = 30, price = 2500 },
{ label = '1 Year', days = 365, price = 22000 },
},
},
transport = {
label = 'Transport License',
description = 'Carry passengers or goods for money (taxi, bus, delivery, trucking).',
categories = { 'Transport' },
review = true,
requiresLicenses = { 'car' },
options = {
{ label = '90 Days', days = 90, price = 7500 },
{ label = '1 Year', days = 365, price = 25000 },
},
},
food = {
label = 'Food Hygiene Certificate',
description = 'Prepare and sell food to the public.',
categories = { 'Food & Drink' },
review = false,
options = {
{ label = '1 Year', days = 365, price = 3000 },
},
},
},
-- ── Business vehicles ───────────────────────
-- The owner registers their own vehicles to an active business.
-- setJobColumn also write the business's linked job (set by the
-- government in the portal) into the owned vehicles
-- table, so job garages see the vehicle. Cleared again
-- when the vehicle is removed.
-- jobColumn that column ('job' exists on ESX owned_vehicles;
-- QBCore has none by default — then nothing is written)
vehicles = {
enabled = true,
fee = 500, -- per vehicle registered
maxPerBusiness = 10,
setJobColumn = true,
jobColumn = 'job',
},
-- ── Employees ───────────────────────────────
-- The owner adds a nearby player by their server ID (they can be removed
-- again any time). Employees see the business under
-- "My Businesses" and count for playerHasPermit / isBusinessEmployee.
employees = {
enabled = true,
maxPerBusiness = 15,
maxDistance = 10.0, -- the new employee must stand this close to the owner (0 = anywhere)
},
-- ── Registration certificate ────────────────
-- An inventory item with the business name, owner and valid permits
-- (ox_inventory: as metadata). The item must exist in your inventory.
certificate = {
enabled = true,
item = 'business_certificate',
price = 100,
},
}
-- ─────────────────────────────────────────────
-- Media Player stations
-- Audio streams / files players can pick in the Media Player app.
-- Use direct links to audio (mp3 / aac / ogg streams). Only the player
-- using the PC hears it.
-- ─────────────────────────────────────────────
Config.MediaStreams = {
-- { label = 'My Radio', url = 'https://example.com/stream.mp3' },
}
-- ─────────────────────────────────────────────
-- Phone integration
-- notifications mail phone app PC Mail app
-- sd-phone yes yes yes read + send
-- lb-phone yes yes yes full
-- 17mov_Phone yes yes yes read-only
-- yseries yes yes yes -
-- qs-smartphone(-pro) yes yes yes -
-- gksphone yes yes - -
-- qb-phone yes yes - -
-- custom Config.PhoneCustom in config/config.server.lua
-- (connect any other phone, e.g. okokPhone)
--
-- system 'auto' (first one that is started; sd-phone is checked
-- first because it also answers to other phones' names)
-- | a resource name from the list above | 'custom' | 'none'
-- app a read-only "City Hall" app inside the phone: shows unpaid
-- bills and expiring insurance & documents, and can set a
-- waypoint to the nearest City Hall. Players can't pay or
-- order anything from the phone — they have to visit City Hall.
-- notify phone notifications (true / false per event)
-- mail also send an email to the phone's mail app
-- texts notification / mail texts (%s placeholders are filled in order)
-- ─────────────────────────────────────────────
Config.Phone = {
system = 'auto',
-- Mail app on the PC that shows the phone's mailbox.
-- lb-phone full two-way sync: read, write, reply, delete (uses lb-phone's mail tables)
-- sd-phone read, write, reply (mark-as-read and delete happen on the phone)
-- 17mov_Phone read-only: inbox / sent / favourites / bin (replies are written on the phone)
-- Hidden when no supported phone is running.
mailApp = {
enabled = true,
maxLength = 4000, -- max characters per mail written on the PC
sendCooldown = 10, -- seconds between two mails from the same player
listLimit = 100, -- mails loaded per folder
},
app = {
enabled = true,
identifier = 'aps_cityhall',
name = 'City Hall',
description = 'Bills, documents, insurance and licenses at a glance.',
developer = 'City Hall',
},
notify = {
billIssued = true,
billPaid = true,
licenseEarned = true,
documentOrdered = true,
insurancePurchased = true,
businessRegistered = true,
businessPending = true,
businessApproved = true,
businessRejected = true,
businessSuspended = true,
licenseRevoked = true,
permitPending = true,
permitApproved = true,
permitRejected = true,
permitRevoked = true,
employeeAdded = true,
businessInspected = true,
taxIssued = true,
taxCollected = true,
licenseSuspended = true,
licenseReinstated = true,
pointsAdded = true,
paycheckReceived = false,
applicationReceived = true,
applicationAccepted = true,
applicationRejected = true,
},
mail = {
billIssued = true,
sender = 'City Hall',
},
texts = {
billIssued = { title = 'New bill', content = 'You received a bill of $%s from %s.' },
billPaid = { title = 'Bill paid', content = 'Your payment of $%s was received.' },
licenseEarned = { title = 'License issued', content = 'Your %s has been issued.' },
documentOrdered = { title = 'Document ready', content = 'Your %s is valid for %s days.' },
insurancePurchased = { title = 'Vehicle insured', content = '%s is insured for %s days.' },
businessRegistered = { title = 'Business registered', content = '%s is now listed in the Business Register.' },
businessPending = { title = 'Registration received', content = '%s is waiting for review by the government.' },
businessApproved = { title = 'Business approved', content = '%s has been approved and is now listed.' },
businessRejected = { title = 'Business rejected', content = '%s was not approved: %s' },
businessSuspended = { title = 'Business suspended', content = '%s has been suspended: %s' },
licenseRevoked = { title = 'License revoked', content = 'Your %s has been revoked.' },
permitPending = { title = 'Permit application', content = 'Your %s application for %s is waiting for review.' },
permitApproved = { title = 'Permit granted', content = '%s granted for %s.' },
permitRejected = { title = 'Permit rejected', content = '%s for %s was not approved: %s' },
permitRevoked = { title = 'Permit revoked', content = '%s for %s has been revoked: %s' },
employeeAdded = { title = 'New job', content = 'You now work for %s.' },
businessInspected = { title = 'Business inspection', content = '%s was inspected: %s.' },
taxIssued = { title = 'New tax', content = '%s: $%s to pay at City Hall.' },
taxCollected = { title = 'Tax collected', content = 'Overdue %s: $%s was taken from your bank account.' },
licenseSuspended = { title = 'License suspended', content = 'Your %s is suspended until %s.' },
licenseReinstated = { title = 'License reinstated', content = 'Your %s is valid again.' },
pointsAdded = { title = 'Penalty points', content = '%s penalty points added. You now have %s of %s.' },
paycheckReceived = { title = 'Salary', content = '$%s salary is waiting for you at City Hall.' },
applicationReceived = { title = 'New application', content = '%s applied for %s.' },
applicationAccepted = { title = 'Application accepted', content = 'You were hired at %s.' },
applicationRejected = { title = 'Application rejected', content = 'Your application to %s was not successful.' },
billMailSubject = 'Bill from %s',
billMailBody = 'You received a %s of $%s from %s.\n\n%s\n\nPay it on any City Hall computer.',
billMailDue = 'Please pay within %s days.',
billMailNoDue = 'There is no due date.',
},
}
-- ─────────────────────────────────────────────
-- Penalty points (Config.Features.penaltyPoints)
-- Traffic tickets with `penaltyPointsCount` (giveBill) and the
-- addPenaltyPoints export add points.
-- limit points at which licenses are taken
-- expireDays points expire after this many days (0 = never)
-- atLimit 'suspend' | 'revoke'
-- suspendDays suspension length when atLimit = 'suspend'
-- licenses City Hall license ids affected at the limit
-- resetAtLimit clear the points once the limit was applied
-- ─────────────────────────────────────────────
Config.PenaltyPoints = {
limit = 24,
expireDays = 30,
atLimit = 'suspend',
suspendDays = 30,
licenses = { 'car', 'motorcycle', 'truck' },
resetAtLimit = true,
}
-- ─────────────────────────────────────────────
-- VIN (Config.Features.vin)
-- A 17-character vehicle identification number for every owned vehicle,
-- shown in the Vehicle Registry and to staff.
-- column / hiddenColumn columns in the owned vehicles table
-- autoMigrate add the columns automatically if missing
-- checkCommand look up the VIN of the closest vehicle
-- (jobs: job → lowest grade; nil = everyone)
-- Console: `_generatevin` gives every vehicle without a VIN one.
-- ─────────────────────────────────────────────
Config.VIN = {
column = 'vin',
hiddenColumn = 'vin_hidden',
autoMigrate = true,
checkCommand = {
enabled = true,
name = 'checkvin',
distance = 5.0,
jobs = { police = 0, sheriff = 0 },
},
}
