> ## Documentation Index
> Fetch the complete documentation index at: https://docs.aps-store.com/llms.txt
> Use this file to discover all available pages before exploring further.

# config.server.lua

Server-only: database tables & columns, Government Portal accounts, security, money account, payment item, refunds, bill history, server hooks and the custom phone adapter.

> [!NOTE]
>
> Runs on: **server**. This file stays editable when the resource is escrow-protected. Restart the resource after editing.

> [!WARNING]
>
> 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

```lua theme={null}
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)
}
```

Set `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.

```lua theme={null}
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

```lua theme={null}
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
    },
}
```

Requests above the limit are ignored and logged once (`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.

> [!NOTE]
>
> Opening the PC away from City Hall:
>
> * `Config.Command.enabled = true` turns 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, set `location.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. |

Server hooks are documented on the [Hooks](https://docs.aps-store.com/assets/aps_cityhall/developer-api/hooks.md) page.

***

## Full View

Preview File Updated: 3.7.0 - 02.11.2026

```lua config/config.server.lua lines expandable theme={null}
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,
}
```


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.