# Configuration

Most settings live in `shared/config.lua`. Departments are in `shared/departments.lua` and the penal code in `shared/penalcode.lua`. These files, together with `editable/` and `locales/`, are not escrowed, so you can edit them freely. Restart the resource after changes.

| File | Purpose |
|---|---|
| `shared/config.lua` | All options below |
| `shared/departments.lua` | Departments, jobs, colors, crests, rank to access level. See [Departments and permissions](/new/mdt/departments-and-permissions) |
| `shared/penalcode.lua` | `Config.PenalCode`: 57 charges seeded into the database on first start. After that, charges are edited in game with the `charges.manage` permission |
| `editable/server/framework.lua` | Citizen, vehicle, license and roster queries. See [Integrations](/new/mdt/integrations) |
| `editable/server/integrations.lua` | Jail, impound, fine, society payout, medical bill adapters |
| `editable/client/integrations.lua` | Death check, vehicle-only rule, phone number, notifications |
| `locales/*.json` | Translations. See [Translations](/new/mdt/translations) |

Values shown as `{}` or `''` are empty by default. Percent and seconds are as named.

## General

### Config.Locale and Config.Languages

| Option | Default | Description |
|---|---|---|
| `Config.Locale` | `'en'` | Server default language code. Falls back to `en` if the file does not exist. |
| `Config.Languages.playerChoice` | `true` | Lets each player pick their own language in Settings. |
| `Config.Languages.offered` | `{}` | Whitelist of language codes players can choose from. Empty offers all languages. The server default is always offered. |
| `Config.Debug` | `false` | Prints extra debug lines in the server console. |

### Config.VersionCheck

| Option | Default | Description |
|---|---|---|
| `enabled` | `true` | Checks for a new version at start and prints the result in the console. |
| `url` | `'https://versions.kojascripts.com'` | Version service address. |
| `reportOtherResources` | `true` | Also reports other Koja resources found on the server. |

### Config.Open

How the MDT is opened.

| Option | Default | Description |
|---|---|---|
| `command` | `'mdt'` | Chat command. Empty string disables the command and the key. |
| `keybind` | `'F6'` | Default key. Players can rebind it in the FiveM keybind settings. |
| `item` | `'mdt_tablet'` | Inventory item that opens the MDT when used. Empty string turns the usable item off. |
| `requireItem` | `false` | When `true`, the player must carry the item to open the MDT. |
| `vehicleOnly` | `false` | When `true`, the MDT only opens inside an emergency class vehicle (vehicle class 18). |
| `allowWhenDead` | `false` | Allow opening the MDT while dead. |
| `allowOffDuty` | `true` | When `false`, officers must be on duty to open the MDT. |

### Config.Tablet

Tablet prop and animation while the MDT is open.

| Option | Default | Description |
|---|---|---|
| `enabled` | `true` | Show the tablet prop and play the animation. |
| `prop` | `'prop_cs_tablet'` | Prop model. |
| `bone` | `60309` | Ped bone the prop attaches to. |
| `offset` | `vector3(0.03, 0.002, -0.0)` | Prop position offset. |
| `rotation` | `vector3(10.0, 160.0, 0.0)` | Prop rotation. |
| `dict` | `'amb@code_human_in_bus_passenger_idles@female@tablet@base'` | Animation dictionary. |
| `clip` | `'base'` | Animation clip. |

### Config.Ui

| Option | Default | Description |
|---|---|---|
| `scale` | `1.0` | Overall tablet scale. |
| `defaultTheme` | `'department'` | Default theme source. `department` uses the department theme. |
| `allowPersonalAccent` | `true` | Players can pick a personal accent color in Settings. It overrides the accent, not the surfaces. |
| `fadeOutside` | `0.4` | Tablet opacity when the mouse moves outside it. |
| `clockFormat` | `'24h'` | Clock format. |
| `currency` | `'$'` | Currency symbol shown in the UI. |
| `dateLocale` | `'en-US'` | Locale used to format dates. |

### Config.Interact

The quick interaction menu for officers: ask for ID, write a citation, run a plate, parking citation, open the MDT. It works with a radial menu or a target resource.

| Option | Default | Description |
|---|---|---|
| `enabled` | `true` | Turn the interaction menu on or off. |
| `key` | `'LMENU'` | Default key (left Alt). Hidden when a radial or target provider takes over. |
| `command` | `'mdtinteract'` | Command behind the key (registered as `+mdtinteract`). |
| `radial` | `'auto'` | Radial menu provider: `'auto'`, a resource name (`koja-police`, `ox_lib`, `qb-radialmenu`), or `false` / `'off'` / `'none'`. |
| `target` | `'auto'` | Target provider: `'auto'`, a resource name (`ox_target`, `koja-interactions`, `qb-target`), or `false` / `'off'` / `'none'`. |
| `keyWithProvider` | `false` | Keep the key binding even when a provider is active. |
| `replacePoliceItems` | `true` | With koja-police, replaces its own radial items (check ID, check plate, bill, MDT) with the MDT versions. |
| `distance` | `4.0` | Maximum distance to a person. |
| `vehicleDistance` | `7.0` | Maximum distance to a vehicle. |
| `askConsent` | `true` | The other player is asked before their ID card is shown. |
| `consentTimeout` | `15` | Seconds the other player has to answer. |

### Config.DebugTools

Demo and test tools. Details in [Demo mode](/new/mdt/demo-mode).

| Option | Default | Description |
|---|---|---|
| `enabled` | `true` | Registers `/mdtdebug` and the demo seed. Set to `false` in production. |
| `aces` | `{ 'koja-mdt.debug', 'command' }` | Aces that may use the tools. |
| `signChance` | `0.75` | Chance that a fake citizen signs a citation when the mood is `random`. |
| `officerModel` | `'s_m_y_cop_01'` | Ped model for fake officers (`/mdtdebug cam`). |
| `citizenModels` | five civilian models | Ped models for fake citizens. |
| `patrolVehicle` | `'police'` | Vehicle for `/mdtdebug cam drive`. |
| `citizenVehicle` | `'sultan'` | Vehicle for `/mdtdebug citizen car`. |

## Records and justice

### Config.Records

| Option | Default | Description |
|---|---|---|
| `sharedBetweenDepartments` | `true` | All law enforcement departments see the same records. When `false`, records are split per department (DOJ still sees all). |
| `citizenSearchLimit` | `25` | Maximum people returned by a search. |
| `vehicleSearchLimit` | `25` | Maximum vehicles returned by a search. |
| `maxNarrativeLength` | `20000` | Maximum characters in a report narrative. |
| `maxImagesPerRecord` | `12` | Maximum photos per record. |
| `vehicleStrikeLimit` | `12` | Strike points on a vehicle at which the limit warning shows. |
| `defaultWarrantHours` | `72` | Default warrant lifetime in hours. |
| `warrantsNeedApproval` | `true` | New warrants wait for a judge (`warrants.approve`). |
| `expireWarrantsEveryMinutes` | `5` | How often expired warrants and overdue citations are checked. |

### Config.Processing

Sentencing a suspect from a report.

| Option | Default | Description |
|---|---|---|
| `enabled` | `true` | Allow processing (fine and jail) from reports. |
| `requireOnline` | `true` | The suspect must be online. |
| `maxDistance` | `10.0` | Maximum distance between officer and suspect. |
| `fineAccount` | `'bank'` | Account the fine is taken from. If the balance is too low, cash is used. |
| `fineSociety` | `'society_police'` | Society account that receives fines. Empty string disables the payout. |
| `allowNegativeBalance` | `false` | Allow the bank balance to go below zero. |
| `jailSystem` | `'auto'` | `'auto'`, a jail resource name, or `'none'`. See [Integrations](/new/mdt/integrations). |
| `maxMonths` | `999` | Highest jail sentence. |
| `pointsLicense` | `'driver'` | License type that receives penalty points. |
| `pointsRevokeAt` | `12` | Point total at which the license is revoked automatically. |

### Config.Custody

Jail and impound records. See also the custody guide in [Features](/new/mdt/features).

| Option | Default | Description |
|---|---|---|
| `enabled` | `true` | Turn the Custody module on or off. |
| `impoundSystem` | `'auto'` | `'auto'`, `'koja-police'`, `'garage'` or `'none'`. `auto` uses koja-police when started, otherwise the garage fallback. |
| `secondsPerMonth` | `60` | Real seconds per sentence month. Keep it equal to your jail script. |
| `maxMonths` | `999` | Highest sentence the MDT stores. |
| `autoRelease` | `true` | Closes a jail record when its release time has passed. |
| `trackedSystems` | `{}` | Jail systems that report releases themselves; their records are never closed by the clock. |
| `defaultFee` | `500` | Default impound fee. |
| `maxFee` | `50000` | Highest impound fee. |
| `defaultHours` | `24` | Default hold time in hours. |
| `maxHours` | `720` | Longest hold time in hours. |
| `despawnRadius` | `15.0` | When an officer impounds in game, a vehicle with that plate within this radius is removed. `0` turns it off. |
| `lots` | four lots | Impound lots: `{ id, label, coords }`. Defaults are Mission Row, Davis, Sandy Shores and Paleto Bay. |

### Config.Citations

| Option | Default | Description |
|---|---|---|
| `enabled` | `true` | Turn citations on or off. |
| `item` | `'citation'` | Inventory item for printed copies. |
| `dueDays` | `7` | Days to pay before a ticket becomes overdue. |
| `lateFee` | `{ percent = 25, flat = 0 }` | Added once when a ticket becomes overdue. |
| `allowWebPay` | `true` | Pay from the web portal. Works when the citizen is online, or when `Bridge.ChargeOffline` is added in `editable/server/framework.lua`. |
| `applyPoints` | `true` | Add the ticket's points to the license point total. |
| `classes` | `{ 'infraction' }` | Penal code classes that can be ticketed. |
| `allowNoJailCharges` | `true` | Also allow any charge with 0 months. |
| `maxViolations` | `12` | Maximum violations on one ticket. |
| `handDistance` | `5.0` | Maximum distance for handing over the ticket. |
| `showOnHandover` | `true` | Show the paper on the violator's screen after hand-over. |
| `contestCreatesCase` | `true` | A contested ticket files a court case. |
| `contestGraceDays` | `3` | Extra days to pay after a contest is upheld. |
| `prefixes` | `lspd = 'LS'`, `bcso = 'BC'`, `sahp = 'SA'`, `default = 'SAN'` | Ticket number prefix per department id. Numbers look like `LS-2026-000123`. |
| `quick` | eight charge codes | Charge codes shown as quick-pick chips. |
| `command` | `'citation'` | Chat command for the ticket book. Empty disables it. |
| `key` | `''` | Optional default key for the ticket book. |
| `bookInGame` | `true` | New citation in the in-game MDT opens the ticket book. The web MDT always uses the form. |
| `nearbyRadius` | `10.0` | Radius of the nearby people list in the book. |
| `requireOfficerSignature` | `true` | The officer must sign before filing. |
| `askCitizenSignature` | `true` | The person receiving the ticket is asked to sign or refuse. |
| `signTimeout` | `30` | Seconds to answer. No answer counts as a refusal. |
| `issueCooldown` | `8` | Seconds between two tickets from one officer. |
| `tearSound` | `true` | Paper tear sound in the book. |
| `readAnim` | clipboard | Animation and prop while reading the paper (`enabled`, `dict`, `clip`, `prop`, `bone`, `offset`, `rotation`). |
| `writeAnim` | notepad and pencil | Animation and props while writing (`enabled`, `dict`, `clip`, `prop`, `bone`, `offset`, `rotation`, `pencil`, `pencilBone`, `pencilOffset`, `pencilRotation`). |
| `handAnim` | give and take | Hand-over animation (`enabled`, `dict`, `give`, `take`, `prop`, `bone`, `offset`, `rotation`, `giveHold`, `takeAt`, `takeHold`, `signDelay`). |

### Config.Doj

Court and DOJ options.

| Option | Default | Description |
|---|---|---|
| `caseNumberPrefix` | `'SA'` | Case number prefix. |
| `courtrooms` | `Courtroom 1`, `Courtroom 2`, `Courtroom 3`, `Chambers` | Rooms offered when scheduling. |
| `caseTypes` | `criminal`, `traffic`, `civil`, `appeal` | Case types. |
| `hearingKinds` | `arraignment`, `preliminary`, `motion`, `trial`, `sentencing`, `appeal`, `other` | Hearing kinds. |
| `docketKinds` | `note`, `filing`, `motion`, `ruling`, `order` | Docket entry kinds. |
| `linkKinds` | `incident`, `evidence`, `bodycam`, `warrant`, `citation`, `document` | Record types that can be linked to a case. |
| `sentence.enabled` | `true` | Allow carrying out sentences from the court page. |
| `sentence.requireOnline` | `true` | The defendant must be online. |
| `sentence.fine` | `true` | A sentence can apply the fine. |
| `sentence.jail` | `true` | A sentence can apply jail time. |
| `expungement.enabled` | `true` | Expungement requests. |
| `expungement.portal` | `true` | Citizens can request through the web portal. |
| `expungement.maxPending` | `2` | Open requests per person. |
| `bar.roles` | `attorney`, `public_defender`, `prosecutor`, `judge` | Roles in the bar registry. |
| `bar.numberPrefix` | `'SABAR'` | Bar number prefix. |
| `seedForms` | `true` | On first start, DOJ departments get four form templates: subpoena, search warrant order, court order, expungement order. |

### Config.Police

Link to the koja-police resource.

| Option | Default | Description |
|---|---|---|
| `enabled` | `true` | Use koja-police when it is started. |
| `resource` | `'koja-police'` | Resource name. |
| `dispatch` | `true` | Let koja-police use the MDT dispatch. |
| `duty` | `true` | Share duty state with koja-police. |

## Dispatch and units

### Config.Dispatch

| Option | Default | Description |
|---|---|---|
| `enabled` | `true` | Turn dispatch and the public call commands on or off. |
| `publicCommands` | `/911` (priority 1), `/311` (priority 3) | Commands players use to call. Each entry: `command`, `code`, `title`, `priority`. |
| `publicCooldown` | `30` | Seconds between public calls from one player. |
| `anonymousCommand` | `'911a'` | Anonymous call command. Empty disables it. |
| `panicKey` | `'F9'` | Default key for the panic button (10-99). Empty disables the key. |
| `panicCooldown` | `15` | Seconds between panic presses. |
| `keepCallsMinutes` | `30` | How long closed calls stay in the log. |
| `maxCalls` | `80` | Maximum calls kept. |
| `blipSeconds` | `90` | How long a call blip stays on the map. |
| `alertSound` | `true` | Play a sound for new priority calls. |
| `toastSeconds` | `8` | How long the call toast shows. |
| `callCodes` | six fire codes | Extra call codes `{ code, title, priority, types }`. `types` is the department types that receive the call (`leo`, `ems`, `fire`). Defaults: `F-STR`, `F-VEH`, `F-BRU`, `F-ALM`, `F-RES`, `F-HAZ`. |

#### Config.Dispatch.gunshots

Automatic "shots fired" calls. The player's client watches for gunfire and the server turns it into a dispatch call to law enforcement, with a witness as the caller.

| Option | Default | Description |
|---|---|---|
| `enabled` | `true` | Turn gunshot alerts on or off. Also needs `Config.Dispatch.enabled`. |
| `code` | `'10-71'` | Call code. |
| `title` | `'Shots fired'` | Call title. |
| `priority` | `1` | Call priority. |
| `message` | `'Caller reports gunshots in the area.'` | Call message. |
| `vehicleMessage` | `'Caller reports shots fired from a vehicle: %s %s.'` | Message when the shooter is in a vehicle. The two values are the vehicle color and model. |
| `reportDelay` | `{ 3, 8 }` | Random delay in seconds, `{ min, max }`, before the call appears. |
| `cooldownSeconds` | `30` | Seconds between alerts from one player. |
| `mergeRadius` | `120.0` | Shots within this distance of a recent alert do not create a new call. |
| `mergeSeconds` | `60` | How long a recent alert blocks new ones inside `mergeRadius`. |
| `fuzzRadius` | `35.0` | The call location is moved by a random distance up to this value, so it is not exact. |
| `ignoreOnDuty` | `true` | Players on duty in one of the `ignoreJobs` do not trigger alerts. |
| `ignoreJobs` | `{ 'police', 'sheriff', 'sahp' }` | Jobs ignored while `ignoreOnDuty` is on. |
| `ignoreSilenced` | `true` | Silenced weapons do not trigger alerts. |
| `ignoreWeapons` | stun gun, fire extinguisher, petrol and hazard cans, flares, snowball, ball, Up-n-Atomizer | Weapons that never trigger alerts. Weapon names, for example `'WEAPON_STUNGUN'`. |
| `ignoreZones` | three zones | Places where shooting is ignored, such as gun ranges. Each entry: `{ coords = vector3(...), radius = 25.0 }`. |

:::hint{type="info" title="Delay and accuracy"}
The call is created `reportDelay` seconds after the shot and its position is offset by up to `fuzzRadius`, which imitates a witness who is not sure where the shots came from. Set `reportDelay = { 0, 0 }` and `fuzzRadius = 0.0` for instant exact calls.
:::

### Config.QuickDispatch

The small call overlay shown on screen while the MDT is closed.

| Option | Default | Description |
|---|---|---|
| `enabled` | `true` | Turn the overlay on or off. |
| `side` | `'right'` | Screen side. |
| `maxVisible` | `4` | Calls shown at once. |
| `idleSeconds` | `20` | Seconds before the overlay fades when idle. |
| `dropMinutes` | `15` | Minutes after which a call is dropped from the overlay. |
| `keys.respond` | `'Y'` | Respond to the focused call. |
| `keys.dismiss` | `'DELETE'` | Dismiss the focused call. |
| `keys.previous` | `'PAGEUP'` | Focus the previous call. |
| `keys.next` | `'PAGEDOWN'` | Focus the next call. |
| `keys.toggle` | `'HOME'` | Show or hide the overlay. |

### Config.DispatchVoice

A radio dispatcher reads new calls aloud. Officers can turn it off and change the volume in Settings.

| Option | Default | Description |
|---|---|---|
| `enabled` | `true` | Turn the voice on or off. |
| `language` | `'en'` | Voice clip language (clips ship for English and Polish). |
| `volume` | `0.8` | Default volume. |
| `radioFx` | `true` | Radio effect on the voice. |
| `alphabet` | `'lapd'` | Phonetic alphabet, `lapd` or `nato`. |
| `minPriority` | `3` | Priority threshold for spoken calls. Priority 1 is the most urgent. |
| `maxQueue` | `4` | Maximum queued announcements. |
| `staleSeconds` | `25` | Announcements older than this are skipped. |
| `speechFallback` | `true` | Use the browser speech synthesis when a clip is missing. |
| `announce.new` | `true` | Announce new calls. |
| `announce.panic` | `true` | Announce panic alerts. |
| `announce.attach` | `true` | Announce responding units. |
| `announce.closed` | `false` | Announce closed calls. |

### Config.Units

Unit statuses shown on the unit board.

| Option | Default | Description |
|---|---|---|
| `statuses` | `available 10-8`, `busy 10-6`, `enroute 10-97`, `onscene 10-23`, `traffic 10-38`, `break 10-7` | Default statuses `{ id, code, color }`. |
| `byType` | `ems`, `fire`, `doj` lists | Status lists per department type. EMS adds `transport` and `hospital`, fire adds `staging`, DOJ uses `court`. |
| `autoEnrouteOnAttach` | `true` | Attaching to a call sets the status to en route. |
| `setWaypointOnAttach` | `true` | Attaching to a call sets a GPS waypoint. |

A department can also define its own `statuses` list in `shared/departments.lua`.

### Config.Duty

| Option | Default | Description |
|---|---|---|
| `trackTime` | `true` | Track duty time for hours and the heatmap. |
| `syncWithFramework` | `true` | Duty in the MDT also sets the framework job duty. |
| `toggleCommand` | `'mdtduty'` | Chat command to toggle duty. Empty disables it. |

### Config.Roster

| Option | Default | Description |
|---|---|---|
| `selfCallsign` | `true` | Officers may set their own callsign and badge. |
| `hireGrade` | `0` | Job grade given when hiring a nearby player. |
| `fireJob` | `'unemployed'` | Job given when someone is fired. |
| `fireGrade` | `0` | Grade given when someone is fired. |

## Photos and cameras

### Config.Camera

The Take a photo button for mugshots, evidence and reports. Needs `screenshot-basic`.

| Option | Default | Description |
|---|---|---|
| `enabled` | `true` | Turn the photo button on or off. |
| `uploadUrl` | `''` | Keyless upload endpoint, for example a Discord webhook. This file is sent to every client, so never put an API key here. For Fivemanage or S3 use `Config.Storage` (below) with `images = 'storage'` instead, which uploads through the server. When both are empty the photo button is hidden. Image links can always be pasted by hand. |
| `uploadField` | `'files[]'` | Form field name for the file. |
| `encoding` | `'webp'` | Image format. |
| `quality` | `0.85` | Image quality (0 to 1). |

### Config.Storage

Where bodycam footage and MDT images are kept. The default keeps everything on the server disk and images inline in the database, exactly as before. See [Storage and CDN](/new/mdt/storage) for step by step setup of Cloudflare R2, AWS S3, Backblaze B2, MinIO and Fivemanage.

| Option | Default | Description |
|---|---|---|
| `driver` | `'local'` | `'local'`, `'s3'` (any S3-compatible service) or `'fivemanage'`. |
| `images` | `'inline'` | `'inline'` stores uploaded pictures as data URLs in the database. `'storage'` uploads them through the server to the driver and saves the public URL. |
| `footage` | `'local'` | `'local'` keeps bodycam segments in `recordings/` inside the resource. `'storage'` uploads each segment to the driver and removes the local copy. |
| `segmentSeconds` | `10` | Seconds of footage packed into one segment file (2 to 60). At 8 fps that is 80 frames and 360 uploads per recording hour. |
| `concurrency` | `3` | Parallel uploads and deletes. |
| `maxBacklog` | `400` | Jobs held in memory. Extra segments wait on disk and are picked up every 30 seconds. |
| `s3.endpoint` | `''` | S3 API endpoint, for example `https://<accountid>.r2.cloudflarestorage.com`. |
| `s3.region` | `'auto'` | `'auto'` for R2. AWS, B2 and MinIO need their real region, for example `'eu-central-1'`. |
| `s3.bucket` | `''` | Bucket name. |
| `s3.publicUrl` | `''` | Public base URL of the bucket (r2.dev, a custom domain or a CDN). Required for `images = 'storage'`. When set, footage is also served from it instead of presigned links. |
| `s3.pathStyle` | `true` | `true` uses `https://endpoint/bucket/key`, `false` uses `https://bucket.endpoint/key`. |
| `s3.presignMinutes` | `10` | Lifetime of presigned playback links. |
| `s3.prefix` | `'koja-mdt/'` | Key prefix for every object. |
| `fivemanage.path` | `'koja-mdt'` | Folder used on Fivemanage. |

Secrets never go in `shared/config.lua`, because that file is sent to every player. Set them as server convars in `server.cfg`. Use `set`, not `setr` or `sets`, so they stay on the server:

```cfg title="server.cfg"
set koja_mdt_s3_access_key "your-access-key-id"
set koja_mdt_s3_secret_key "your-secret-access-key"
set koja_mdt_fivemanage_key "your-fivemanage-api-key"
```

You can also fill `editable/server/storage.lua` instead. It is loaded on the server only and a non-empty value there wins over the convar.

The startup banner prints a `Storage` line with the driver and the result of a self-test (`HeadBucket` for S3, a tiny upload and delete for Fivemanage). Any missing option is printed as a warning and the MDT falls back to local disk and inline images.

:::hint{type="info" title="Cloudflare R2 in short"}
Create a bucket, create an API token with Object Read & Write for that bucket, set `endpoint` to `https://<accountid>.r2.cloudflarestorage.com` and `region` to `'auto'`. For images, enable the r2.dev URL or connect a custom domain and put it in `publicUrl`. Add the CORS policy below so the in-game player can fetch footage. The full walkthrough is on the [Storage and CDN](/new/mdt/storage) page.
:::

The bucket must allow `GET` from the MDT interface and from your web portal. Paste this CORS policy and replace the last origin with your portal address:

```json title="CORS policy"
[
  {
    "AllowedOrigins": ["https://cfx-nui-koja-mdt", "nui://koja-mdt", "https://mdt.example.com"],
    "AllowedMethods": ["GET", "HEAD"],
    "AllowedHeaders": ["*"],
    "ExposeHeaders": ["Content-Length", "ETag"],
    "MaxAgeSeconds": 3600
  }
]
```

### Config.Avatars

Profile pictures taken from the character's head shot.

| Option | Default | Description |
|---|---|---|
| `enabled` | `true` | Turn avatars on or off. |
| `captureOnLoad` | `true` | Capture when the player loads in. |
| `loadDelaySeconds` | `10` | Delay after load before capture. |
| `refreshMinutes` | `20` | Automatic refresh interval. |
| `command` | `'mdtphoto'` | Command to refresh your photo. Empty disables it. |
| `transparent` | `true` | Transparent background. |
| `size` | `128` | Image size in pixels. |
| `format` | `'image/webp'` | Image format. |
| `quality` | `0.85` | Image quality. |
| `minUploadSeconds` | `60` | Minimum seconds between uploads. |
| `maxLength` | `150000` | Maximum stored image size. |
| `selfie.enabled` | `true` | Selfie photo for the profile. Needs `screenshot-basic`. |
| `selfie.size` | `256` | Selfie size. |
| `selfie.quality` | `0.88` | Selfie quality. |
| `selfie.cooldownSeconds` | `20` | Cooldown between selfies. |
| `selfie.maxLength` | `400000` | Maximum size of a selfie image in characters. The effective limit is the larger of this and `maxLength`. A bigger selfie is rejected and the reason is printed in the server console. |
| `selfie.phone` | `0` | Phone variant used for the selfie camera. |
| `selfie.crop` | `{ side = 0.7, centerX = 0.42, centerY = 0.45 }` | Crop area of the captured frame. |

### Config.Cameras

CCTV cameras and the camera viewer.

| Option | Default | Description |
|---|---|---|
| `enabled` | `true` | Turn CCTV on or off. |
| `adminAce` | `'koja-mdt.cameras'` | Ace for placing and editing cameras. |
| `seedDefaults` | `true` | Seed default cameras on first start. |
| `timecycle`, `timecycleStrength`, `extraTimecycle`, `brokenTimecycle` | `CAMERA_secuirity`, `1.0`, `scanline_cam_cheap`, `CAMERA_secuirity_FUZZ` | Screen filter while viewing. |
| `defaultFov`, `minFov`, `maxFov` | `60.0`, `8.0`, `90.0` | Field of view and zoom limits. |
| `lookSpeed`, `keySpeed`, `zoomStep` | `6.0`, `1.2`, `4.0` | Control speeds. |
| `hudInterval` | `33` | HUD update interval in ms. |
| `maxMoveSpeed` | `3.0` | Maximum camera move speed. |
| `propModel`, `propModels` | `prop_cctv_cam_01a` and six more | Camera prop models offered when placing. |
| `propDistance` | `150.0` | Distance at which camera props are streamed. |
| `limits` | pan -180 to 180, tilt -89 to 60 | `panMin`, `panMax`, `tiltMin`, `tiltMax`, `defaultPan`, `defaultTiltDown`, `defaultTiltUp`. |
| `freecam` | speeds | `speed`, `fastMultiplier`, `slowMultiplier`, `lookSpeed`, `raycastDistance` for the placement free camera. |
| `placement` | placement defaults | `propByDefault`, `wallPan`, `lensGap`, `hiddenGap`, `poleHeight`, `poleLensDrop`, `frustumRange`, `reviewDistance`, `propHeadings`. |
| `keys` | controls and labels | Key controls used in the viewer and in placement mode. Each is `{ controls = { ... }, label = '...' }`. |
| `anpr` | camera plate reading | See below. |

Camera ANPR (`Config.Cameras.anpr`):

| Option | Default | Description |
|---|---|---|
| `enabled` | `true` | Read plates through CCTV cameras. |
| `range` | `80.0` | Reading range in meters. |
| `interval` | `200` | Scan interval in ms. |
| `trackInterval` | `33` | Tracking interval in ms. |
| `maxVehicles` | `12` | Vehicles tracked at once. |
| `units` | `'kmh'` | Speed units. |
| `cacheSeconds` | `60` | Result cache time. |
| `maxPlatesPerRequest` | `10` | Plates per lookup request. |
| `hitSound` | `true` | Sound on a hit. |

### Config.Bodycams

| Option | Default | Description |
|---|---|---|
| `enabled` | `true` | Turn bodycams on or off. |
| `item` | `'bodycam'` | Item name. |
| `requireItem` | `false` | The officer must carry the item. |
| `key` | `'F7'` | Default key to start or stop. Empty disables the key. |
| `command` | `'bodycam'` | Command: `/bodycam [on, off, rec, stop]`. Empty disables it. |
| `auto.duty` | `false` | Start automatically when going on duty. |
| `auto.siren` | `true` | Start when sirens are switched on. |
| `auto.weapon` | `true` | Start when a weapon is drawn. |
| `auto.dispatch` | `true` | Start when attached to a call. |
| `auto.panic` | `true` | Start on panic. |
| `idleStopSeconds` | `0` | Stop after this many idle seconds. `0` turns it off. |
| `breadcrumbSeconds` | `15` | Position sample interval. |
| `keepDays` | `30` | Days sessions are kept. `0` keeps them forever. |
| `crossDepartment` | `false` | Allow watching bodycams of other departments. |
| `label` | `'BODY 4'` | Label shown on the recording overlay. |
| `shotCooldown` | `3` | Seconds between recorded shot events. |
| `view` | camera mount | `bone`, `offset`, `fov`, `shake`, `streamTimeout` for the live view. |
| `keys` | viewer controls | `exit` (Backspace), `next` (E), `previous` (Q). |
| `recording` | see below | Footage capture and storage settings. |

#### Config.Bodycams.recording

Footage is captured by the wearer's client as a series of pictures (needs `screenshot-basic`), sent to the server in batches and packed into segments. See [Storage and CDN](/new/mdt/storage) for where segments are kept.

| Option | Default | Description |
|---|---|---|
| `enabled` | `true` | Record footage. Live view still works without it. |
| `intervalMs` | `125` | Milliseconds between frames (50 to 10000). 125 is 8 frames per second. See the table below. |
| `maxInFlight` | `4` | Frames captured at the same time on the client (1 to 8). Higher keeps the frame rate steady on a slow machine but uses more CPU. |
| `width` | `560` | Frame width in pixels (160 to 1920). |
| `quality` | `0.42` | Image quality (0.1 to 1.0). |
| `format` | `'jpg'` | `'jpg'` or `'webp'`. |
| `batchFrames` | `16` | Frames sent to the server in one message (1 to 24). |
| `latentBps` | `400000` | Upload speed limit in bytes per second for frame batches. Raise it if frames are dropped at a high frame rate. |
| `maxFrameKB` | `80` | Largest accepted frame (8 to 512). Bigger frames are dropped. |
| `keepDays` | `14` | Days footage is kept (0 keeps it forever). Separate from `Config.Bodycams.keepDays`, which keeps the session data. |
| `keepLinked` | `true` | Footage attached to an incident is deleted last when the disk limit is reached and is not deleted by `keepDays`. |
| `maxMinutesPerSession` | `60` | Longest footage per recording (1 to 720). |
| `maxDiskMB` | `20480` | Total disk limit for footage in MB. The oldest footage is deleted when it is exceeded. `0` turns the limit off. |
| `http` | `true` | Serve footage to the player over the web route. |
| `baseUrl` | `''` | Public base URL for footage links. Empty uses the web portal address (`Config.Web.publicUrl`). |
| `tokenMinutes` | `10` | Lifetime of a playback link in minutes (1 to 120). |

Frame rate is set with `intervalMs`:

| `intervalMs` | Frames per second | Notes |
|---|---|---|
| `200` | 5 | Lightest. Choppy but good enough for review. |
| `125` | 8 | Default. Smooth enough for most uses. |
| `100` | 10 | Smoother motion. |
| `66` | 15 | Smoothest. Heavy. |

:::hint{type="warning" title="Performance and disk impact"}
Every frame is a screenshot taken on the officer's machine and uploaded to the server, so a higher frame rate costs more client FPS, more upload bandwidth and more disk. Disk use grows in proportion to the frame rate. At the default width and quality a frame is about 12 to 15 KB, so 8 fps is about 400 MB per recording hour, 5 fps about 250 MB, 10 fps about 500 MB and 15 fps about 750 MB. When you raise the frame rate, check `latentBps`, `maxDiskMB` and `segmentSeconds` in [Config.Storage](#configstorage) too. Lowering `width` or `quality` is the cheapest way to save disk.
:::

### Config.Anpr and in-vehicle ALPR

| Option | Default | Description |
|---|---|---|
| `enabled` | `true` | Turn plate lookups on or off. |
| `cacheSeconds` | `30` | Lookup cache time. |
| `maxPlatesPerRequest` | `10` | Plates per request. |
| `requireDuty` | `true` | Vehicle ALPR only works on duty. |
| `hitCooldown` | `120` | Seconds before the same plate alerts again. |
| `unknownIsHit` | `false` | Treat unregistered plates as hits. |
| `hitFlags` | `{}` | Extra vehicle flags that count as a hit. Stolen, BOLO and warrant are always hits. |
| `autoDispatch` | `true` | A hit creates a dispatch call. |
| `dispatchCode` | `'10-ALPR'` | Call code. |
| `dispatchCooldown` | `300` | Seconds between calls for the same plate. |
| `blip` | `{ sprite = 225, color = 1, scale = 1.0 }` | Call blip. |
| `keepDays` | `14` | Days the ANPR log is kept. |
| `vehicle.enabled` | `true` | Turn the in-vehicle ALPR on or off. |
| `vehicle.models` | police, police2, police3, police4, sheriff, sheriff2, fbi, fbi2, pranger | Vehicle models with ALPR. |
| `vehicle.allowEmergencyClass` | `true` | Also allow any emergency class vehicle. |
| `vehicle.range` | `45.0` | Detection range. |
| `vehicle.radius` | `2.5` | Detection radius. |
| `vehicle.cone` | `22.0` | Detection cone angle. |
| `vehicle.interval` | `400` | Scan interval in ms. |
| `vehicle.clientCacheSeconds` | `25` | Client side cache. |
| `vehicle.units` | `'kmh'` | Speed units. |
| `vehicle.position` | `'bottom-right'` | Panel position. |
| `vehicle.scale` | `1.0` | Panel scale. |
| `vehicle.sound` | `true` | Hit sound. |
| `vehicle.history` | `6` | Plates shown in the panel history. |
| `vehicle.autoShow` | `true` | Show the panel by default when sitting in an ALPR vehicle. |
| `vehicle.keys.toggle` | `'F10'` | Show or hide the panel. |
| `vehicle.keys.lockFront` | `'UP'` | Lock the front plate. |
| `vehicle.keys.lockRear` | `'DOWN'` | Lock the rear plate. |
| `vehicle.keys.open` | `'RIGHT'` | Open the locked vehicle in the MDT. |

## Inventory and forms

### Config.Inventory

| Option | Default | Description |
|---|---|---|
| `system` | `'auto'` | `'auto'` uses koja_grid_inventory when started. `'framework'` forces the framework inventory through koja-lib. |
| `registerItems` | `true` | With koja_grid_inventory, register `citation`, `mdt_document` and `bodycam` automatically. |
| `items` | three items | Definitions `{ label, description, size, weight, image }` for those items. |

### Config.Forms

| Option | Default | Description |
|---|---|---|
| `enabled` | `true` | Turn forms on or off. |
| `item` | `'mdt_document'` | Item given when a form is filed. |
| `requireSignature` | `true` | Forms must be signed. |
| `readAnim` | clipboard | Animation and prop while reading a document. |

### Config.Mail, Config.Notifications, Config.Calendar

| Option | Default | Description |
|---|---|---|
| `Config.Mail.maxRecipients` | `30` | Recipients per mail. |
| `Config.Mail.maxBodyLength` | `10000` | Maximum characters in a mail. |
| `Config.Mail.keepDays` | `0` | Days mail is kept. `0` keeps it forever. |
| `Config.Notifications.keepDays` | `30` | Days notifications are kept. |
| `Config.Calendar.remindMinutes` | `30` | Reminder before an event. |
| `Config.Calendar.notifyOnCreate` | `true` | Notify attendees of new events. |
| `Config.Calendar.notifyOnComment` | `true` | Notify on comments. |
| `Config.Calendar.maxRangeDays` | `62` | Longest range that can be loaded. |
| `Config.Calendar.maxAttendees` | `500` | Attendees per event. |
| `Config.Calendar.maxCommentLength` | `500` | Characters per comment. |
| `Config.Calendar.reactions` | eight emoji | Reactions offered on events. |
| `Config.Calendar.court` | `true` | Show court hearings in the calendar. |

## Logs, admin and security

### Config.Logs

| Option | Default | Description |
|---|---|---|
| `keepDays` | `30` | Days audit entries are kept. `0` keeps them forever. |
| `webhook` | `''` | Discord webhook for all categories. |
| `webhooks` | `incidents`, `warrants`, `processing`, `roster`, `dispatch` | One webhook per category, all empty by default. |

### Config.Admin

| Option | Default | Description |
|---|---|---|
| `ace` | `'koja-mdt.admin'` | Ace that grants admin panel access. |
| `allowCommandAce` | `true` | The generic `command` ace also grants access. |
| `departmentOverrides` | `true` | Admin panel can override departments, ranks and permissions. |
| `jobEditing` | `true` | Admin panel can edit jobs and grades. Works on ESX with `jobs` and `job_grades` tables. |
| `logsKeepDays` | `30` | Age used by the purge logs button when `Config.Logs.keepDays` is `0`. |

### Config.Security

Every request from the UI is rate limited and permission checked on the server.

| Option | Default | Description |
|---|---|---|
| `burst` | `12` | Request bucket size per player. |
| `perSecond` | `6` | Bucket refill per second. |
| `dropAfterFlags` | `0` | Kick a player after this many suspicious flags. `0` never kicks. |
| `dropMessage` | `'koja-mdt: suspicious activity'` | Kick message. |
| `limits` | per endpoint | Stricter buckets `{ burst, perSecond }` for `citizens:search`, `vehicles:search`, `incidents:process`, `dispatch:public`, `anpr:lookup`, `anpr:vehicle`, `anpr:camera`, `bodycam:toggle`, `bodycam:auto`, `bodycam:shot`, `bodycam:beat`, `bodycams:watch`. |

## Web portal

### Config.Web

See the portal section in [Features](/new/mdt/features).

| Option | Default | Description |
|---|---|---|
| `enabled` | `true` | Turn the portal on or off. |
| `publicUrl` | `''` | Your own public address, for example behind nginx. Takes priority over the relay and the Cfx.re proxy. |
| `allowCitizens` | `true` | Any player can sign in to the citizen portal. |
| `citizenRecord` | `true` | Citizens see their own record and licenses. |
| `loginCodeMinutes` | `10` | How long the `/mdtweb` code is valid. |
| `sessionDays` | `30` | Session length, renewed on use. |
| `maxSessionsPerAccount` | `5` | Devices per account. |
| `command` | `'mdtweb'` | Command that gives a login code. Empty disables it. |
| `allowedOrigins` | `{}` | Extra origins allowed for cross-origin requests. |
| `trustProxyHeaders` | `false` | Trust the client IP from a reverse proxy, so rate limits see the real address. |
| `relay.enabled` | `true` | Use the relay for a short HTTPS address. |
| `relay.url` | `'https://mdt.kojascripts.com'` | Relay address. |
| `relay.id` | `''` | Readable server id (4 to 40 characters, `a-z`, `0-9`, `-`). Empty generates a random one and keeps it. |
| `relay.localUrl` | `'http://127.0.0.1:30120'` | Local address the relay forwards requests to. |

## Staff data

### Config.Divisions

List of default divisions created for each department on first start. Each entry has `name`, `short`, `icon`, `color`, `description`. Defaults: Firearms, Traffic Enforcement, Detective Bureau, K-9 Unit, S.W.A.T and Human Resources.

### Config.Certifications

List of default certifications `{ id, label, color }`. Defaults: `fto`, `swat`, `k9`, `air`, `marine`, `motor`, `detective`, `traffic`, `negotiator`, `sniper`.

### Config.ProfileTags

Flags on a person's file `{ id, label, color }`. Defaults: `armed`, `gang`, `parole`, `probation`, `violent`, `officer_safety`, `mental`, `informant`, `flight_risk`.

### Config.IncidentTypes

Report types `{ id, label, icon, template }`. The template is the starting text of the narrative. Defaults: `arrest`, `citation`, `traffic`, `pursuit`, `investigation`, `use_of_force`, `death`, `other`.

### Config.EvidenceTypes

Evidence types `{ id, label }`. Defaults: `weapon`, `casing`, `blood`, `drugs`, `cash`, `document`, `electronic`, `photo`, `vehicle`, `other`.

## EMS

### Config.Ems

| Option | Default | Description |
|---|---|---|
| `policeSummary.enabled` | `false` | Lets selected police ranks read a patient's medical summary (`patients.summary`). Every view is logged. |
| `policeSummary.ranks` | `supervisor`, `command` | Access levels that get it. |
| `bloodTypes` | eight types | Blood types offered. |
| `incidentTypes` | eleven types | ePCR incident types `{ id, label }`. |
| `catalog` | procedures and medications | Treatment catalog `{ id, label, kind, dose, price }`. `kind` is `procedure` or `medication`. |
| `fees` | five fees | Extra fees `{ id, label, price }`: call-out, ground transport, air transport, hospital admission, consultation. |
| `hospitals` | Pillbox, Sandy Shores, Paleto Bay | Hospitals `{ id, label, short, beds }` for the bed board. |
| `billing.enabled` | `true` | Medical bills. |
| `billing.account` | `'bank'` | Account charged. Cash is used if the bank is short. |
| `billing.society` | `'society_ambulance'` | Society that receives the money. |
| `billing.requireOnline` | `true` | Patient must be online. |
| `billing.maxDistance` | `0.0` | Maximum medic to patient distance. `0` means no limit. |
| `billing.allowCustomItems` | `true` | Allow custom bill lines. |
| `billing.maxCustomPrice` | `25000` | Highest price of a custom line. |
| `billing.maxTotal` | `100000` | Highest bill total. |
| `maxVitals`, `maxInjuries`, `maxTreatments` | `6`, `24`, `40` | Limits per ePCR report. |

### Config.Ems.ambulance (koja-ambulance link)

| Option | Default | Description |
|---|---|---|
| `enabled` | `'auto'` | `'auto'` links when koja-ambulance is started. `false` turns the link off. |
| `resource` | `'koja-ambulance'` | Resource name. |
| `jobs` | `{ 'ambulance' }` | Must match `Config.Jobs` of koja-ambulance. |
| `duty` | `'sync'` | `'sync'` the MDT toggle and `/mdtduty` switch koja-ambulance duty. `'follow'` duty is only changed at the hospital. `false` keeps duty separate. |
| `dutyPollSeconds` | `5` | How often koja-ambulance duty is read. |
| `import` | `true` | Import from patient button in the ePCR editor. |
| `history` | `true` | Show koja-ambulance records and invoices on the patient page. |
| `down` | `true` | Dashboard panel with downed patients. |
| `billing` | `'mdt'` | `'mdt'` charges bills in the MDT. `'ambulance'` sends them as koja-ambulance invoices. |
| `treatmentWindowMinutes` | `60` | How far back treatments are read for import. |
| `dispatch.enabled` | `true` | Distress signals appear as EMS calls. |
| `dispatch.mirror` | `true` | Mirror calls koja-ambulance sends to medics. |
| `dispatch.distress` | `true` | Create calls from distress signals. |
| `dispatch.downed` | `false` | Create a call as soon as someone goes down. |
| `dispatch.closeOnRevive` | `true` | Close the call when the patient is revived. |
| `dispatch.forward` | `false` | Send MDT calls for EMS to koja-ambulance. |
| `dispatch.code` | `'10-52'` | Call code. |
| `beds.enabled` | `true` | Show occupied koja-ambulance beds on the board. |
| `beds.admit` | `true` | Check-ins create an MDT admission. |
| `beds.discharge` | `true` | Freeing the bed discharges the admission. |
| `beds.admitResting` | `false` | Also admit patients who are only resting. |
| `beds.map` | `{}` | Bed mapping `[hospital][index]` to MDT beds. Empty uses the MDT bed list in order. |
| `regions` | head, chest, abdomen, arms, legs | Maps koja-ambulance body zones to the ePCR body diagram. |
| `injuryKinds` | gunshot, stab, laceration and more | Maps wound types to ePCR injury kinds. |
| `treatments` | bandage, tourniquet, morphine and more | Maps koja-ambulance treatments to catalog ids. |

:::hint{type="info" title="Hospital ids"}
Hospital ids in `Config.Ems.hospitals` have to match the koja-ambulance hospital keys (`pillbox`, `sandy`, `paleto` by default) for the bed link to work.
:::
