Overview
Sal's Kewl CAD is a hosted computer aided dispatch and records management system for FiveM roleplay communities. It is two pieces that are bought and run as one thing:
- The web platform — a browser application at your own
address,
yourcommunity.cad.salskewlscripts.com. It holds every call, every unit, every person, vehicle, report, citation, warrant and case your community has. This is the system of record, and it runs on our infrastructure rather than yours. - The game bridge (
sals_kewlcadbridge) — one resource that drops into your server'sresources/folder. It reports what happens in game and carries dispatch's answers back. It stores nothing and decides nothing.
Four groups of people use it, and each gets a different screen:
| Who | Where they work | What they get |
|---|---|---|
| Dispatchers | A browser, at /cad |
The dispatch workspace: a docking layout of panels that can be tabbed, split, floated or popped out into their own browser windows across as many monitors as they have. |
| Field units officers, deputies, troopers, medics, firefighters |
In game (F6 or /mdt), or the same
browser app at /mdt |
The terminal: their calls, their status, lookups, reports, the fireground. A report started in a patrol car is finishable on a laptop, because it is the same form and the same draft. |
| Players | In game (/character), or /portal |
The citizen portal: their characters, vehicles, licenses, tickets to pay, court dates and job applications. |
| You, the community owner | /admin |
The owner panel: branding, agencies, personnel, roles, the game server link, integrations, the realism toggles, the audit log and billing. |
What makes this different from a CAD you self-host
Three things, and they are the reasons the architecture is the way it is.
It does not go down when your server does. The platform is the system of record. Your dispatchers keep working while the game server restarts, a wipe never takes the records with it, and the call history survives you moving hosts.
Pop-out windows are real browser windows. A dispatcher with three monitors opens the map on one, the unit board on another and the call they are working on the third. These are genuine windows at genuine URLs, so they land where the operating system puts them and survive the first window navigating away. Five windows still share one connection to the platform — see Realtime rooms & gap replay.
Your server opens no ports. The bridge dials out over HTTPS. Nothing connects in, so there is no firewall change, no port forward and nothing for somebody to find with a port scanner.
What this manual covers
Everything, in roughly the order you will need it. Sections 1 to 10 are the admin side: what the platform is and how to set your community up. Sections 11 to 13 are the game bridge. Sections 14 to 32 are daily use, for dispatchers, field units and players. Sections 33 to 37 are for people writing their own scripts against it. The rest is reference and troubleshooting.
If you are setting up for the first time, read Setting up your community and Linking your game server, in that order, and skip the rest until something stops making sense. Most communities are running a shift within fifteen minutes of their first sign-in.
How it fits together
Worth understanding before you put it on a production server, because it explains most of the behavior you will meet later.
The platform is the system of record
Everything is stored on the platform. The bridge holds nothing — no database, no save file, nothing to back up. When a unit goes on duty in game, the bridge sends "this player went on duty" and the platform decides what that means: which unit they stepped into, whether their role allows it, what their status becomes, and who gets told.
That split is strict and it is deliberate. The bridge never decides, never caches an answer it was not given, and never accepts a client's word for anything that matters. An officer's game client cannot tell the server it has a permission; it can only ask, and the platform answers.
The five parts of the platform
| Part | What it does |
|---|---|
| API | Every domain rule and every write. If something was created, changed or deleted, this did it. |
| Gateway | The realtime connections — browsers, in-game terminals and game servers. A separate process on purpose: a dispatcher with five pop-out windows on a slow connection must not be able to hold a database transaction open, and the API must be able to restart without every board in every community going blank. |
| Worker | Background jobs: Discord role sync, data import and export, and the commands that have to survive an outage. |
| Web app | The dispatch workspace, the terminal, the citizen portal and the owner panel. One bundle, four surfaces — which is why a report started in the car opens on a laptop as the same draft. |
| Database | PostgreSQL with PostGIS. Every table that holds community data carries a tenant id, and the database itself refuses to show one community's rows to another — see Security & isolation. |
How a call gets from the game to a dispatcher's screen
Take an officer typing /ts at a traffic stop. The whole path, in
order:
- The bridge reads the plate of the vehicle in front of the car and sends
call.create.v1up its outbound connection, signed. - The gateway checks the signature and hands the event to the API. The gateway deliberately knows nothing about what the event means.
- The API finds which CAD account those game identifiers belong to, which unit that member is in, and whether their role allows creating a call. It assigns a call number from a database sequence, works out the nearest postal, decides which agencies it routes to, attaches the officer, and writes the whole thing in one transaction.
- The API publishes the new call to the
callsroom. - Every browser subscribed to that room — every dispatcher, and every in-game terminal showing the board — receives it and the queue redraws. No refresh, no polling.
- The platform sends an acknowledgement back down to the bridge, which raises
sals_kewlcad:eventResultinside FiveM so your other resources can react.
A dispatcher clicking + Call in the browser takes the same path from step 3 onward. That is the point: there is exactly one set of rules about numbering, routing, timelines and who gets told, rather than one set for the game and a second for the web that slowly drift apart.
What happens when the link drops
Nothing breaks, and this is worth knowing because it will happen.
- In game: status changes, notes and calls queue up in memory
and replay in order when the connection returns. The HUD says
CAD offline so units know why nobody is answering rather than assuming
dispatch is ignoring them.
QueueDepth()tells a script how far behind it is. - In the browser: dispatchers keep working. The platform never needed your game server to be up. Commands headed for the game that must survive an outage — a fine, a sentence, a license suspension — go through a job queue with retries. Commands that are only useful immediately, like an assignment notification, are dropped deliberately: one that arrives twenty minutes later is worse than one that never arrives.
- On reconnect: a browser tells the gateway the last sequence number it saw for each room and is sent what it missed. Past 500 events it is told to resync — refetch from the API — rather than being replayed every frame, which is both cheaper and leaves it in a known state either way.
Where the game is the truth, and where the CAD is
Both systems hold some of the same facts. The split is fixed, and the platform enforces it:
| The game decides | The CAD decides |
|---|---|
| Character names, dates of birth, gender | Everything a player fills in about themselves: address, phone, emergency contact, medical details, appearance |
| Jobs and grades | Which agency and role those map to |
| Which vehicles a character owns, and their plates | Whether a vehicle is stolen, flagged or impounded |
| Whether a license exists | Whether that license is valid, suspended or revoked |
| Where a unit physically is | What its status is, what call it is on, and who it is |
So a name that is wrong in the CAD is fixed in your character creator, not in the CAD; and a suspension handed down in the CAD reaches the character in game on its own. See Licenses & the DMV.
Requirements & plans
What you need
- A subscription. Every plan starts with a 14-day free trial and everything carries over if you subscribe — the trial is the real product with smaller limits, not a demo.
- A FiveM server with Lua 5.4. Any artifacts from the last
couple of years. The bridge's manifest sets
lua54 'yes'. - A browser, for the dispatchers. Any current one. Pop-out windows work everywhere; nothing here needs an extension or a desktop app.
- Nothing else. No framework is required and the bridge lists no dependencies at all — see Linking your game server.
Frameworks
Works with qb-core, qbx_core, es_extended, ND Core or standalone. Detection happens at runtime, and there is nothing to configure. On standalone there is no character data to sync, so your people live in the CAD and are typed in or created through the citizen portal instead.
The plans and what they limit
All four plans include the whole CAD. The differences are capacity and a short list of features, not a crippled dispatch screen.
| Limit | Trial | Community | Pro | Network |
|---|---|---|---|---|
| Game servers | 1 | 1 | 3 | 25 |
| Agencies | 3 | 10 | 100 | 500 |
| Members | 25 | 150 | 500 | 5,000 |
| Attachment storage | 250 MB | 5 GB | 25 GB | 250 GB |
| Bridge events per minute, per server | 600 | 3,000 | 10,000 | 60,000 |
| REST API calls per minute | 60 | 300 | 1,200 | 6,000 |
| Outbound webhooks | — | 3 | 20 | 100 |
| Audit history you can read back | 14 days | 90 days | 1 year | 3 years |
| Trial length | 14 days | 14 days | 14 days | 30 days |
| Feature | Trial | Community | Pro | Network |
|---|---|---|---|---|
| Pop-out windows | yes | yes | yes | yes |
| Discord login and role sync | — | yes | yes | yes |
| Sal's Kewl script integrations | — | yes | yes | yes |
| Build your own report forms | — | — | yes | yes |
| Your own domain | — | — | yes | yes |
| Public REST API and webhooks | — | — | yes | yes |
| Response-time and activity dashboards | — | — | yes | yes |
| White label | — | — | — | yes |
| Dedicated database | — | — | — | yes |
| Priority support with an agreed response time | — | — | — | yes |
Who each is meant for: Community is a typical roleplay server. Pro is a large server, or a community running more than one. Network is for multi-server networks.
Your current usage against each limit is on the first page of the owner panel, and a figure turns amber past 85% so you find out before a member cannot be invited rather than after. Hitting a limit refuses the one action; it never stops dispatch.
Billing
Subscriptions, cards, invoices and tax are handled by Stripe, and the Billing page in the owner panel sends you to Stripe's own customer portal for anything to do with payment details. The only thing the platform keeps is which plan you are on.
That matters during an outage: if Stripe is unreachable, your limits stay exactly as they were. Nothing about dispatch depends on a payment processor being up.
Setting up your community
The Getting set up checklist on the first page of the owner panel is the short version, and it ticks itself off as you go. There are four items.
1. Sign in
Your community has its own address —
yourcommunity.cad.salskewlscripts.com — and the sign-in page shows
your community's name and branding before anybody has authenticated. That is
deliberate: landing on a generic login when you typed your own community's address
feels like the wrong website.
2. Pick your agencies
Agencies come from templates with ranks, units, callsign conventions and roles already filled in, so you are never looking at an empty screen. Three starter bundles:
| Bundle | What it creates |
|---|---|
| Los Santos county starter | City police, county sheriff, state police, fire, EMS, tow, courts and the civilian portal, all sharing one dispatch. The one most communities want. |
| Small server starter | One police agency, EMS, and the civilian portal. Nothing else. |
| Everything on | Every template, including federal, corrections, DMV, park rangers and DOT. |
Pick a bundle or choose agencies individually; either way every name, rank, color and callsign is yours to rename afterwards. See Agencies, ranks & units.
Add the Civilian Services agency even if you think you will not use it. It is what the citizen portal hangs off, and it costs nothing — it has no units and no callsigns.
3. Link your game server
One resource, one key, and your server dials out so you open no ports. The whole procedure is Linking your game server.
4. Invite your staff
Either invite people by email from Personnel, or map your Discord roles to agencies and roles and let that do it — see People, invitations & signing in.
5. Turn on two-factor for yourself
Required for owners, and the checklist will keep nagging until it is done. It is not bureaucracy: an owner account holds every permission inside the community, and somebody who takes one over can read every record in it. Authenticator app, from your profile.
What you get out of the box
Provisioning a community writes a working configuration, not an empty shell. Every one of these is editable afterwards and nothing reads the templates again once your rows exist:
- Agencies with ranks, starting units, callsign patterns and a color used on the map, the roster and unit chips.
- Roles — a member role, a supervisor role and a command role per agency, each with a sensible permission bundle. See Roles & permissions.
- Status codes — plain words by default, ten-codes if you prefer them. See the appendix.
- Call types matched to the agencies you turned on: a fire department gets structure fires and alarm activations, a sheriff's office gets warrant service.
- Response plans — first, second and third alarm cards for fire, and response levels for EMS. See Run cards & response plans.
- Report forms for every report type, deliberately short.
- A penal code with charge classes, fines and jail time.
- Alert rules so a robbery script plugged in on day one produces sensible dispatch behavior with no configuration. See Integrations & alert rules.
Branding
On the Overview page: your community's display name, logo, favicon, accent color, default theme and a line of text on the login page. On Pro and above you can serve the whole thing from your own hostname; on Network the branding replaces ours throughout.
The accent color is used for emphasis across the whole app, so pick one that reads against both the dark and light themes — the theme is each person's own choice and your default is only a default.
Agencies, ranks & units
An agency is a department: a police department, a sheriff's office, a fire department, a court. It owns ranks, roles, units and stations, and it has a department type that decides what the platform offers it.
Department types
The type is not cosmetic. It decides which record types the agency can reach, which report forms it gets, whether run cards and station alerting appear, and whether its members can read criminal history or medical history.
| Type | What it means |
|---|---|
| Dispatch / Communications | Runs the board. Not a field agency. |
| City Police | Law enforcement. |
| County Sheriff | Law enforcement. Usually also the jail and warrant service. |
| State Police / Highway Patrol | Law enforcement. |
| Federal Agency | Law enforcement. Can start hidden from other agencies' maps. |
| Game Wardens / Parks | Law enforcement. |
| Coast Guard / Marine | Law enforcement. |
| Air Operations | Law enforcement. |
| Fire | Reads patient medical details, gets run cards, station alerting and the NFIRS-style fire report. |
| EMS | Reads patient medical details, writes patient care reports, sees the hospital board. |
| Tow / Roadside | Tow rotation, job queue, impound intake and release. |
| Courts | The docket, case files, warrant signing and sentencing. |
| Corrections / Jail | Booking queue, cell roster and sentence timers. |
| Licensing / DMV | Issues and suspends licenses, registers vehicles. |
| DOT / Public Works | Road closures and highway incidents; can draw on the map. |
| Civilian | The self-service portal your players use. |
| Business Dispatch | A civilian business that takes job calls. |
The eight law-enforcement types are the ones whose members can read criminal history. Fire and EMS are the two that read patient medical details. Neither gets the other's — a lookup run by a medic shows blood type and allergies and says plainly that the criminal half is not theirs to see, rather than pretending it is empty.
The shipped agency templates
| Template | Short | Callsign pattern | Starting units |
|---|---|---|---|
| County Communications | DISPATCH | DISPATCH-1 | Two dispatch positions |
| Los Santos Police Department | LSPD | 1-ADAM-12 | Two patrol, watch commander, K9, SWAT element, detectives |
| Blaine County Sheriff's Office | BCSO | S-213 | Two patrol, patrol sergeant, K9 |
| San Andreas State Police | SASP | TROOP 2-14 | Two patrol, motor unit, K9, air support |
| Federal Investigation Bureau | FIB | FIB-7 | Detectives, hostage rescue |
| Los Santos Fire Department | LSFD | ENGINE 7 | Engine, ladder, rescue, brush, battalion |
| San Andreas EMS | EMS | MEDIC 41 | Two medic units, field supervisor, air rescue |
| Kewl Towing & Recovery | TOW | TOW-3 | Two tow trucks |
| San Andreas Superior Court | COURT | JUDGE-1 | Courtroom 1 |
| Bolingbroke Penitentiary | DOC | DOC-12 | Booking |
| Department of Motor Vehicles | DMV | DMV-1 | Counter 1 |
| San Andreas Park Rangers | SAPR | RANGER-4 | Patrol, marine |
| San Andreas DOT | DOT | DOT-2 | Closure crew |
| Civilian Services | CIV | — | None |
Ranks
Each template ships a rank ladder, lowest to highest — LSPD runs Cadet through Chief of Police, the fire department Probationary Firefighter through Fire Chief, EMS EMT Trainee through EMS Chief. Ranks sort the roster and appear next to a member's name on the unit board and in reports.
A rank is not a permission. Promoting somebody to Sergeant does not let them approve reports; giving them the Supervisor role does. Keeping those separate is what lets a community have a Lieutenant who is not an admin and a quiet Corporal who is.
Units
A unit is a seat, not a person. ENGINE 7 exists
whether or not anybody is in it, which is what lets four different firefighters
step into it across a week and have the statistics still mean something. Who is in
the seat right now is tracked separately, and the unit board shows the crew when you
hover the callsign.
Every unit has a kind, which drives its icon on the map, whether a run card can call for it, and how the recommendation engine ranks it:
| Group | Kinds |
|---|---|
| Law enforcement | patrol, k9, supervisor, swat, detective, traffic |
| Fire | engine, ladder, rescue, brush, tanker, battalion |
| Medical | medic, ambulance |
| Other | tow, air, marine, dispatch, other |
Units also carry a capacity — seats — so an engine reads as a crew of four and a patrol car as one or two.
Dispatch-kind units are deliberately excluded from the "dispatch nearest unit" suggestions. A dispatcher is on duty and available, and without that exclusion they would show up in the list looking like the nearest resource, which is the kind of suggestion that makes people stop trusting the whole menu.
Jurisdictions and stations
Two optional pieces of geography, both drawn on the map:
- Jurisdictions are city, county and state areas. With jurisdiction routing on, a call goes to whoever covers the spot it happened. They nest, so a state trooper's reach covers the counties and cities inside the state. Off — and it ships off — every call lands in one shared queue, which is how most servers play.
- Stations are firehouses and posts, with a footprint on the map and an apparatus roster. They matter for station alerting and for run cards, where a station's due order is what the department decided in advance. See Station alerting.
Postals
If your community uses a postal map, upload it and every call gets a readable postal attached automatically — the nearest one to where it happened. Dispatchers read postals on the radio far more often than street names, so this is worth the five minutes.
People, invitations & signing in
A member is a person in your community. They hold one or more memberships — one per agency they belong to — and each membership carries a rank, a callsign, a badge number and one or more roles.
That shape is why a player can be an LSPD officer on Tuesday and a volunteer firefighter on Thursday without two accounts, and why the permission engine always needs to know which agency they are working as right now.
The active agency
Somebody in more than one agency picks which they are working as when they come on duty. That choice drives every agency-scoped permission check for the session: a deputy's supervisor role in the sheriff's office does not reach into the fire department's reports, even while they are standing in both.
On a server with jobAgencyMap configured, the framework job a player
is wearing picks the agency for them and /duty needs no menu at
all.
Ways to sign in
Four, and they all end in exactly the same place — a session row and a short access token — so adding a fifth later changes nothing downstream:
| Method | How it works |
|---|---|
| Password | Email and password. Two-factor is available to everyone and required for owners. |
| Magic link | A one-time link by email. Useful for people who will sign in three times a year. |
| Discord | OAuth. One Discord application serves every community, and a player who signs in this way is matched to the Discord ID your game server already reports — so nobody types a code. Community plan and above, with Discord sync on. Can also drive roles — see below. |
| A code from the game | A player types /cadlink, the bridge shows them a
six-character code, and they enter it on the website. This is how a game
account is tied to a CAD account, and it is done once per player. |
Most players never do any of this. A player gets a CAD account the first time they go on duty, open the in-game terminal, or open the citizen portal. The platform creates it in the agency their framework job is mapped to, with that agency's default role.
The map is yours — Owner panel, Player accounts, job to agency. The agency is never taken from anything the game sends, only from that map, and an unmapped job gets a civilian account rather than a guess. Your plan's member limit still applies.
/cadlink and the code box stay as the fallback, for somebody
attaching an account they already have. If you would rather approve everyone by
hand, turn off the autoEnroll setting and nothing is created
automatically.
Inviting people
Personnel in the owner panel. An invitation names the agency, the role and optionally a rank and callsign, and arrives as a link that creates the account on first use. Invitations can be revoked before they are accepted.
If you are standing a community up with twenty people at once, create the invitations in a batch and post the links in your staff channel — each is single-use and tied to the email it was issued for.
Discord role sync
Community plan and above. Map a Discord role to an agency and a CAD role, and membership follows the Discord role: somebody promoted in Discord is promoted in the CAD, and somebody who leaves the role loses the access. The sync runs as a background job, so it is a minute or two behind rather than instant.
It is worth being deliberate about which direction is the truth. If Discord drives roles, a change made in the CAD will be reverted on the next pass; most communities find it less confusing to let Discord own membership and the CAD own everything else.
Sessions and sign-out
Everyone can see their own open sessions and sign one out — the forgotten browser at a friend's house, the terminal on a server they no longer play. In-game terminal sessions are separate and short-lived by design: an in-game browser is a different risk from a real one, so the token the terminal gets expires in hours rather than weeks.
Two-factor
Authenticator app, from anybody's profile. Required for community owners, and worth requiring of anybody who can read criminal history or approve reports. Turning it on does not log anyone out.
Roles & permissions
This is the model the whole platform is built on, and ten minutes here saves a lot of confusion later.
A permission is three things
Every permission is written resource:action@scope:
report:approve@agency
│ │ └─ scope — how far the grant reaches
│ └───────── action — what may be done
└────────────────── resource — what it is done to
Scope never says what is allowed; only how far. That is the whole trick. There are four, narrow to wide:
| Scope | Reaches |
|---|---|
@self | Only rows this person owns or created. Their own reports, their own layout, their own character. |
@agency | Rows belonging to an agency they are in, plus rows belonging to no agency at all. |
@jurisdiction | Rows inside a jurisdiction they reach — which includes everything nested inside it, so a state grant covers the counties and cities. |
@tenant | Everything in the community. |
So report:update@self lets somebody edit their own report;
report:update@agency lets a supervisor edit anyone's in their
department; report:update@tenant reaches the whole community.
How a decision is made
In order, and the first match wins:
- Community owner? Owners and co-owners hold everything inside their own community, period.
- Any of their roles? A member's effective permissions are the
union of their roles in the agency they are working as. A role granted through an
agency applies only to that agency's rows, even if the permission string says
@tenant— otherwise a deputy's role in one agency would reach into another. - A mutual-aid grant? One agency can lend another specific rights, optionally only for the duration of one call and optionally with an expiry. Only relevant once you turn on private agencies.
- Otherwise denied, and the denial is logged with the reason — so support can answer "why can't my sergeant approve reports" without a database session.
The browser runs the same decision code to decide what to gray out, but that copy is only ever a hint. The server answer is the one that counts, and it is checked again on every single request. Nothing can be reached by guessing a URL.
The roles you start with
Most agencies get three, following a deliberate "simple first" rule: a member who can do the job, a supervisor who can approve and override, and a command role that configures the agency. Anything more granular is yours to build.
| Agency type | Roles |
|---|---|
| Dispatch | Dispatcher · Lead Dispatcher · Communications Manager |
| City Police | Officer · Supervisor · Command |
| Sheriff | Deputy · Supervisor · Command |
| State Police | Trooper · Supervisor · Command |
| Federal | Agent · Supervisor · Command |
| Game Wardens | Warden · Supervisor · Command |
| Marine / Air Ops | Officer / Pilot · Supervisor · Command |
| Fire | Firefighter · Supervisor · Command |
| EMS | Medic · Supervisor · Command |
| Tow | Driver · Yard Manager |
| Courts | Clerk of Court · Attorney · Judge |
| Corrections | Corrections Officer · Jail Administrator |
| DMV | DMV Clerk |
| DOT | DOT Worker |
| Civilian | Civilian |
| Business | Employee |
What each layer adds
Roles are built out of bundles, which is the quickest way to understand what a given role can actually do:
| Bundle | What it grants |
|---|---|
| CAD base every on-duty role |
See calls and units, read and write call notes, see the map, send and read messages, see BOLOs, manage their own layout and time clock, search, see the roster, and set their own status. |
| Field base every field unit |
CAD base, plus: create and update calls, put themselves on a call, read and write their own reports, submit them, and read people and vehicles. |
| Law enforcement | Create and edit people and vehicles, read criminal history, firearms, licenses and businesses, raise flags, write citations and arrests, log evidence and impounds, request warrants, create and broadcast BOLOs, read the penal code and case files. |
| Fire | Read patient medical details, write fire incident reports with their NFIRS coding, see station alerting, the hospital board, and the run card they are working on. |
| EMS | Read and update patient medical details, write and submit patient care reports, see the hospital board, station alerting and run cards. |
| Dispatch | Create, update, delete, assign, dispatch, merge and close calls; force any unit's status; draw on the map; broadcast messages; tone out stations; apply a run card and strike the next alarm; manage the signs and the radio panel; create and broadcast BOLOs; run lookups; manage the tow rotation; see the hospital board and statistics. |
| Supervisor extra | Approve and edit anybody's reports in the agency, assign and dispatch units, merge and close calls, force statuses, manage members, record discipline, read agency statistics and time clocks, push a layout as the agency default, and strike an alarm. |
| Command extra | Everything that configures the agency: ranks, roles, units, status codes, report forms, run cards and response plans, certifications, applications, and the agency's own audit log. |
The split that keeps coming up: manage versus strike
Two permissions on run cards, and the difference is the clearest illustration of how the model is meant to be used:
run_card:strike— escalate a working fire to a second alarm. A dispatcher's decision, made in thirty seconds on the radio. Dispatchers and supervisors hold it.run_card:manage— edit what a second alarm means. A chief's decision, made once in an office. Only command roles hold it.
When you build your own roles, look for that shape: the thing done in the moment and the thing decided in advance are usually two permissions, not one.
Building your own roles
Roles in the owner panel. The editor renders straight from the catalog in the next section, grouped the same way, so everything that can be granted is visible and a typo is impossible — an unknown resource or action is refused at save time rather than silently denying forever.
Three things worth doing:
- Start from a shipped role and copy it rather than from empty. The bundles above took a long time to get right.
- Be careful with
@tenant. It is the right scope for dispatch and wrong for almost everything else. A supervisor who should see their own department's reports wants@agency. - Grant
viewandlisttogether. They are separate on purpose —viewis one row,listis the index — but a role with one and not the other is almost always a mistake.
The permission catalog
Everything that can be granted. The role editor renders from this same list, so nothing exists that is not here. Actions marked on a resource are the only ones that may be granted on it; anything else is refused when you save.
Computer aided dispatch
| Resource | Actions | What it covers |
|---|---|---|
call | view, list, create, update, delete, assign, dispatch, merge, close | Calls for service. |
call_note | view, create, update, delete | Notes on the call timeline. |
unit | view, list, create, update, delete, force_status | Units. force_status is a dispatcher or supervisor overriding somebody else's status. |
bolo | view, list, create, update, delete, broadcast | BOLOs. broadcast pushes it into the game. |
message | view, create, broadcast | Messages between units and dispatch. |
map | view | The live map. |
map_draw | create, update, delete | Drawing closures, perimeters and boxes on the map. |
station_alerting | view, tone_out | The Stations panel, and setting a house off. |
signs | view, manage | The electronic signs in the game. |
radio | view, manage | The radio activity panel. |
layout | view, create, update, delete, manage | Workspace layouts. manage is pushing one as an agency default. |
tow_rotation | view, manage | The tow rotation list. |
hospital_board | view, manage | Hospital destinations and diversion status. |
response_plan | view, list, create, update, delete | What rolls on a given kind of call. |
run_card | view, manage, strike | See the split described in the previous section. |
Records management
| Resource | Actions | What it covers |
|---|---|---|
person | view, list, create, update, delete, export | The master name index. |
person_medical | view, update | Blood type, allergies, conditions, DNR. Fire and EMS. |
person_criminal | view | Criminal history. Law enforcement and the courts. |
vehicle | view, list, create, update, delete | The vehicle index. |
firearm | view, list, create, update, delete | Registered firearms, by serial. |
business | view, list, create, update, delete | Registered businesses. |
license | view, list, create, update, revoke, issue | Driver, CDL, weapon, hunting and the rest. |
flag | view, create, update, delete | Person flags: armed and dangerous, officer safety, medical alert. |
report | view, list, create, update, delete, submit, approve, seal, unseal, export | Every report type except the two below. |
fire_report | view, list, create, update, delete, approve | The fire incident report and its NFIRS coding. Its own resource because statistics are built on it. |
pcr | view, list, create, update, submit, approve | Patient care reports. Its own resource for the same reason, and agency-private by default. |
evidence | view, list, create, update, delete | Evidence and chain of custody. |
citation | view, list, create, update, delete | Citations and warnings. |
arrest | view, list, create, update, delete | Arrest records. |
impound | view, list, create, update, delete | Impounds and release. |
search | view | The cross-index search box. |
stats | view, export | The statistics pages. |
Judicial
| Resource | Actions | What it covers |
|---|---|---|
case | view, list, create, update, close, seal, unseal | Case files. |
hearing | view, list, create, update, delete | The docket. |
warrant | view, list, create, update, sign, revoke | create is requesting one; sign is a judge granting it. |
sentence | view, create, update | Sentencing, which starts the jail clock in game. |
penal_code | view, list, create, update, delete, import, export | Your charge list, fines and jail time. |
booking | view, list, create, update | The jail: booking, cell roster, release. |
Personnel
| Resource | Actions | What it covers |
|---|---|---|
member | view, list, create, update, delete | Memberships: rank, callsign, badge, roles. |
roster | view | The roster panel. |
rank | view, create, update, delete | The rank ladder. |
certification | view, create, update, delete | Training records: K9, SWAT, paramedic, FTO. |
discipline | view, create, update, delete | Disciplinary records. Agency-private by default. |
application | view, list, update, approve | Job applications from the citizen portal. |
timeclock | view, update | Hours on duty. |
Administration
| Resource | Actions | What it covers |
|---|---|---|
agency | view, list, create, update, delete, manage | The agencies themselves. |
jurisdiction | view, list, create, update, delete | The drawn city, county and state areas. |
station | view, list, create, update, delete | Firehouses and posts. |
role | view, list, create, update, delete | The role editor itself. |
status_code | view, create, update, delete | Your status set. |
call_type | view, create, update, delete | Your call types. |
form_definition | view, create, update, delete | Report forms. Pro and above. |
game_server | view, create, update, delete | Linked FiveM servers. |
api_key | view, create, revoke, rotate | Bridge keys and public API keys. |
integration | view, manage | The connected-products page. |
alert_rule | view, create, update, delete | What happens to an incoming third-party alert. |
webhook | view, create, update, delete | Outbound webhooks. Pro and above. |
tenant_settings | view, update | The realism toggles and everything else on the settings pages. |
audit | view, export | The audit log. |
billing | view, manage | Plan and payment. |
data_export | export, import | Bulk import and export. |
Wildcards
Resource and action may both be *. A community owner holds exactly
one permission — *:*@tenant — and that is the only place a wildcard is
used by default. You can use them in your own roles; be aware that
call:*@tenant includes delete.
Record visibility
Permissions answer "may this member approve a report". Visibility answers a different question: "should this member see another agency's reports at all". The two are separate, and both have to pass.
The default is shared
Out of the box almost everything is visible across your whole community: calls, BOLOs, warrants, people, vehicles, firearms, businesses, incident reports, arrests, citations, evidence, impounds, cases, bookings, shift logs and fire pre-plans.
That is how most roleplay communities actually play. A deputy runs a plate and expects to see what the city PD wrote about it, and a server where they cannot is a server where people stop using the records system.
What starts private
Four record types, each for a real reason:
| Type | Why |
|---|---|
| Patient care reports | Medical records. Only the EMS agency that wrote one reads it. |
| Personnel files | A department's own staff records. |
| Internal affairs | The point of an internal investigation is that the subject's own shift does not read it. |
| Use of force | Reviewed inside the department before it goes anywhere. |
Changing it
Turn on Private agency records under Realism, and each record type above gets a shared / agency-private switch. Agency-private means the row is visible only to members of the agency that owns it — and to anyone holding a mutual-aid grant from that agency.
Think about this before switching record types to private rather than after. A report written while a type was shared does not become unreadable, but your community's habits form fast, and a sheriff's office that has spent a month unable to read city arrests will have started keeping its own notes somewhere else.
Mutual aid
One agency lending another specific rights, for a limited time or for the duration of a single call. It only matters once private agencies are on, and it is the right answer to "the fire department needs to read this one police report" rather than making the whole type shared.
A call-scoped grant is exactly that: rights on one call and nothing else, and it expires with the grant.
Undercover units
Separate from all of the above, and on by default: units marked plain-clothes are hidden from everyone except dispatch on the map. Federal agencies can also start hidden from other agencies' maps entirely.
The realism toggles
One page in the owner panel, and the most important thing to say about it is this: everything here ships off except the three that make a new community work. A CAD with every option turned on is harder to use, not more realistic.
Dispatch and calls
| Setting | Default | What it does |
|---|---|---|
| Route calls by jurisdiction | Off | Draw city, county and state areas on the map and calls go to whoever covers that spot. Off, every call lands in one shared queue, which is how most servers play. Turn it on when you have more law enforcement agencies than dispatchers and the queue has become noise. |
| Station alerting | Off | Tone out fire and EMS stations and see who acknowledges. Adds the Stations panel and can alert automatically when a call is assigned. |
| Show ten-codes | Off | Puts 10-8 and 10-97 next to the plain words.
The statuses themselves do not change — this is labels only. |
| Tow rotation | Off | Tow jobs go to whichever company is next in line rather than whoever grabs it first. |
Records and paperwork
| Setting | Default | What it does |
|---|---|---|
| Supervisor review of reports | Off | Reports go to a supervisor before they lock, and can be returned with a note. Good for communities that train people; friction for ones that do not. |
| Detailed fire and EMS coding | Off | Adds the NFIRS-style coding sections to fire reports and the fuller vitals layout to patient care reports. Worth it if your fire department cares about its statistics; a lot of extra fields if it does not. |
| Private agency records | Off | Lets you make record types visible only to the agency that wrote them. See Record visibility. |
Courts and corrections
| Setting | Default | What it does |
|---|---|---|
| Court workflow | Off | Cases, the docket, pleas, hearings and sentencing. A sentence tells the game to start the jail clock. Warrants work either way — those are law enforcement, not court. |
Civilians
| Setting | Default | What it does |
|---|---|---|
| Civilian portal | On | Players manage their own characters, vehicles and tickets, and see their court dates. |
| Let civilians register their own vehicles and firearms | On | Off, only the DMV and law enforcement can create those records. |
Map and history
| Setting | Default | What it does |
|---|---|---|
| Keep position history for playback | On | Lets a supervisor replay unit movement on a closed call. Off, positions are live only and nothing is stored — which is also a few hundred fewer database rows a minute on a busy server. |
The other settings pages
Not on the Realism page, but worth knowing they exist:
| Setting | Default | What it does |
|---|---|---|
| Dispatch mode | Shared | One dispatch for the whole community, or one per agency. |
| Unit recommendation | On | Suggest the nearest available unit of the right kind. |
| Automatic tone-out on assignment | Off | Tone the station when a fire or EMS call is assigned, without a dispatcher pressing anything. |
| Close a call when the last unit clears | Off | Useful on a server with no dedicated dispatchers. |
| Status timers | On | The board complains when a unit sits too long in a timed status. |
| Audible cues | All on | New call, panic, priority escalation and tone-out. Each dispatcher can override for themselves. |
| Map style | Satellite | Also road and atlas. |
| Live position interval | 2 s | How often positions are pushed to browsers, 1–10 seconds. |
| Position history retention | 7 days | 0–90 days. |
| Show postals | On | Postal numbers on the map. |
| Show breadcrumbs | Off | A trail behind each moving unit. |
| Hide plain-clothes units | On | From everyone but dispatch. |
| Characters come from the framework | On | Identity fields are read-only in the CAD when the game owns them. |
| Characters per player | 3 | 1–20. |
| Online ticket payment | On | Players pay citations from the portal. |
| 9-1-1 from the portal | Off | Most communities want a 9-1-1 call to come from somebody actually in the world. |
| Businesses need approval | On | A player registering a business waits for a yes. |
| Time zone | America/Los_Angeles | Every timestamp a human reads. |
Retention
How long things are kept, and all four are yours to set:
| What | Default | Range |
|---|---|---|
| Audit log | 365 days | 0 to 3,650, capped by your plan |
| Closed calls | 30 days | 1 to 365 |
| Messages | 30 days | 1 to 365 |
| Unit positions | 7 days | 0 to 90 |
Closed-call retention trims the call, not the paperwork. Reports, citations, arrests and cases are permanent records and are never trimmed by this setting. Shortening it is how you keep a busy server's board fast without throwing away anything that matters.
Linking your game server
One resource, two secrets, one restart.
1. Get your key pair
In the owner panel, Game servers → Link a game server. Name the server something you will recognize, pick its framework if you want the label to be right, and the platform issues two values:
- an API key, beginning
skc_live_, which says which community and which server is talking; - a signing secret, shown once, which proves a message was not altered on the way.
Both values are secrets, and the signing secret is shown once. Together they authenticate your entire community to the platform — treat them like a database password. Never commit them to a repository and never paste them in Discord. If one leaks, rotate the pair from this same page; the old pair stops working immediately.
2. Install the resource
Drop the sals_kewlcadbridge folder into your server's
resources/ directory and add it to server.cfg:
ensure sals_kewlcadbridge
3. Paste the two values
Open config.lua and set them:
Config.ApiKey = 'skc_live_...' -- Game servers → API key
Config.SigningSecret = '...' -- shown once, next to the key
4. Restart and read the console
You should see a short banner:
[sals_kewlcadbridge] ----------------------------------------
[sals_kewlcadbridge] Sal's Kewl CAD bridge 2.1.0
[sals_kewlcadbridge] framework: qb-core
[sals_kewlcadbridge] ----------------------------------------
If either value is still the placeholder, the bridge says so and stops there
rather than starting in a half-working state. For anything else, set
Config.Debug.Enabled = true and restart to see the detail — it reports
the framework it detected, whether it connected, and every rejected message with
the reason.
5. Have your people link their accounts
Each player types /cadlink once and enters the six-character code on
the website. Until they do, the platform does not know who they are and they will
not appear as a unit. This includes civilians, who need it for
/character.
What is sent, and what is not
Worth being able to answer when a player asks:
| Sent | Not sent |
|---|---|
| On-duty unit callsigns, statuses and positions | Off-duty positions, unless you deliberately enable it |
| Calls and call notes | Chat, of any kind |
| Lookups that are run, and by whom | Anything about players who never go on duty, beyond a 9-1-1 they placed themselves |
| Panic and backup requests | Any file, screenshot or voice from your server |
| Characters, owned vehicles and licenses, if sync is on | Anything from a framework table you did not point it at |
Config.TrackOffDuty ships false and should stay that
way. Players consider off-duty tracking surveillance, and dispatch has no use for
it.
How the connection works
- Outbound only. The bridge opens a WebSocket out to the gateway. Nothing connects in; there is nothing to forward and nothing to find on a port scan.
- Every message is signed with HMAC-SHA256 over the body, a timestamp and a nonce, using your signing secret. The three are signed together, so a captured signature cannot be reused with a fresh timestamp. A replayed or altered message is rejected.
- FiveM ships no crypto for Lua, so SHA-256 and HMAC are implemented in pure Lua in the resource and verified against the FIPS 180-4 and RFC 4231 test vectors on every build. A drift there would reject every request from your server with an error nobody could diagnose.
- HTTPS fallback. Some hosts block long-lived sockets. If the
WebSocket cannot connect, the bridge falls back to plain HTTPS with the same
authentication and the same handlers — you lose latency and nothing else. Controlled
by
Config.AllowHttpFallback, which ships on. - An outage is survivable. Messages queue while disconnected
and replay in order.
QueueDepth()tells a script how far behind it is.
Pushing configuration without a restart
When you add a status code, rename a command or add a call type, press Push config on the Game servers page. The platform sends the current set down to the bridge, which picks it up live. Restarting the resource does the same thing; the button exists so you do not have to.
More than one server
Link each one separately — each gets its own key pair and appears on its own row, with its own player count and last-seen time. Calls, units and records are shared across all of them, because they are all one community. Plan limits cap how many you may link.
Bridge configuration reference
Everything in config.lua, commented in plain language in the file
itself. This is the same list with more context.
The link
| Setting | Default | What it does |
|---|---|---|
Config.ApiKey | placeholder | Your community's API key. Required. |
Config.SigningSecret | placeholder | Signs every message. Required. |
Config.Gateway | wss://cad.salskewlscripts.com/ws | Only change if support asks, or if you are self-hosting. |
Config.Api | https://cad.salskewlscripts.com | Same. |
Config.AllowHttpFallback | true | Fall back to plain HTTPS when the WebSocket cannot connect. |
Intervals
| Setting | Default | What it does |
|---|---|---|
Config.Intervals.Location | 3000 ms | How often on-duty positions go up. Lower is smoother on the dispatcher's map and more traffic; the platform caps it anyway. |
Config.Intervals.Heartbeat | 5000 ms | Liveness ping, so the CAD knows the server is alive. |
Config.Intervals.ReconnectMin | 2000 ms | Shortest wait before retrying a dropped connection. |
Config.Intervals.ReconnectMax | 30000 ms | Longest wait. It backs off between the two. |
Config.TrackOffDuty | false | Off-duty position tracking. Leave it off. |
Commands
Every command is renameable. The left side is what the bridge calls it internally
and must not change; the right side is what your players type. Set one to
false to remove it entirely.
Config.Commands = {
duty = 'duty', status = 'status', available = '10-8',
enroute = 'enr', onscene = 'os', clear = 'clr', busy = 'busy',
trafficstop = 'ts', attach = 'attach', detach = 'detach', note = 'note',
backup = 'backup', panic = 'panic',
plate = 'plate', name = 'name', serial = 'serial',
bolo = 'bolo', cite = 'cite', arrest = 'arrest',
tow = 'tow', ems = 'ems', fire = 'fire',
transport = 'transport', athosp = 'athosp', incident = 'incident',
mdt = 'mdt', character = 'character', cadlink = 'cadlink',
emergency911 = '911', nonemergency = '311',
}
You can also rename commands from the platform side, in tenant settings, which is the better place if you run more than one server and want them consistent.
Which jobs may use duty commands
Config.EmergencyJobs = {
'police', 'sheriff', 'state', 'bcso', 'sasp', 'lspd',
'ambulance', 'ems', 'fire', 'fd', 'doc', 'tow',
}
Leave it empty to allow anyone. This only keeps the command list tidy for civilians — the platform checks permissions properly regardless, so this is not a security control and is not meant to be one.
The in-game terminal
| Setting | Default | What it does |
|---|---|---|
Config.Mdt.Key | F6 | Opens the terminal. Players can rebind it in FiveM's own settings. |
Config.Mdt.RequireProp | false | Requires a laptop or tablet prop in hand, with the animation. |
Config.Mdt.AllowOnFoot | true | Whether the terminal works outside a vehicle. |
Config.Mdt.DimBackground | true | Fade the game behind it, so it reads as a screen. |
Config.Mdt.Chrome | 'tablet' | tablet draws a device the character is holding; fullscreen fills the screen and reads more like a menu. The platform's own setting wins when one is set there. |
The HUD
| Setting | Default | What it does |
|---|---|---|
Config.Hud.Enabled | true | The status HUD. |
Config.Hud.Position | 'top-right' | Or top-left, bottom-left, bottom-right. |
Config.Hud.ShowPendingCount | true | How many calls are waiting in the unit's area. |
Config.Hud.WaypointOnAssign | true | Set a waypoint automatically when dispatch assigns something. |
Framework sync
Characters created in game appear in the CAD without anyone typing them twice. A character is sent when its player loads in, when their job changes, and in a slow pass over everyone online every few minutes.
| Setting | Default | What it does |
|---|---|---|
Config.Sync.Enabled | true | The whole feature. Standalone servers have nothing to sync. |
Config.Sync.IntervalMinutes | 15 | Minutes between full passes over everyone online. |
Config.Sync.ApplyLicenses | true | Apply the CAD's suspensions, revocations and reinstatements to the character in game. With this off the CAD still records them but the game copy does not move. |
Config.Sync.Vehicles.Enabled | true | Read owned vehicles from the framework's table. |
Config.Sync.Vehicles.Table | framework default | player_vehicles for qb/qbx, owned_vehicles for ESX, nd_vehicles for ND. Set only if your fork differs. |
Config.Sync.Vehicles.OwnerColumn | framework default | e.g. citizenid. |
Config.Sync.Vehicles.PlateColumn | framework default | e.g. plate. |
Config.Sync.Vehicles.ModelColumn | framework default | nil if your table has no model column. |
Config.Sync.Vehicles.PropsColumn | framework default | The JSON blob, read for a color name. |
Config.Sync.Licenses.Enabled | true | Sync licenses. |
Config.Sync.Licenses.Table | user_licenses | Where ESX keeps them. qb and qbx keep them in player metadata and need nothing here. |
Config.Sync.LicenseMap | ESX names | Your framework's license names to the CAD's, for anything not already mapped. |
Vehicle reads go through oxmysql, mysql-async or
ghmattimysql, whichever is running — no framework exposes owned
vehicles through an export, so this is a plain SELECT against their own
table. It is read-only; the bridge never writes to your framework's
tables. A wrong table or column name costs one console warning and nothing else:
after three failed reads in a row it stops trying and says so.
The CAD license types are driver, cdl,
motorcycle, weapon, hunting,
fishing, pilot, boating and
business. ESX's drive, drive_bike,
drive_truck and weapon are mapped already.
Integrations
All detected at runtime with GetResourceState. Nothing here is a hard
dependency, and a missing resource just means that feature stays off.
| Setting | What it connects |
|---|---|
SalsKewlDispatch | Your 9-1-1 call takers send worked calls into the CAD, with the transcript and the answers. |
SalsKewlRadio | Channel activity and the emergency button reach the CAD's radio panel. |
SalsKewlStationAlert | The CAD's tone-outs set the firehouse off, the station's call board reads the CAD, and the watch-desk reset reports back. Configured in that resource. |
SalsKewlSignage | Amber alerts, BOLOs and closures pushed from the CAD onto the highway boards, and radar readings sent up as plate reads. Configured in that resource, under Config.Cad. |
Shims.ps_dispatch | Forward ps-dispatch events, so an existing robbery script reaches the CAD without editing it. |
Shims.cd_dispatch | Same for cd_dispatch. |
Shims.core_dispatch | Same for core_dispatch. |
The live call board
The platform can push its whole board — every open call, who is on it, the roster — into your server, for anything that displays it in game: a station TV, a wall in the dispatch office.
| Setting | Default | What it does |
|---|---|---|
Config.Board.Enabled | 'auto' |
'auto' sends it while something on this server reads it —
Station Alert or Sal's Kewl Dispatch is running, a resource called
WantBoard(), or one of the board exports was read in the last two
minutes. true always; false never, and station boards
on this server stay empty. |
Config.Board.AutoFor |
Station Alert, Dispatch | Resources that read the board without having to say so. Add your own, or
have it call WantBoard() at start. |
Sensitive calls are never on the board. A board on a wall is a public place.
Nights ERS
Bridges night_ers callouts to the CAD, and does nothing at all if
that resource is not running. See Nights ERS callouts for what it
does and the one rule that shapes it.
| Setting | Default | What it does |
|---|---|---|
Config.Ers.Enabled | true | Master switch. Harmless left on without night_ers installed. |
Config.Ers.OfferTimeoutMs | 5000 | How long the CAD shows an unanswered offer. Match it to your ERS offer timeout; ERS still decides when its own offer lapses. |
Config.Ers.TrafficStops | true | Whether an ERS traffic stop becomes a call as it starts. |
Config.Ers.Pursuits | true | Whether a pursuit does. Pursuits come in as priority 1. |
Debug and locale
| Setting | Default | What it does |
|---|---|---|
Config.Debug.Enabled | false | Verbose logging. Ships off; leave it off in production — it prints every message in both directions, which is a lot on a busy server. |
Config.Debug.Verbose | false | Print outbound events as well as inbound commands. |
Config.Locale | 'en' | Reads locales/. |
Street commands
Everything a unit would otherwise open a menu for. Names below are the defaults
and every one is renameable in Config.Commands.
The important thing about these is that they are not a lesser interface. A
/ts typed in a patrol car and a dispatcher clicking "new traffic stop"
produce the same event and go through the same rules — there is one set of
behavior, not two.
Duty and status
| Command | Does |
|---|---|
/duty | Go on or off duty, choosing a unit. With a job-to-agency map configured, no picker appears. |
/10-8 | Available. |
/enr | En route to your assigned call. |
/os | On scene. |
/clr | Clear, with an optional disposition. |
/busy | Busy, unavailable for assignment. |
/status <code> | Any other status your community defines. |
Calls
| Command | Does |
|---|---|
/ts | Traffic stop on the vehicle in front of you. The plate is read for you. |
/attach /detach | Put yourself on a call, or clear from it. |
/note <text> | Add a note to your current call. |
/backup [urgency] | Request backup at your location. |
/panic | Emergency alert. Goes to every dispatcher immediately, at priority 1. |
/911 <text> | Emergency call. Any player, on duty or not. |
/311 <text> | Non-emergency call. |
Lookups
Each requires the matching permission in the CAD. The answer comes back as a few lines in chat — the danger first, then the identifying facts, then a pointer to the terminal for the rest. The common case is a plate read at a stop light, and opening a full-screen interface to answer that is worse than four lines of text.
| Command | Returns |
|---|---|
/plate <plate> | Vehicle, owner, registration, flags. |
/name <first> <last> | Person, warrants, licenses, flags. |
/serial <serial> | Firearm by serial number. |
Actions
| Command | Does |
|---|---|
/bolo <text> | Create a BOLO. |
/cite /arrest | Start a citation or an arrest in the terminal, prefilled. |
/incident | Start an incident report where you are, prefilled. |
/tow /ems /fire | Request a tow, EMS or fire to your location. |
/transport /athosp | Patient transport, and arrival at hospital. |
/mdt | Open the terminal. |
/character | Your own record. Any player. |
/cadlink <code> | Link this game account to your CAD account. One time, per player. |
What "prefilled" means
/cite, /arrest and /incident open the form
already filled in with what the game knows and the browser cannot:
- the nearest player within four meters, resolved to their CAD person record;
- the plate of the vehicle the officer is looking at, or sitting in;
- the street, with coordinates.
If nobody is close, the form opens empty and says so rather than guessing.
The in-game terminal
Opened with F6 or /mdt. It is the same
application a dispatcher's browser loads, which is the whole point: a unit writes a
report in the car and finishes it on a laptop as the same draft, against the same
form and the same permissions.
It is a device, not a web page
That distinction drives every choice in it:
- A status bar across the top with the callsign, the clock and the link state — the three things somebody glances up to check.
- A home screen of tiles, big enough to hit on a touchscreen in a moving car.
- One level of navigation, with a back arrow that always goes home. Anything deeper gets lost while driving.
- A status strip that is always visible: your status, your call, and the quick statuses as buttons.
The tiles
| Tile | What it is |
|---|---|
| Calls | What is waiting and what you are on. Attach, detach, note, close. |
| Lookup | One box that works out what you typed — a plate, a person, an address, a phone number. The dangerous part of the answer comes first. |
| Reports | Your drafts and your submitted reports, and the form to write a new one. |
| BOLOs | What is currently out. |
| My shift | Your time on duty, your calls, your paperwork. |
| Assignment | The fireground tab. Fire and EMS only — see below. |
| Dispatch | Only if your community turned it on — see below. |
| Commands | The command reference, as its own screen rather than a wall of text under every tab. |
Tiles are gated three ways and all three have to pass: the member's role has to grant it, the community has to allow it in game, and — for the fireground — the member has to actually be on a fire or EMS agency. A tile nobody can use is worse than a missing one, because it teaches people the terminal is unreliable.
What the terminal knows that the browser does not
A card showing where the unit is and what it is driving, with two buttons that save a lot of typing:
- Run plate ahead — the plate of the vehicle in front, without anybody reading it off the screen and typing it in.
- Cite here — opens a citation with the street and coordinates already filled.
The fireground tab
Built around one idea: a firefighter on a call should never have to type a time or remember a number.
It shows the whole assignment, not just this unit's part — because the first question on any working fire is "what else is coming and how far out are they" — and a row of buttons that stamp the times. Pressing On scene here is what produces the travel time that ends up in the department's annual report, and it is the only way that number is ever right.
Dispatch from the car
Off unless the community switches it on, and most do not. The point of a dispatcher is that somebody is not driving, and giving every officer the board is how a community stops needing dispatchers at all.
But a small server running with nobody in the chair needs an officer to be able to take a call and send somebody, and telling them to alt-tab to a browser is worse. So it is a setting, off by default.
It is deliberately a subset. Creating a call, assigning a unit and striking a run card are things you can do at a stop light. Merging calls, editing call types and reassigning across agencies need a screen and a keyboard, and they stay in the web app.
The setting only decides whether the terminal offers these. It never grants a permission — every one of them is still checked server-side against the member's role. Switching it on widens what the terminal shows; it does not widen what anybody may do.
Terminal sessions
The terminal is handed a short-lived token rather than a password, and it expires in hours rather than weeks. An in-game browser is a different risk from a real one. When it expires the terminal says Terminal session ended and reopening it mints a new one.
The citizen portal
What a player sees about themselves. In game with /character, or in a
browser at /portal. Written to be read by somebody who has never seen a
CAD and does not want to.
The five tabs
| Tab | What is there |
|---|---|
| My characters | Each character, with the details a medic who finds them unconscious would want: address, phone, emergency contact, blood type, allergies, conditions and a DNR flag. No framework tracks any of that, which is why it is here. |
| Vehicles | What they own, its plate and registration, and whether it is currently flagged or impounded. |
| Tickets | Citations they owe, with the charge, the fine and a pay button if your community allows online payment. |
| Court | Hearings they are due at, with the date and the case. Only when court workflow is on. |
| Apply for a job | Applications to whichever agencies are accepting them. A command role reviews these under Personnel. |
What a player may change, and what they may not
On a server that syncs characters from the game, the identity fields — name, date of birth, gender — are shown but locked, with a line saying where to change them. That is the same split described in How it fits together: the game owns identity, the CAD owns everything else. Everything that is theirs is editable here.
Why the medical block matters
It is the single most-used part of this portal on servers that roleplay EMS. A medic running a name on an unconscious patient gets blood type, allergies, conditions and the DNR flag — and the only way any of that is ever there is if the player filled it in themselves. Tell your players it exists.
Registering things
If your community allows it, players can register their own vehicles and firearms from here. Turn it off under Realism and only the DMV and law enforcement can create those records, which is the right answer for a community that roleplays the DMV as an actual counter you queue at.
9-1-1 from the portal
Off by default. Most communities want a 9-1-1 call to come from somebody actually in the world, not from a web page. Turn it on if yours disagrees.
The dispatch workspace
At /cad. A docking layout where every panel can be tabbed, split,
floated or popped out into its own browser window.
The layout you start with
Queue and units on the left, the call being worked in the middle, the map and alerts on the right, with Lookup and Messages tabbed behind the call and BOLOs behind alerts.
The unit board gets the most vertical space of anything on screen, and that is a considered choice rather than an accident: the pending queue is usually four or five rows, while the roster is every unit in every agency and a dispatcher reads it constantly. A queue that scrolls is an annoyance; a unit board that scrolls means somebody gets missed.
Every panel
| Panel | What it shows |
|---|---|
| Pending | Calls waiting for a unit. Priority first, then oldest first. |
| Active | Calls being worked: dispatched, active, held. |
| Call | Everything about the one call you are working. |
| Closed | Recently closed, with dispositions. |
| Units | The status board, grouped by agency. |
| Roster | Everyone in the community, not just who is on duty. |
| Map | Live positions, calls, drawings and the heat map. |
| Lookup | One search box across people, vehicles, plates, firearms, property and reports. |
| BOLOs | What is currently out, and the form to issue one. |
| Alerts | Incoming third-party alerts, panic and backup. |
| Messages | Text between dispatch and units. Reaches every terminal and every dispatcher screen straight away. |
| Stations | Fire and EMS houses: tone out, see acknowledgements, edit a station. |
| Signs | The electronic signs in the game, and what they are showing. |
| Hospitals | Destinations and diversion status. |
| Tow | The tow queue and the rotation. |
| Radio | Channel activity, and who is talking. |
Pop-out windows
Pick a panel from Pop out… and it opens as a real browser window at a real URL — which is what makes it land on a second monitor, stay there, and survive the first window navigating away.
Every pop-out carries the session with it, and they share a single realtime connection: the first window to load elects itself leader, holds the socket, and rebroadcasts every frame to the others. Five windows do not mean five connections and do not mean five times the traffic.
Saving layouts
Your arrangement is saved as you change it — immediately in your own browser, and to the platform a couple of seconds later, so a resize or a pop-out survives a refresh and follows you to another machine.
- Reset layout puts back the default arrangement.
- A supervisor with
layout:manage@agencycan push a layout as the agency default, which is what a new dispatcher in that agency gets. - The order of preference is: your own saved layout, then the agency default, then the built-in one. A layout saved by an older build that fails to restore falls back rather than leaving you with an empty workspace.
Quick actions
Three things a dispatcher starts from scratch most often, sitting next to the command line rather than buried in a panel that may not even be on screen:
| Action | Shortcut |
|---|---|
| + Call | Alt + N |
| + BOLO | Alt + B |
| Signs | — |
Alt rather than Ctrl, because Ctrl + N is "new window" in every browser and cannot be reliably intercepted. Both are ignored while you are typing in a field, so an n in a narrative never opens a dialog on top of the one you are in.
Themes
Dark, light, system or high contrast, chosen per person from the toolbar and kept in that browser. Your community's default is only a default — a dispatcher working a night shift gets to decide for themselves.
The dispatcher command line
The box at the top left. Press / or Ctrl + K from anywhere to focus it.
This is the feature experienced dispatchers end up living in. A dispatcher who learns six of these stops touching the mouse, which is most of why people who have used real CAD software prefer it to web ones.
The commands
| Command | Does | Example |
|---|---|---|
u <callsign> <status> | Set a unit's status | u 1-ADAM-12 ONS |
a <callsign> <call#> | Assign a unit to a call | a S-213 26-000119 |
d <callsign> | Clear a unit from its call | d S-213 |
c <type> [location] | Start a new call of that type | c TS Route 68 |
n <call#> <text> | Add a note to a call | n 119 driver detained |
x <call#> [disposition] | Close a call | x 119 citation issued |
f <callsign> | Follow a unit on the map | f TROOP 2-14 |
? | Show the list | ? |
It forgives abbreviation
Dispatchers abbreviate, so the command line does too:
1a12finds1-ADAM-12— punctuation and case are ignored on a second pass.119finds26-000119— a call number matches on its ending as well as in full.
It tells you what went wrong
A bad status code does not fail silently; it answers with the codes your community actually uses. A call number that matches nothing open says so. Successes clear the box; failures leave what you typed there so you can fix it rather than retype it.
Every command resolves to exactly the same API call a click would make, so
nothing here can do something the interface cannot — and nothing here skips a
permission check. A dispatcher without call:close@tenant gets the same
refusal from x as from the button.
Working a call
Where calls come from
| Source | How |
|---|---|
| A dispatcher | + Call, Alt+N, c <type>, or right-clicking the map where it happened. |
| A unit | /ts, /tow, /ems, /fire, /incident — or the terminal. |
| A player | /911 or /311, with the caller attached. |
| Panic or backup | /panic and /backup, as priority alerts. |
| A third-party script | An alert through the rules engine — a robbery script, an alarm, an ALPR hit. See Integrations & alert rules. |
| Sal's Kewl Dispatch | A worked 9-1-1 call dumped in whole, with the transcript, the call taker's answers and the times. |
| Your own script | CreateCall or the REST API. |
Every one of them lands in the same place and follows the same rules about numbering, routing, the timeline and who gets told.
The pending queue
Sorted the way a dispatcher thinks: priority first, then oldest first. A P1 that came in ten seconds ago still outranks a P4 from an hour ago.
A call that has been waiting too long for its priority turns amber and then red. The thresholds are per priority, because "too long" means something different for a shots-fired call than for a parking complaint:
| Priority | Amber after | Red after |
|---|---|---|
| P1 — life at risk | 30 s | 1 min |
| P2 — urgent | 1 min | 3 min |
| P3 — prompt | 3 min | 7 min |
| P4 — routine | 7 min | 15 min |
| P5 — when convenient | 30 min | 1 hour |
Each row shows the call number, the title, the service code, the location and postal, the caller if there is one, and the units already on it. Two separate ages are shown: how long since the call came in and how long since a unit was actually dispatched. A call can be four minutes old and thirty seconds dispatched, and those two numbers mean very different things to a supervisor.
Assigning units
Four ways, and they all do the same thing:
- Drag a unit from the status board onto the call. This is how dispatchers who have used a real CAD expect it to work.
- Right-click the call and pick from the list — available units only, nearest first when the call has coordinates, with the distance shown.
- The command line:
a 1-ADAM-12 119. - Recommend on the call panel, which suggests the nearest available units of the right kind.
Assigning does three things at once: it attaches the unit, moves it to en route, and tells the game — the officer gets a notification and a waypoint without typing anything.
The call panel
It stays live: a note typed in a patrol car appears here without a refresh, because the panel subscribes to that call's own room.
On it you will find the priority (changeable from a dropdown), the status, the source, the agencies it routed to, the units attached and the units that have cleared, the full timeline, and a box to add a note. Depending on your permissions and the call: Recommend, Run card, Tone out and Close.
Call statuses
| Status | Means |
|---|---|
pending | Waiting. Nothing has been sent. |
dispatched | Units assigned, nobody on scene yet. |
active | Somebody is on scene. |
held | Parked deliberately — waiting on something, not forgotten. |
closed | Done, with a disposition. |
Merging duplicate calls
Several 9-1-1 calls about the same crash is the normal case, not an edge one. Merging moves the units, the notes and the timeline across rather than asking anyone to retype anything, and leaves a pointer behind so the old call number still resolves when somebody reads it off a report later.
When a new call is created close to a recent one of the same kind, the platform offers the merge rather than creating it blind.
Closing
Close with a disposition — the short answer to "what happened" that makes the closed list worth reading. If your community turned on Close a call when the last unit clears, it closes itself when everyone detaches, which is the right setting for a server with no dedicated dispatchers.
Playback
With position history on, a supervisor can replay unit movement on a closed call — where everybody was, minute by minute. This is the honest answer to "did anyone actually go" and is worth the storage on a community that runs internal reviews.
Units & status
Reading the board
Units are grouped by agency, with the agency color down the left, and sorted within the group by how committed they are — panic first, then on scene, en route, transporting, at hospital, busy, available, out of service, off duty. The thing needing attention is always at the top.
Each row shows:
- the callsign, with a crew count if more than one person is in it — hover for who, with ranks and badge numbers;
- the status, as your community's own code, in the status color;
- what it is on: the call number, service code and location when committed, or who is in it when not;
- two timers — see below;
- icons for lights and siren, and for plain clothes.
The two timers
The first is time in the current status. The second, when the unit is on a call, is time on that call. They are different numbers and both matter: going en route, arriving and then waiting resets the status timer three times, and the question "how long has this unit been tied up" is answered by neither of the first two.
A unit that sits past its status's timer is marked overdue and the figure turns amber. The defaults are 10 minutes en route, 30 on scene, 20 busy or transporting, 45 at hospital — all editable per status code.
Changing status
| Who | How |
|---|---|
| The unit itself | /10-8, /enr, /os, /clr, or the quick-status buttons on the terminal. |
| A dispatcher or supervisor | The dropdown on the row, the right-click menu, or u <callsign> <code>. Needs unit:force_status. |
| The platform | Assigning a unit moves it to en route automatically. |
Every status change is recorded in an append-only log, and that log is what the response-time statistics are computed from. It is also what stamps the fireground milestones — see Run cards & response plans.
Which statuses a unit can be put into
Four states only mean anything while the unit is assigned to a call, because each
one stamps a milestone onto that call: en_route, on_scene,
transporting and at_hospital. A unit sitting in quarters
with nothing assigned cannot be put on scene — of what? — so those four are greyed
out in the menu with the reason beside them, and refused by the platform if
something asks anyway. The refusal names the unit and says what to do instead,
because it is read mid-shift.
The others are about the unit itself and are always available: available, busy, out of service, off duty. Panic cannot be set by hand at all. It comes from the unit — the panic button, or the terminal — and it is cleared by putting them back in service. A dispatcher who could raise one by hand could manufacture the one alert nobody is allowed to ignore.
It is not a ladder. A crew that arrives and has to back out to the street goes on scene → en route, and a unit that clears the hospital and returns to the same call goes back again. The only question asked is whether the status makes sense right now, never whether it follows the one before. A CAD that insists dispatch work only moves forward gets told whatever the software will accept, and then the times mean nothing.
A street command refused this way says so in game: the bridge acknowledges
/os the moment it is typed, so when the platform disagrees it pushes
the reason back to that player rather than leaving the acknowledgement standing.
The right-click menu
On any unit row: open the call it is on, clear it from that call, set a status from your community's own quick codes, show it on the map, or copy the crew's names. Everything there was already possible and took two or three clicks through a detail panel.
The same menu is on the units listed inside a call, which is where you are actually looking when a crew says they have arrived.
Status codes and states
Your community defines its own status codes — the labels, the colors, the timers and which appear as quick buttons. Every code maps onto one of nine underlying states, which is what keeps the boards, timers and recommendation working whatever you call things:
| State | Means | Dispatchable? |
|---|---|---|
off_duty | Not working | No |
available | In service, free | Yes |
en_route | Traveling to a call | No — committed |
on_scene | Arrived | No — committed |
busy | Unavailable for assignment | No |
transporting | Moving a patient or a prisoner | No — committed |
at_hospital | At the destination | No — committed |
out_of_service | Mechanical, meal, training | No |
panic | Emergency | No — committed |
Only available is offered for a new assignment. So a community that
renames 10-8 to In service and maps it to
available keeps every piece of automatic behavior.
The status log
Every change is written down: what it went from and to, how long the unit spent in the previous status, which call it was on if any, where the change came from — the board, the terminal, the game, the API — and whether dispatch forced it rather than the unit choosing. That last one is the question actually being asked whenever somebody looks a shift up: the crew cleared early, or dispatch cleared them?
On a call, each change also lands on that call's own timeline, which is what you read when you open the call. The log is the other half — the transitions that belong to no call at all: went available at the station, out of service for fuel, off duty mid-shift. A supervisor reconstructing a night needs those gaps as much as the calls.
| Endpoint | What it answers |
|---|---|
GET /v1/logs/unit-status | The log for a window, newest first. Filter with unitId, callId, or offCallOnly=true for the transitions no call will ever show. Defaults to the last 24 hours. |
GET /v1/units/:id/status-log | One unit's own. |
The roster
A separate panel from the status board, because they answer different questions — "who can I send" versus "who is actually playing" — and mixing them makes both worse. The roster shows everyone in the community with their agency, rank, callsign, whether they are on duty, and their hours over the last seven days.
The map
Drawn on a canvas in game coordinates, which is worth one sentence of explanation: San Andreas is not on Earth. Its coordinates are a flat local system, the jurisdiction polygons are stored in that system, and a conventional slippy-map library would mean projecting everything twice to arrive back where it started. Canvas also redraws two hundred moving units without breaking a sweat.
The basemap
Units, calls and zones are drawn over a tiled map of San Andreas. The tiles are
FiveNet's livemap-tiles — Apache-2.0 and community-drawn, cut from the
Virus_City postal map and the DLK HD atlas, and deliberately not Rockstar's own
imagery, which is what makes them something we can host for you. Sal's Kewl Dispatch
already draws the same set, so a community running both sees one map in two places
rather than two that disagree.
There is nothing to configure and nothing to upload. If you self-host and want your own cut of the atlas, or no basemap at all, the web app reads three settings at build time:
| Setting | What it does |
|---|---|
VITE_MAP_TILES | A {z}/{x}/{y} tile URL. Set it to off for the plain grid. |
VITE_MAP_TRANSFORM | World coordinates to map pixels, as sx,ox,sy,oy. The default is the transform any 16384px gdal2tiles cut of the standard atlas produces — change it only together with the tiles, or every unit ends up in the sea. |
VITE_MAP_ZOOM | Minimum and maximum zoom, as min,max. |
What is on it
- Units, colored by agency and shaped by kind, moving live.
- Calls, colored by priority.
- Hospitals, stations and hydrants, on a toggle.
- Jurisdiction boundaries, on a toggle.
- Postals, if your community uploaded a postal map.
- Drawings — closures, perimeters, staging areas — put there by
anybody with
map_draw. - A heat map of where calls have been over the last thirty days, for supervisors deciding where to post people.
What you can do on it
| Action | How |
|---|---|
| Select a call | Click it. Calls win over units under the cursor, because a click usually means "that call". |
| Create a call where you clicked | Right-click. |
| Assign a unit | Drag it from the status board onto the map. |
| Follow a unit | f <callsign>, or click it on the board. |
| Fit everything | The fit button. |
How it frames itself
The first frame that has data fits the camera around what is actually happening, rather than opening on the whole island with every unit as a dot in one corner. After that the camera is yours.
Privacy on the map
Units marked plain-clothes are hidden from everyone except dispatch, on by default. Federal agencies can start hidden from other agencies' maps entirely. Off-duty players are never on it at all.
There is no satellite tile layer behind the map yet — it draws in game coordinates with no imagery underneath. Tiles layer in behind everything described here without changing any of it when that lands.
BOLOs, alerts & messages
BOLOs
Issue one from + BOLO, Alt +
B, the BOLOs panel, or /bolo in game. A BOLO
has a subject — person, vehicle, plate, property or other — a title, a body, an
optional plate, and the agencies it is for.
Broadcasting one pushes it into the game: it reaches every terminal and shows in the notification system of every unit it is addressed to. A BOLO with a plate on it also joins the hotlist, so an ALPR read of that plate comes back as a hit.
BOLOs are canceled rather than deleted, so the record of what was out and when survives.
Alerts
The Alerts panel is the feed of things that happened which are not yet calls: panic buttons, backup requests, alarm activations, ALPR hits, toll violations, radio emergencies and anything a third-party script sent in.
What becomes a call and what stays an alert is your community's decision, set per alert type in the rules engine — see Integrations & alert rules. A dispatcher can dismiss an alert, or act on it.
Panic
/panic, the radio emergency button, or a phone script. It goes to
every dispatcher immediately, creates a priority 1 call with the officer's position,
and the unit's status goes to panic — which puts it at the very top of
the board, above everything else, with its own color.
This is the one thing in the system that is deliberately impossible to miss.
Messages
Text between dispatch and units, on channels. It reaches every terminal and every
dispatcher screen straight away. A dispatcher with
message:broadcast@tenant can send to everyone at once.
Messages are kept for 30 days by default, which is adjustable. They are not a record of anything — use a call note for that, because call notes live on the call's timeline and survive forever.
The lookup panel
Type a plate, a name, a serial, an address or a phone number and the panel works out what you meant. The answer leads with what would hurt somebody — warrants, armed and dangerous, officer safety flags — and then gives the identifying facts.
What the answer contains depends on who is asking. Law enforcement sees criminal history; fire and EMS see blood type, allergies, conditions and DNR; neither sees the other's half, and the panel says which half is missing rather than pretending it is empty. That sentence is the difference between a medic trusting the system and a medic assuming it is broken.
Run cards & response plans
Three different things, kept as three different fields on purpose, because conflating them is how fire statistics become meaningless:
- A caller reports a structure fire. That is the call type.
- Three engines, a ladder, a rescue, a chief and a medic roll. That is the response.
- It turns out to be a cooking fire confined to the container. That is the incident type, coded afterward.
Levels are cumulative
A second alarm is everything in the first plus everything in the second, which is how a real escalation works — you do not cancel the first-alarm assignment when you strike the second. The platform follows that: raising a call from a first alarm to a second sends only the second alarm's apparatus, because the first alarm's units are already on the call and are skipped. Striking the same level twice sends nothing the second time.
The plans you start with
| Plan | First alarm | Covers |
|---|---|---|
| Residential structure box | 2 engines, 1 ladder, 1 rescue, 1 battalion, 1 medic | Structure fire. Two engines put water on it and open it up; the rescue is the crew that goes in for anybody still inside, and the medic stands by for them as much as for the occupants. Second and third alarms add lines, a second ladder, a tanker, a division supervisor and rehab. |
| Commercial structure box | 3 engines, 2 ladders, 1 rescue, 2 battalion, 1 medic | A bigger building needs more water and more people before the first line is even charged. Heavier at every level. |
| Vehicle fire | 1 engine | Unless it is against a building, in which case it gets a box instead. |
| Brush & wildland | 2 brush, 1 engine, 1 tanker | Brush units first because they can leave the road, an engine for structure protection, and a tanker because there is rarely a hydrant where this happens. The extended attack adds air attack. |
| Alarm activation | 1 engine, non-emergent | Nine out of ten of these are burnt toast or a contractor — and the tenth is why somebody still goes. The upgrade level is a full box. |
| Vehicle extrication | 1 rescue, 1 engine, 1 medic | A rescue for the tools, an engine for the hose line standing by over the fuel, and a medic for the person in the car. |
| Hazardous materials | 1 engine, 1 battalion | Nobody commits until somebody has identified the product. The full hazmat level adds an entry team, decon and responder rehab. |
| EMS — single unit | 1 medic | The great majority of medical calls, and it needs nothing else. |
| EMS — cardiac arrest | 1 medic, 1 engine, 1 supervisor | An arrest needs hands more than it needs vehicles: enough people to rotate compressions without stopping. The engine company is there to be those hands. |
| EMS — multiple casualties | 3 medics, 1 engine, 1 supervisor | More patients than one crew can triage. Every level adds transport capacity, and command exists from the first minute because the hard part is sorting rather than treating. |
| EMS — interfacility transfer | 1 ambulance, non-emergent | Scheduled and routine, and it should never tone a station out. |
Every one of these is a starting point. Every department argues about its own run cards; the point of shipping defaults is that a new community is not staring at an empty screen on its first structure fire.
Response modes
Each level is emergent (lights and siren) or non-emergent (normal traffic), and the mode goes down to the game with the assignment.
How a plan is matched to a call
In order, most specific first. The first one that matches wins, and the dispatcher is told which it was:
- A drawn box — a response area somebody drew on the map.
- The station district the call falls in.
- The call type's own plan.
- The old flat run-card count on the call type, if a plan was never configured.
Nothing matching is the normal answer for a police call — run cards are a fire and EMS idea and most calls do not have one.
Using it
Run card on the call panel shows what will be sent before it is sent, with a line saying how the plan was matched. A dispatcher committing six pieces of apparatus on a button press wants to have read the list first — and "why did this call get the commercial box" needs an answer.
Press Strike on a level and it fills that level's requirements with real apparatus.
How apparatus is picked
Due order first. A station's list is what the department decided in advance, and second-guessing it with a distance calculation is how you end up sending the wrong engine to a street that borders two districts. Only when the due list is exhausted — or was never configured — does it fall back to the nearest available unit of the right kind.
Shortfalls
If the plan asks for three engines and two are available, you are told so, loudly, naming what was short and by how much. That is deliberate: asking for three engines and getting two is the single most important thing to know on a working fire, and exactly the sort of thing a quieter interface would swallow into a notification nobody reads. The message says what it means — nothing else of that type is on duty and free, so mutual aid or a move-up is the next call.
The times, and why they are automatic
As each piece of apparatus changes status, the platform stamps a milestone: dispatched, en route, on scene, transporting, at hospital, available. Turnout and travel times are computed from those.
Nobody types any of it. A time a dispatcher has to remember to record is a time that does not get recorded, and the numbers a department is judged on are only ever right if they are automatic. First write wins for every stamp: a crew that goes en route, arrives, then goes en route again to the hospital has one turnout time, not two.
Who can do what
| Permission | Who holds it | What it is |
|---|---|---|
run_card:view | Everyone on the fireground | See the card you are running on. |
run_card:strike | Dispatchers, supervisors, command | Escalate a working fire to the next alarm. |
run_card:manage | Command only | Edit what an alarm level means. |
Station alerting
Behind the Station alerting realism toggle. With it on, the Stations panel appears and the platform can set a firehouse off.
Setting a house off
Three ways:
- From a call — Tone out on the call panel tones the stations whose agencies are on it.
- From the Stations panel — pick a station, pick the call it is for, or run it as a drill with no call at all.
- Automatically, when a fire or EMS call is assigned, if you turned on automatic tone-out on assignment.
A station whose alerting is switched off is refused rather than silently skipped — somebody pressed a button and is waiting for something to happen, and silence is the worst possible answer.
Acknowledgement
When somebody hits the reset at the watch desk in game, that comes back up and the panel shows the house as acknowledged, who hit it, which units are responding and against which call. A station that has not acknowledged stays visibly un-acknowledged, which is the whole reason to have a panel rather than just a button.
Setting stations up
A station has a name, a slug, a tone set, an apparatus roster and an on/off switch for alerting.
The slug must match the station id in Sal's Kewl Station Alert. Without one, the game cannot match the station and the tone-out goes nowhere. The panel warns you when a station has no slug.
With Sal's Kewl Station Alert
If you run that resource, the two halves fit together with nothing to configure on the bridge side:
- the CAD's tone-outs set the house off — lights, tones and the run on the board;
- the station's call board reads the CAD's live board, so the wall screen shows what dispatch sees;
- the watch-desk reset reports back as an acknowledgement.
Stations and apparatus created in game appear here on their own. See that resource's own manual for the game-side half.
Without it
Station alerting still works — the Stations panel, the tone-out record and the acknowledgement state all live on the platform. What you lose is the physical house lighting up, which is the part that happens in the world.
Records
A full page rather than a dock panel, at /cad/records, because
looking somebody up properly is a sit-down job and a records system squeezed into a
320-pixel panel is a records system that gets abandoned.
The indexes
| Index | Holds |
|---|---|
| People | The master name index. Identity, address, phone, physical description, licenses, flags, criminal history, medical details, known vehicles and associated reports. |
| Vehicles | Plate, model, color, owner, registration, insurance, and whether it is stolen, flagged or impounded. |
| Firearms | Serial number, type, registered owner, and whether it has been reported stolen or used in an incident. |
| Businesses | Registered businesses, owners and addresses. |
| Property & evidence | Evidence items with a chain of custody — every hand it has passed through, and when. |
| Reports | Everything written, by type and status. |
Where records come from
Mostly, nobody types them. On a framework server, characters, their owned vehicles and their licenses arrive from the game on their own — when a player loads in, when their job changes, and in a slow pass every few minutes. A dealership sale reaches the CAD as a change of owner on the plate.
The rest is created by officers as they work: a person who has never been in the system gets a record the first time somebody writes a report naming them.
On a fresh community, lookups legitimately return nothing. The records are in the CAD, not in the game, and the CAD has not met anybody yet. Import what you have from Settings → Import, or let records accumulate for a week. This surprises people often enough to be worth telling your staff up front.
Flags
Flags are what change how a name search reads, and they are the reason the lookup puts danger first:
| Flag | Means |
|---|---|
armed_dangerous | Armed and dangerous. |
officer_safety | A caution without a specific weapon. |
felony_conviction | Prior felony. |
mental_health | Known mental health history. |
medical_alert | A condition responders need to know about. |
missing | Reported missing. |
gang | Known affiliation. |
probation / parole | Under supervision. |
do_not_detain | Usually an undercover officer or a protected witness. Read it before you act on anything else. |
custom | Whatever your community needs. |
Search
One box across every index. It will take a plate, a partial name, a serial, a phone number, an address or a report number and work out which it is. The lookup panel in the workspace and the terminal's lookup tile are the same search, shaped for smaller screens.
The medical and criminal split
Worth repeating because it is the behavior people ask about most:
- Law enforcement — the eight LEO department types — read criminal history.
- Fire and EMS read blood type, allergies, conditions and DNR.
- Neither gets the other's half by default, and the interface says which half is missing rather than showing an empty box.
Both are ordinary permissions — person_criminal:view and
person_medical:view — so a community that wants its fire investigators
to read criminal history grants it.
Reports & the form engine
The report types
| Type | Written by |
|---|---|
| Incident | Anyone |
| Arrest | Law enforcement |
| Citation | Law enforcement |
| Warning | Law enforcement |
| Field interview | Law enforcement |
| Use of force | Law enforcement. Agency-private by default. |
| Pursuit | Law enforcement |
| Evidence | Law enforcement, corrections |
| Impound | Law enforcement, tow |
| Warrant request | Law enforcement |
| Fire incident | Fire. Its own permission; statistics are built on it. |
| Fire inspection | Fire |
| Fire pre-plan | Fire |
| Apparatus check | Fire, EMS |
| Patient care report | EMS. Its own permission, agency-private by default. |
| Tow job | Tow |
| Shift log | Anyone |
| Supplement | Added to an existing report |
The life of a report
| Status | Means |
|---|---|
draft | Being written. Only the author sees it. |
submitted | Handed in. With approvals off, it goes straight to approved. |
returned | A supervisor sent it back with a note. |
approved | Accepted. |
locked | No longer editable. Changes go in a supplement. |
amended | A supplement was added. |
sealed | A judge sealed it. See Courts. |
With Supervisor review off — the default — a submitted report is approved and locked. Turn it on and reports wait for a supervisor, who can approve or return with a note.
Forms are data, not code
A report is a form definition — a versioned schema of sections and fields — plus the values. The form drives the sections, the fields, their widths, and which of them get copied into indexed columns so search can find them.
That means a community that adds a field to its arrest report sees it on the next load, in the browser and in the in-game terminal, with no release from us. Building your own forms is a Pro feature; editing the shipped ones is available to everybody.
Field types
| Type | What it is |
|---|---|
text, textarea, number, currency | The plain ones. |
select, multiselect, checkbox | Choices. |
date, datetime, time | When. |
person | Links to a person record, with lookup. |
vehicle | Links to a vehicle record, with plate lookup. |
charges | The charge picker, which totals fines and jail time from your penal code. |
location | A map point plus street and postal. |
unit | Unit picker from the current roster. |
vitals | Repeating vitals rows, for a patient care report. |
attachments | File upload. |
signature | A typed name plus a timestamp. It is not a legal signature and says so. |
nfirs_code | The NFIRS code picker, shown only with detailed fire coding on. |
Fields can repeat, can show conditionally on another field's value, and can be marked to copy into an indexed column — person, vehicle, location, when it happened, the narrative, the charges or an amount — which is what makes a report findable by search afterwards.
Short forms are deliberate
The shipped forms have only the fields most people actually fill in, and every extra section is marked optional so you turn it on on purpose. A ten-section arrest report is how a records system stops being used; it is better to have a hundred complete short reports than twenty complete long ones and eighty abandoned drafts.
Where reports are written
Anywhere. The browser, the in-game terminal, or started in one and finished in the
other — it is the same draft against the same form. /cite,
/arrest and /incident open prefilled from what the game
knows.
Attachments
Photographs, bodycam links and files, against your plan's storage allowance. Uploads are scanned where a scanner is configured; your community's allowance is on the owner panel's first page.
Sealing
A judge can seal a report, which takes it out of every ordinary view and search. Unsealing is also a judicial action, and both are recorded in the audit log with who and when.
Citations, fines & payment
Writing one
/cite in game opens the form with the nearest person, the plate in
front of the car and the street already filled in. From the browser, Records → new
citation.
The charge picker reads your community's penal code and totals the fine and the jail time as you add charges, so an officer is never adding up numbers.
The penal code
Your charge list, editable under Settings, with a class, a fine and jail time per charge. The five classes:
| Class | Typically |
|---|---|
infraction | A fine, no jail. Most traffic. |
citation | A written citation with a court date. |
misdemeanor | Fine and short jail. |
felony | Serious. Usually a case file. |
ordinance | Local rules. |
The code can be imported and exported, which is the easy way to move a penal code between a test community and a live one, or to adopt one your community already wrote.
Billing it to the player
Bill on a citation sends a fine.issue.v1 down to the
game, which charges the character through your framework. This is one of the commands
that survives an outage: it goes through a job queue with retries
rather than being dropped, because a fine that silently vanishes is a fine the player
never pays and nobody can explain.
A fine can be issued against an offline character and lands when they next load in.
Paying it
Two ways:
- From the citizen portal, if your community allows online payment. The player sees what they owe and pays it.
- In game, through whatever your framework or a payment script
does. The result comes back as
payment.v1and the citation is marked paid.
Court dates
A citation can carry a court date, which puts it on the docket when court workflow is on and shows in the player's Court tab. With court workflow off, the date is just a field — useful, but nothing schedules itself.
Licenses & the DMV
The license types
driver, cdl, motorcycle,
weapon, hunting, fishing, pilot,
boating and business.
The statuses
| Status | Means |
|---|---|
valid | In good standing. |
suspended | Temporarily withdrawn, optionally with a date it lifts. |
revoked | Taken away. Needs reissuing, not reinstating. |
expired | Lapsed. |
none | Never held one. |
Who decides what
This is the clearest case of the split described in How it fits together:
- The game decides whether a license exists. A character who passed the driving test has one.
- The CAD decides whether it is in good standing. A suspension is a CAD action.
The framework keeps a copy so in-game scripts — the vehicle dealer, a traffic stop
check — can read it without a round trip. Every change the CAD makes goes down to the
game and is applied to the character, in qb metadata, esx_license or
ND's licenses, depending on your framework.
Suspensions reach the game, even offline
Suspend a license while the player is offline and it lands the next time they load in. The platform re-sends any standing suspension whenever the game copy disagrees with it, which is what makes that work rather than depending on a message arriving at the right moment.
If you would rather the CAD recorded suspensions without touching the game, set
Config.Sync.ApplyLicenses = false.
Reacting to it in your own scripts
Listen for sals_kewlcad:licenseChanged(data, applied). The second
argument says whether the bridge already changed the character's license in the
framework, so a DMV script or a vehicle shop can do its own thing without
double-applying.
Points, if you want them
The DMV agency template exists for communities that roleplay the counter: DMV
clerks can issue, suspend, revoke and reinstate, register vehicles, and create people.
Give the role to whoever staffs it and law enforcement keeps only
license:view, which is usually what a community wants — an officer sees
the status and the DMV changes it.
Courts, warrants & the jail
Mostly behind the Court workflow toggle, because most communities do not roleplay court and a module they will not use is clutter. For the ones that do, this is where the chain ends: a stop becomes an arrest, the arrest becomes a case, the case becomes a sentence, and the sentence starts a timer in the game.
Warrants work either way
Warrants are law enforcement, not court, and they work whether or not court workflow is on. The lifecycle:
| Status | Means |
|---|---|
requested | An officer asked for one, with probable cause. |
signed | A judge granted it. |
active | Out and servable. |
served | Executed. |
recalled | Withdrawn. |
expired | Timed out. |
denied | A judge said no. |
Four types: arrest, search, bench and
failure_to_appear. Requesting and signing are deliberately different
permissions — officers request, judges sign.
An active warrant shows at the top of that person's lookup, in game and in the browser, and puts their known plates on the hotlist so an ALPR read comes back as a hit.
Cases
| Status | Means |
|---|---|
open | Filed. |
arraignment | First appearance. |
pending_trial | Scheduled. |
in_trial | Being heard. |
adjudicated | Decided. |
dismissed | Thrown out. |
appealed | Under appeal. |
closed | Finished. |
sealed | Out of ordinary view and search. |
Pleas are not_guilty, guilty, no_contest or
none. Verdicts are guilty, not_guilty,
dismissed, mistrial or plea.
The docket
Scheduled hearings, with the case, the date and who is due. It is also what a player sees in the Court tab of their portal, so scheduling a hearing is how you tell somebody to turn up.
Sentencing
A sentence carries jail time, a fine and community service. Handing one down sends
sentence.start.v1 to the game, which starts the jail clock on that
character. Like a fine, this is one of the commands that survives an outage.
The jail
The corrections agency gets the booking queue, the cell roster and sentence
timers. Booking statuses are booked, held,
sentenced, released and escaped.
The three court roles
| Role | Can |
|---|---|
| Clerk of Court | File cases, schedule hearings, keep the docket, read citations, arrests, warrants and reports. |
| Attorney | District attorney or public defender: read case files, criminal history, evidence and reports, update cases and file for hearings. |
| Judge | Everything above, plus: sign and revoke warrants, rule on cases, hand down sentences, revoke licenses, seal and unseal cases and reports, and edit the penal code. |
Sealing is a judicial power and nothing else in the platform grants it — which is the point. A sealed record is one the police cannot quietly unseal.
Tow & impound
Tow jobs
A tow is requested with /tow from an officer at a scene, created by a
dispatcher, or raised by a script. It lands in the tow queue, which drivers see in
their terminal and dispatch sees in the Tow panel.
| Status | Means |
|---|---|
queued | Waiting for a company. |
offered | Offered to whoever is next on the rotation. |
accepted | Taken. |
on_scene | Driver has arrived. |
complete | Done. |
canceled | Called off. |
First come, or rotation
Two models, chosen with the Tow rotation realism toggle:
- Off (default) — a job sits in the queue and whichever driver grabs it first gets it. Simple, and fine on a server with one tow company.
- On — jobs are offered to whichever company is next in line, in order, the way a real rotation list works. Worth turning on the moment you have two companies competing, because otherwise the one with the fastest clicker gets everything.
The rotation itself is managed by anyone with
tow_rotation:manage@tenant — a yard manager or a dispatcher.
Impounds
An impound records the vehicle, where it was taken from, why, the release fee, the lot and space, an inventory and condition photographs.
| Status | Means |
|---|---|
impounded | In the lot. |
hold | Investigative hold — it is not going anywhere until somebody says so. |
released | Returned to the owner. |
auctioned | Disposed of. |
Impounding a vehicle sends impound.flag.v1 to the game, so the
vehicle is flagged there too and your garage script can refuse to hand it over.
Releasing it sends the opposite.
The two tow roles
| Role | Can |
|---|---|
| Driver | Take tow jobs, create impounds, write reports and invoices. |
| Yard Manager | Run the rotation, release vehicles, manage drivers, and configure the agency. |
Hospitals & EMS
The hospital board
Destinations with their current diversion status, so a medic transporting is sent somewhere that can take the patient:
| Status | Means |
|---|---|
open | Taking everything. |
limited | Taking some things. Call first. |
closed | On diversion. Go somewhere else. |
Anyone with hospital_board:manage sets the status. Fire, EMS and
dispatch read it.
Transport
/transport puts a unit in the transporting state;
/athosp marks arrival at hospital. Both stamp milestones, which is where
the transport and at-hospital times in the fire and EMS statistics come from.
Patient care reports
The PCR is its own resource with its own permissions, and it is agency-private by default — only the EMS agency that wrote one reads it. That is the one default we would recommend leaving alone.
With Detailed fire and EMS coding on, the PCR gets the fuller vitals layout: repeating vitals rows with times, rather than a single set.
The medical record a medic actually reads
Blood type, allergies, conditions and the DNR flag, from the person's record. Players fill those in themselves in the citizen portal, and no framework tracks any of it — so the quality of this depends entirely on whether your players know the portal exists. It is worth a pinned message.
EMS roles
Medic, Supervisor and Command. A medic reads and updates patient medical details, writes and submits patient care reports, and sees the hospital board, station alerting and run cards. Supervisors approve PCRs.
Signs & highway boards
The electronic signs in the game — overhead gantries, roadside boards, variable message signs — as reported by the sign resource running on your server.
It is a mirror, not a second source of truth
The game decides what a sign looks like; the CAD only asks it to show something. The sign resource sends its whole inventory up whenever a board is placed, moved, deleted or changes what it says, and that replaces the previous set wholesale. So the panel is always showing what is actually out there.
Putting a message up
Five kinds, and the sign resource decides what each looks like because the CAD does not know what a gantry looks like:
| Kind | For |
|---|---|
amber_alert | A missing child or a vehicle to look for. |
bolo | A BOLO pushed to the highway. |
closure | A road closure. |
notice | Anything else. |
clear | Take it down. |
A message can carry an expiry, so a closure takes itself down.
Modes
Instead of a message you can set a named mode — a preset defined on the game side. The panel tells you when a board is being held by a mode, by the CAD, or by another resource, so two systems fighting over one sign is visible rather than mysterious.
With Sal's Kewl Signage
Amber alerts, BOLOs and closures push from the CAD onto the boards, and radar
readings come back up as plate reads which can become ALPR hits. Configured in that
resource under Config.Cad; nothing to set on the bridge side.
From your own script
Report your signs with ReportSigns(signs, modes) and the CAD's panel
draws them. See Exports & events for scripts.
Statistics
Three questions, which is all a community actually asks: how fast are we getting there, what are we getting called to, and who is playing.
Response times
Three intervals, each measured from the append-only milestones on the call rather than from anything a client reported — which is why they are worth putting on a screen at all:
| Interval | From | To |
|---|---|---|
| To dispatch | The call arriving | A unit being assigned |
| Turnout | Assignment | En route |
| Travel | En route | On scene |
What we get called to
Call volume by type, by priority, by agency and per day. Useful for the obvious reason and for one less obvious one: it shows which of your call types nobody ever uses, which is usually a sign the list is too long.
Activity
Who was on duty, for how long, over the period. Also on the Roster panel as a seven-day figure per member.
Fire and EMS statistics
A separate page, because the questions are different. A police commander asks how many calls and how long to arrive. A fire chief asks about turnout time, per station and per apparatus, because those are the numbers that justify a budget or lose one.
Everything on it is read from the response milestones stamped automatically as each piece changes status. None of it is typed by anyone, which is the only reason it is worth reading.
Why the 90th percentile
The page leads with the 90th percentile rather than the average, and quotes it against NFPA 1710:
| Benchmark | NFPA 1710 |
|---|---|
| Turnout, fire suppression | 80 seconds |
| Turnout, EMS | 60 seconds |
| Travel, first arriving engine | 4 minutes |
All three are stated at the 90th percentile in the standard, and that is the point: an average hides the bad night that everyone remembers. A department that is slow and consistent reads as slow, which is the honest answer.
The fire page also shows
- By station and by apparatus — where the slow numbers actually are.
- Escalation — how often a first alarm was not enough, which is the number that tells a chief their run cards are wrong.
- What we were actually called to — incident types as coded afterward, against the call types as reported. The gap between those two is interesting on its own.
- Property loss, where it was recorded.
Exporting
Anyone with stats:export can take the underlying rows out. Advanced
dashboards are a Pro feature; the basic figures are on every plan.
Integrations & alert rules
Connected products
The other Sal's Kewl resources are detected at runtime and need no configuration on the bridge side. Put them on the same server and they find each other:
| Product | What it adds |
|---|---|
| Sal's Kewl Dispatch | Your 9-1-1 call takers work a call and dump it into the CAD whole: the caller, the location, the transcript, the answers they gave, the people and vehicles mentioned, and the times. The CAD answers with the call number it was given, so the call taker can read it back. |
| Sal's Kewl Radio | Channel activity and the emergency button reach the Radio panel. A radio emergency becomes a priority 1 call. |
| Sal's Kewl Station Alert | Tone-outs set the firehouse off, the station's wall board reads the CAD's live board, and the watch-desk reset comes back as an acknowledgement. |
| Sal's Kewl Signage | Amber alerts, BOLOs and closures go out to the highway boards; radar readings come back as plate reads. |
Shims for other dispatch resources
Three are forwarded without editing them: ps_dispatch,
cd_dispatch and core_dispatch. If your server already runs
a robbery or crime script that talks to one of those, its alerts reach the CAD the
moment you switch the shim on.
The alert rules engine
Any script — ours or yours — can send an alert. What happens next is your community's decision, not ours, and that decision is a rule.
Three possible actions:
| Action | What happens |
|---|---|
| Create a call | It becomes a call of the type and priority you chose, routed to the department types you named. |
| Tell dispatch only | It appears in the Alerts panel. A dispatcher decides whether it is worth a call. |
| Ignore it | Dropped. |
The three knobs that keep the feed usable
| Knob | What it does |
|---|---|
| De-duplicate | Merge a second alert of the same type within x meters and y seconds. Four people reporting the same crash is one call. |
| Cooldown | Ignore further alerts of this type for a while, wherever they are. Right for speed cameras. |
| Reporting chance | The chance an alert is reported at all, 0 to 1. |
Reporting chance looks like a bug the first time you meet it, and it is not. "60% of drug sales get reported" is a roleplay balance setting. Not every crime in a city gets called in, and a server where every one does turns dispatch into a metronome. It sits next to the rest of the rule because it is the same kind of decision.
There is also a rate cap per minute per resource, which is what stops a misbehaving script from filling your queue.
The rules you start with
Chosen so a server that plugs in a robbery script and a shots-fired detector gets sensible dispatch behavior with no configuration at all.
| Alert type | Action | Becomes | P | De-dupe | Chance |
|---|---|---|---|---|---|
robbery_store | Call | ROB | 1 | 120 m / 5 min | — |
robbery_bank | Call | ROB | 1 | 200 m / 10 min | — |
robbery_jewelry | Call | ROB | 1 | 150 m / 5 min | — |
burglary_house | Call | BURG | 2 | 60 m / 5 min | 80% |
alarm_business | Call | ALARM | 3 | 50 m / 10 min | — |
alarm_residential | Call | ALARM | 3 | 50 m / 10 min | — |
alarm_fire | Call | ALARMFIRE | 3 | 50 m / 10 min | — |
shots_fired | Call | SHOTS | 1 | 100 m / 1 min | — |
vehicle_theft | Call | GTA | 3 | 80 m / 3 min | 70% |
carjacking | Call | GTA | 1 | 80 m / 2 min | — |
drug_sale | Notify | — | — | 2 min cooldown | 40% |
fight | Call | DIST | 3 | 50 m / 3 min | 60% |
medical_alert | Call | MEDB | 2 | 30 m / 2 min | — |
speed_camera | Notify | — | — | 30 s cooldown | — |
alpr_hit | Notify | — | — | 60 s cooldown | — |
toll_violation | Notify | — | — | 60 s cooldown | — |
suspicious_person | Call | SUSP | 4 | 60 m / 5 min | 50% |
phone_panic | Call | PANIC | 1 | 30 m / 1 min | — |
radio_emergency | Call | PANIC | 1 | 30 s | — |
Every row is editable, and you can add rules for alert types of your own invention.
The plate hotlist
The bridge keeps a list of plates worth knowing about in memory, so an ALPR read can be checked without a round trip. It is derived from warrants, BOLOs and vehicle flags rather than being a second list that goes stale — so flagging a car makes it an ALPR hit immediately, and clearing the flag stops it.
IsPlateFlagged(plate) is the export; it returns whether and
why.
Outbound webhooks
Pro and above. Send CAD events to your own bot or website — a Discord bot that posts new calls, a status page, a leaderboard. Configured on this page, with a per-plan cap on how many.
Nights ERS callouts
If you run Nights Emergency Response Simulator
(night_ers) beside the bridge, its callouts reach your dispatch board
and can be worked like anything else. It is detected at runtime: install it and the
integration starts, remove it and nothing breaks, and it is never listed as a
dependency.
The rule this is built around
An offered callout is never written to the CAD. The simulator offers far more work than a community answers, and a record of every refusal would bury the calls you actually ran underneath the ones nobody took.
So an offer appears on the board as an offer — with no call number — and is held only in memory, with a short life. If nobody takes it, it expires and there is nothing to clean up, because nothing was written: no call, no number, no entry in any history.
Accepting is what creates the call. From that moment it is an ordinary call: a number, a timeline, units, notes, and it closes like any other. There is no separate ERS code path and no reduced history, which is the point — "tracked as if it were a real call" is only true if it stops being a special case the moment it becomes one.
What reaches the board
| In ERS | In the CAD |
|---|---|
| A callout is offered | An offer on the board, no call number. Dispatch sees it, and so does the unit it was offered to. |
| Nobody takes it | It disappears. Nothing was saved. |
| A unit accepts | A real call, numbered and timed, with the unit on it. The people and vehicles ERS invented arrive with it, as the call's narrative and flags. |
| The unit arrives | They go on scene, which stamps the call the same way an arrival always does. |
| The callout completes, or ends | The call closes, with a disposition that keeps the difference between the two. |
| A patient is delivered to hospital | A line on the call's timeline. |
| A traffic stop or pursuit starts | A real call immediately — there is no offer step, because the unit is already out with it. Pursuits come in as priority 1. |
| A stop or pursuit ends | A note on the call. The call is not closed: a stop that turns into an arrest is still being worked after ERS considers its scene finished. |
Dispatch can push one
A dispatcher can send a callout offer to a particular unit, which is what makes the CAD in charge of the simulator rather than a log of what it already decided. The bridge answers that by calling ERS's own offer function for that player.
Setting it up
- Install
night_ersas its authors describe. - Update the bridge to 2.2.0 or later.
- Leave
Config.Ers.Enabledattrue, and setOfferTimeoutMsto match your ERS offer timeout. - Restart both. The CAD's game server page lists
night_ersamong the detected resources once the bridge has said hello.
Turn stops or pursuits off with Config.Ers.TrafficStops and
Config.Ers.Pursuits if you would rather those stayed off the board —
see the configuration reference.
What it does not do
- An expired offer leaves no trace. There is no list of callouts your community turned down, by design.
- ERS decides when its own offer lapses. The CAD's timeout only controls how long the offer is shown, and it holds the detail slightly longer than that so a unit accepting a moment late still gets a call with everything on it.
- Nothing about your own calls, units or records changes when ERS is installed. An ERS call is a call.
The public API & webhooks
Pro and above. A documented, versioned REST surface so third-party scripts and your own website can read from the CAD.
Keys
Public API keys come from the same place as bridge keys but have a different scope, deliberately: a bridge key cannot read the public API and an API key cannot send game events. One key, one job. Both can be rotated and revoked from the owner panel.
The endpoints
| Endpoint | Returns |
|---|---|
GET /v1/public/status | Live server status — units on duty, open calls. For your community's own website. |
GET /v1/public/calls | Open calls. Sensitive calls are excluded. |
GET /v1/public/units | Units on duty. |
GET /v1/public/bolos | Active BOLOs. |
GET /v1/public/plate/:plate | A plate check. What most third-party scripts actually want. |
GET /v1/public/schema | The developer reference: every event type and its shape. |
GET /v1/public/schema/:type | One event's shape, for wiring up an integration. |
The schema endpoint is the documentation
It is published from the same definitions the gateway validates against, so the shapes it describes and the shapes that are accepted cannot drift apart. That is the failure mode which makes integrating with somebody else's CAD miserable, and it is worth the one endpoint to avoid.
Reads here, writes through the bridge
The public API is read-mostly on purpose. Writes go through the bridge endpoints, which already have signing, nonces and idempotency worked out — there is no reason to build a second way in with weaker guarantees.
If your script runs outside the game, or you would simply rather use HTTP, the bridge's own alert endpoint takes third-party alerts over plain HTTPS with the same authentication and the same rules engine.
Rate limits
Per minute, per plan: 60 on trial, 300 on Community, 1,200 on Pro and 6,000 on Network. Bridge events are counted separately and far more generously, because a busy server legitimately sends a lot.
Exports & events for scripts
The Lua surface the bridge gives your own resources. Call them as
exports.sals_kewlcadbridge:Name(...), and wrap each in
pcall — the bridge may be reconnecting.
Alerts and calls
-- Third-party alert. The one most integrations want.
exports.sals_kewlcadbridge:SendAlert({
title = 'Silent alarm',
kind = 'alarm',
coords = GetEntityCoords(PlayerPedId()),
priority = 2,
})
exports.sals_kewlcadbridge:CreateCall({
typeCode = '10-90', location = '...', narrative = '...',
})
exports.sals_kewlcadbridge:AddCallNote(callNumber, 'text')
Units and duty
exports.sals_kewlcadbridge:IsOnDuty(source) --> boolean
exports.sals_kewlcadbridge:GetOnDutyUnits() --> table
exports.sals_kewlcadbridge:SetUnitStatus(source, '10-8')
exports.sals_kewlcadbridge:GoOnDuty(source, { unitId = '...' })
exports.sals_kewlcadbridge:GoOffDuty(source)
exports.sals_kewlcadbridge:Panic(source, 'note')
exports.sals_kewlcadbridge:RequestBackup(source, 'urgent', 'note')
SetUnitStatus takes a state — en_route,
on_scene, available, busy,
out_of_service — as well as a code, for resources whose verbs are
states.
Lookups on a unit's behalf
-- Answered by event, exactly once, so a radio can read it back on the air.
local ref = exports.sals_kewlcadbridge:Lookup(source, 'plate', '46EEK572')
AddEventHandler('sals_kewlcad:lookupAnswered', function(ref, detail, err)
-- detail = { lines = {...}, warnings = {...} }
-- or err = 'offline' | 'no answer' | reason
end)
Plate readers and ANPR
exports.sals_kewlcadbridge:IsPlateFlagged(plate) --> boolean, reason
exports.sals_kewlcadbridge:PlateRead({
plate = '...', coords = ..., camera = 'I-5 North',
})
Radio, tone-outs and tolls
exports.sals_kewlcadbridge:RadioActivity(channel, talking, source)
exports.sals_kewlcadbridge:RadioEmergency(source, channel)
-- stationId is the CAD's id, as it arrived on sals_kewlcad:toneOut.
-- `who` is optional: the player who hit the reset, or a name.
exports.sals_kewlcadbridge:AcknowledgeToneOut(stationId, respondingUnits, callNumber, who)
exports.sals_kewlcadbridge:TollPass({ plate = '...', plaza = '...', amount = 4.50 })
The live call board
For anything that shows the CAD inside the game: a station TV, a wall in the dispatch office. The platform pushes the board whenever it changes, only while something here wants it.
exports.sals_kewlcadbridge:ActiveCalls()
--> { { id, number, kind, typeCode, typeLabel, priority, status, title,
-- location, postal, coords, assigned = { 'E1', 'M7' }, primaryUnit,
-- agencies, createdAt, updatedAt }, ... } (times in Unix ms)
exports.sals_kewlcadbridge:Roster()
--> { { id, callsign, label, kind, deptKind, agency, state, status,
-- statusLabel, statusColor, onDuty, available, callId, callNumber,
-- panic, src }, ... }
exports.sals_kewlcadbridge:BoardAge() --> seconds since the board arrived
exports.sals_kewlcadbridge:BoardAt() --> when it was built, Unix ms
exports.sals_kewlcadbridge:WantBoard() --> keep it coming while my resource runs
exports.sals_kewlcadbridge:ReleaseBoard()
Or listen instead of polling:
AddEventHandler('sals_kewlcad:boardChanged', function(board) end) -- { at, calls, units }
AddEventHandler('sals_kewlcad:boardStale', function(ageSeconds) end) -- the link dropped
Sensitive calls are never on the board. Station Alert and Sal's Kewl Dispatch are
detected and need no call to WantBoard().
Characters, vehicles and licenses
The bridge syncs these itself when a character loads and when a job changes. These are for the moments it cannot see.
-- After a character creator finishes, or a name change.
exports.sals_kewlcadbridge:SyncPlayer(source)
-- A dealership sale or a transfer. The plate is the key; sending it again
-- with a new owner records the sale.
exports.sals_kewlcadbridge:SyncVehicle({
plate = 'ABC 123',
model = 'sultan', -- optional
color = 'Metallic Black', -- optional
ownerCharacterId = citizenid, -- or source = playerId
})
-- Or, as a server event:
TriggerEvent('sals_kewlcad:vehicleChanged', { plate = 'ABC 123', source = src })
exports.sals_kewlcadbridge:SyncAll()
-- Open the terminal or the citizen portal from your own script:
-- a tablet item, a phone app, a "my record" button.
exports.sals_kewlcadbridge:OpenMdt(source, '/mdt')
exports.sals_kewlcadbridge:OpenPortal(source)
Signs
-- The whole set, every time; the CAD's Signs panel draws it.
-- Sal's Kewl Signage does this itself.
exports.sals_kewlcadbridge:ReportSigns(signs, modes)
Migrating from another dispatch resource
exports.sals_kewlcadbridge:DumpToCad(package)
exports.sals_kewlcadbridge:UpdateDumpedCall(sourceCallId, update)
Push an existing call in and keep updating it by your own id, so a resource you are moving away from can run alongside the CAD during the changeover.
Health
exports.sals_kewlcadbridge:IsConnected() --> boolean
exports.sals_kewlcadbridge:QueueDepth() --> messages waiting
Events you can listen for
| Event | Fires when |
|---|---|
sals_kewlcad:callAssigned | A call is assigned. Client and server, the latter with the player it landed on. |
sals_kewlcad:lookupAnswered | A lookup you requested comes back. Exactly once. |
sals_kewlcad:boardChanged | The live board changed. |
sals_kewlcad:boardStale | The link dropped and the board is now old. |
sals_kewlcad:toneOut | A station is toned out. |
sals_kewlcad:licenseChanged | A CAD license decision arrived. Second argument says whether the bridge already applied it. |
sals_kewlcad:vehicleChanged | A vehicle changed hands. Server event you raise. |
sals_kewlcad:eventResult | The platform's answer to any event you sent. |
sals_kewlcad:dumpAccepted | A dumped 9-1-1 call was accepted, with the CAD number it was given. |
sals_kewlcad:linkChanged | The link to the platform went up or down. |
These event names are part of the contract. Renaming one breaks whatever a community built on it, so they are treated the same way as the wire protocol — see the next section.
The event contract
Everything that crosses between your server and the platform. You do not need this to run a community; you need it to write something that talks to one.
The envelope
Every message, in both directions, carries the same wrapper:
{
"v": 1,
"id": "a unique id for this message, 8-64 characters",
"type": "unit.status.v1",
"ts": 1790304000000,
"seq": 41,
"data": { }
}
seq is optional on the way up and is what makes gap recovery
possible: the bridge numbers its own messages, so the platform can ask for what it
missed.
Versioning
Every type carries its version in its name — call.create.v1, never
call.create. That is the entire versioning strategy, and it is
deliberate:
- Adding an optional field is not a new version. Old bridges keep working because the field is optional; new ones send it.
- Removing a field, renaming one, or changing what a value means is a new
version.
call.create.v2ships alongsidev1, both are accepted, andv1is removed only once no bridge in the wild still sends it. - An unknown type is refused, not ignored. A bridge sending something the platform does not recognize gets an error naming the type. Silently dropping it is how a community spends an evening wondering why nothing arrives.
The bridge reports its own version in bridge.hello.v1, so the platform
knows what a given server is capable of before it sends anything down.
Game to CAD — 24 types
| Type | What it reports |
|---|---|
bridge.hello.v1 | Resource version, framework, detected integrations, server name, player count, last sequence seen. |
bridge.heartbeat.v1 | Player count, on-duty count, queue depth, uptime, and whether anything wants the board. |
unit.location.v1 | Up to 300 unit positions in one message. |
unit.duty.v1 | Somebody went on or off duty, with the agency, job, grade, callsign and unit kind. |
unit.status.v1 | A status change, by code or by state. |
call.create.v1 | A new call: type, title, priority, location, narrative, source, caller, plate, agencies, whether to attach the sender, and a dedupe key. |
call.note.v1 | A note on a call. |
call.attach.v1 | A unit putting itself on a call. |
call.detach.v1 | A unit clearing from one. |
panic.v1 | Emergency, with coordinates, and whether it came from a radio. |
backup.request.v1 | Backup, with urgency 1 to 3. |
lookup.request.v1 | A lookup: plate, name, serial, VIN, phone or anything. |
alert.generic.v1 | A third-party alert. The one most integrations use. |
framework.sync.v1 | Characters, vehicles and licenses — up to 500, 1,000 and 1,000 respectively. |
payment.v1 | Something was paid: a citation, an invoice, an impound fee, bail. |
radio.activity.v1 | Somebody keyed up or stopped talking on a channel. |
radio.emergency.v1 | The emergency button on a radio. |
alerting.ack.v1 | A station acknowledged a tone-out. |
toll.pass.v1 | A plate through a gantry, paid or not. |
signs.inventory.v1 | Every sign on the server and the named modes available. |
alpr.read.v1 | A plate read, with camera, speed and limit. |
dispatch.dump.v1 | A worked 9-1-1 call: caller, location, narrative, answers, transcript, people and vehicles mentioned, call taker and times. |
dispatch.update.v1 | An update to a dumped call, by your own id. |
link.request.v1 | A player ran /cadlink. |
CAD to game — 18 types
The bridge treats these as instructions, never as truth to store. It shows a notification, sets a waypoint, or calls a framework export.
| Type | What it asks for |
|---|---|
config.push.v1 | The current status codes, call types, command names, job-to-agency map, intervals and feature flags. |
unit.assigned.v1 | You have a call: number, type, priority, location, narrative, and whether to set a waypoint. |
unit.unassigned.v1 | Cleared, and whether to drop the waypoint. |
unit.status.set.v1 | Your status is now this, and whether it was forced. |
bolo.broadcast.v1 | A BOLO, to the agencies named. |
alerting.toneout.v1 | Tone this station: station, call, type, location, units, tone, narrative. |
fine.issue.v1 | Charge this character. Survives an outage. |
sentence.start.v1 | Start the jail clock: jail seconds, fine, community service, facility. Survives an outage. |
license.suspend.v1 | Suspend, revoke or reinstate a license, with a reason and an until date. Survives an outage. |
impound.flag.v1 | This plate is impounded, or no longer is. |
plate.hotlist.v1 | The plates worth knowing about — replace, add or remove, up to 2,000. |
billboard.set.v1 | Put this on the signs, or a named mode, with an expiry. |
board.snapshot.v1 | The whole live board: up to 200 calls and 500 units. |
notify.v1 | Show somebody a message, at a level, optionally with a waypoint. |
lookup.result.v1 | The answer to a lookup: lines, warnings, and a path to open in the terminal. |
mdt.open.v1 | Open the terminal at this path with this session token, prefilled. |
link.code.v1 | Here is the player's six-character link code and how long it lasts. |
ack.v1 | Up to this sequence number was accepted; these were rejected, and why. |
Why the board is a snapshot
Three decisions, each the cheaper one:
- A snapshot, not a delta. The consumer compares it against the last one, so a lost message costs nothing and nobody ever has to reconcile a stream. Open calls are a small set and the whole board is a few kilobytes.
- Debounced per community. A dispatcher assigning three units to a structure fire is one push, not three.
- Only to servers that asked. The heartbeat carries a flag while something on that server wants it, so a server running nothing that reads the board never receives one.
Generated, not written
The full field-by-field reference for every type above is generated from the same definitions the bridge validates against and the gateway parses with. Hand-written event documentation goes stale in about two releases and nobody notices until somebody has already built an integration against the wrong shape.
Realtime rooms & gap replay
How a dispatcher's screen stays current without polling.
Rooms
A browser subscribes to rooms, and the permission on each is checked once, at subscribe time, rather than on every event. That is why events need no per-message filtering on the way out, which is most of why this is cheap.
| Room | Permission needed |
|---|---|
calls | call:list@tenant |
units | unit:list@tenant |
map | map:view@tenant |
bolos | bolo:list@tenant |
alerts | call:list@tenant |
call:<id> | call:view@tenant |
agency:<id> | unit:list@agency |
channel:<id> | message:view@tenant |
user:<id> | layout:view@self |
docket | hearing:list@tenant |
tow | tow_rotation:view@tenant |
hospital | hospital_board:view@tenant |
radio | radio:view@tenant |
stations | station_alerting:view@tenant |
signs | signs:view@tenant |
admin | tenant_settings:view@tenant |
Every room name begins with the community's own id, and a subscribe to a room whose prefix is not the connection's own community is refused before the permission is even considered.
One socket, however many windows
Five pop-out windows do not mean five connections. The first window to load elects itself leader, holds the socket, and rebroadcasts every frame to the others. If the leader closes, whoever notices first takes over.
The trade is that switching which window is leader costs a reconnect, which is a blink. It is done this way rather than with a shared worker because shared workers are unavailable in several browsers people actually use, and the election is twenty lines of code.
Gap replay
On reconnect — a laptop waking from sleep, a dropped wifi — a client sends the last sequence number it saw for each room and receives what it missed. Past 500 events it is told to resync and refetch from the API instead of being sent every frame, which is both cheaper and leaves it in a known state either way.
This is why a dispatcher who closes the lid on a laptop and opens it twenty minutes later sees a correct board rather than a stale one or a blank one.
Data, retention & export
Your data is yours
Export in the owner panel takes everything out: people, vehicles, calls, reports, citations, cases and the rest. It runs as a background job and you are told when it is ready, because a community with four years of history is not a request that finishes in a browser timeout.
Importing
Import brings in what you already have — a spreadsheet of people from your old system, a penal code, a vehicle list. This is the right first move on a community that has been running for years without a CAD, because an empty records system is a records system nobody uses.
The penal code specifically can be imported and exported on its own, which is the easy way to move one between a test community and a live one.
What is kept, and for how long
| Data | Default | Notes |
|---|---|---|
| Reports, citations, arrests, cases, warrants, people, vehicles | Forever | These are the records. Nothing trims them. |
| Closed calls | 30 days | Adjustable 1–365. Trimming a call never trims the paperwork written on it. |
| Messages | 30 days | Adjustable 1–365. |
| Unit positions | 7 days | Adjustable 0–90. Set 0 to keep none. |
| Audit log | 365 days | Adjustable, capped by your plan: 14 days on trial up to 3 years on Network. |
| Attachments | Forever | Against your plan's storage allowance. |
The audit log
Every write is recorded with who did it, what it was, when, and where it came from — the web, the in-game terminal, a street command, the API, the system itself, or platform staff.
That last field is more useful than it sounds. "Who deleted this call" is a different question from "was it deleted from a patrol car or from a browser", and the second one is usually the one that explains what happened.
Filter it by person, by action, by date. Command roles see their own agency's; owners see everything.
If you stop subscribing
A community moves to read-only rather than being deleted. Your dispatchers can still open it, every record is still there and still exportable, and nothing can be written. Export before you go, and ask us if you need help getting it out.
Security & isolation
This section exists because you are trusting somebody else's infrastructure with your community's records, and you are entitled to know how that is held up.
The one guarantee
A bug must not leak data between communities. Everything else in this platform is a feature; that is the product. Three mechanisms hold it up, and all three are load-bearing.
1. The database refuses
Every table holding community data carries a community id, and row-level security policies are applied to all of them by a loop rather than a list — so a new table cannot be forgotten by somebody writing a migration in a hurry. The application connects as a role that is explicitly not allowed to bypass those policies, and every query runs inside a transaction with the community id set.
There is no second way to reach the database. A route gets one handle, and that handle is already scoped.
2. Foreign keys carry the community
A policy stops one community reading another's rows. It does not stop one writing a row of its own that points at one, because an ordinary foreign key only checks that the id exists — not whose it is.
So every cross-table reference in the schema — all 197 of them — is a composite that includes the community id. A row that pointed somewhere it should not simply cannot be written.
3. The platform refuses to start if either is wrong
On startup the API checks that its database role cannot bypass the policies, that no rows are visible with no community set, and that every foreign key is community-scoped. If any of those is false it refuses to serve.
A failed deploy is a much better outcome than a silent leak, and this is the check that makes that the default.
Tested, including the negative cases
All three are covered by tests that run against a real PostgreSQL rather than a mock — because the two things most worth testing here are database features, and a mock of either would pass while the real thing was broken. Every endpoint has a cross-community test: a valid session for one community, pointed at another community's row ids, must get a refusal and nothing else.
Authentication
- Sessions are rows, with rotating refresh tokens and short-lived access tokens. Signing out one browser does not sign out the others.
- Two-factor by authenticator app, available to everyone, required for owners.
- The sign-in page says nothing about whether an email address exists, which is how an attacker would otherwise enumerate your members.
- In-game terminal tokens are separate and expire in hours.
The bridge connection
- Outbound only; your server opens no ports.
- An API key identifies the server; an HMAC signature proves the body was not altered; a timestamp and a nonce stop a captured request being replayed. None of the three is sufficient alone, and the signature covers all three together.
- A bridge key cannot read the public API and an API key cannot send game events.
- Rotating a key closes the connection immediately, which is the behavior you want.
What platform staff can see
Said plainly, because you should know:
- Support staff do not become members of your community. There is no staff account sitting in your roster.
- When staff need to see what you see, they impersonate, which mints a real session with real roles and writes both names on every row it touches. There is no invisible mode.
- Impersonation sessions appear in your own audit log, marked as such, with the staff member named.
- Platform staff hold no shortcut around the permission engine — they act through a real subject with real roles, precisely so that the audit trail shows what was actually used.
Your own responsibilities
Three, and they are the ones that actually get communities into trouble:
- Treat the API key and signing secret like a database password. Never in a public repository, never in Discord. Rotate immediately if either leaks.
- Turn on two-factor for owners and for anybody who can read criminal history or approve reports.
- Be deliberate about
@tenantgrants. Most mistakes in a role are a scope that is one step too wide.
Troubleshooting
The bridge will not start
"API key is still the placeholder" — paste the real values from
Game servers into config.lua. The bridge stops deliberately rather than
starting half-working.
For anything else, set Config.Debug.Enabled = true and restart. It
reports the framework it detected, whether it connected, and every rejected message
with the reason.
"invalid signature" in the console
Your signing secret does not match the one on the platform. Regenerate the pair under Settings → Game servers and paste both values again — the key and the secret are issued together and do not mix across generations.
The bridge connects but no units appear
Almost always this: players have to link their game account once with
/cadlink, using the code from their CAD profile. Until they do,
the platform does not know who they are. Have one person do it and watch them appear
on the board to confirm.
Commands say somebody is not allowed
Permissions are checked by the platform, never by the resource. Check the member's
role in the CAD, and check which agency they are working as —
@agency grants do not reach another agency's rows.
Config.EmergencyJobs only tidies the command list for civilians and is
not a permission control.
Lookups return nothing
Expected on a fresh community. The records are in the CAD, not in the game, and the CAD has not met anybody yet. Import what you have from Settings → Import, or let records accumulate.
Characters appear but their vehicles do not
Owned vehicles are read from your framework's own table through
oxmysql, mysql-async or ghmattimysql. If none
of those is running, or your fork uses different table or column names, set them in
Config.Sync.Vehicles.
After three failed reads in a row the bridge stops trying and prints one warning saying so — fix the config and restart the resource.
A player's name is wrong in the CAD
On a server that syncs from the framework, names, dates of birth and gender come from the game. Change them in the character creator or your framework's tools and they update on the next load. Everything else about a character is edited in the portal.
A license suspension did not reach the game
Check Config.Sync.ApplyLicenses is true. If the player
was offline, it lands the next time they load in — the platform re-sends any standing
suspension whenever the game copy disagrees, so it is not lost, only waiting.
Tone-outs go nowhere
The station's slug must match the station id in Sal's Kewl Station Alert. The Stations panel warns when a station has no slug. Also check the station's own alerting switch is on — a station with it off refuses a tone-out rather than swallowing it, and will tell you.
Signs are empty in the CAD
The sign resource reports its inventory up; the CAD does not discover signs on its own. Check the sign resource is running and that its CAD integration is configured on its side.
The station board in game is empty
Check Config.Board.Enabled. On 'auto' the board is sent
only while something on the server wants it — if your display resource is not in
Config.Board.AutoFor and does not call WantBoard(), nothing
arrives. Add it to the list or set Enabled = true.
The run card sent fewer units than it asked for
That is the shortfall report, and it is working. Nothing else of that kind is on duty and free. Mutual aid or a move-up is the next call — the platform will not invent apparatus.
A dispatcher's layout is wrong after an update
Reset layout on the toolbar. A layout saved by an older build that cannot be restored falls back to the default rather than leaving an empty workspace, but a layout that restores badly is best reset.
Nothing happens at all
Set Config.Debug.Enabled = true, restart, and read the console. It
reports the framework it detected, whether it connected, and every rejected message
with the reason. If the connection is the problem, check whether your host blocks
long-lived sockets and confirm Config.AllowHttpFallback is
true.
Still stuck
Have these ready and support can usually answer in one message: your community slug, the bridge version from the console banner, whether debug is on, and the exact line from the console.
Appendix: status codes & call types
Plain words — what a new community gets
| Code | Label | State | Timer | Quick |
|---|---|---|---|---|
OFF | Off duty | off_duty | — | — |
AVL | Available | available | — | yes |
ENR | En route | en_route | 10 min | yes |
ONS | On scene | on_scene | 30 min | yes |
BSY | Busy | busy | 20 min | yes |
TRN | Transporting | transporting | 20 min | — |
HOS | At hospital | at_hospital | 45 min | — |
OOS | Out of service | out_of_service | — | — |
PAN | EMERGENCY | panic | — | — |
Ten codes — the alternative set
| Code | Label | State | Timer |
|---|---|---|---|
10-8 | In service | available | — |
10-76 | En route | en_route | 10 min |
10-97 | On scene | on_scene | 30 min |
10-6 | Busy | busy | 20 min |
10-15 | Transporting | transporting | 20 min |
10-23 | At hospital | at_hospital | 45 min |
10-7 | Out of service | out_of_service | — |
10-42 | Off duty | off_duty | — |
10-33 | EMERGENCY | panic | — |
Fire and EMS add two more on top of whichever set you chose:
STG staging and AVL-S available on scene.
Priorities
| Priority | Means |
|---|---|
| P1 | Life at risk. Everything drops for this. |
| P2 | Urgent. Crime in progress or serious injury. |
| P3 | Prompt. Respond when a unit clears. |
| P4 | Routine. |
| P5 | When convenient. Paperwork and follow-ups. |
Law enforcement call types
| Code | Label | P | Also goes to |
|---|---|---|---|
TS | Traffic stop | 4 | — |
TC | Traffic collision | 3 | EMS, fire |
DUI | Impaired driver | 3 | — |
PURS | Pursuit | 1 | 3 patrol, 1 air |
SHOTS | Shots fired | 1 | 3 patrol |
ROB | Robbery in progress | 1 | 3 patrol |
BURG | Burglary | 2 | — |
ALARM | Alarm | 3 | — |
DIST | Disturbance | 3 | — |
DV | Domestic disturbance | 2 | EMS |
ASSLT | Assault | 2 | EMS |
GTA | Vehicle theft | 3 | — |
SUSP | Suspicious person or vehicle | 4 | — |
WELF | Welfare check | 4 | EMS |
WARR | Warrant service | 3 | — |
BACKUP | Officer requesting backup | 1 | 2 patrol |
PANIC | OFFICER EMERGENCY | 1 | 4 patrol, 1 supervisor, 1 medic; fire and EMS too |
ASSIST | Assist other agency | 3 | — |
FOLLOWUP | Investigation follow-up | 5 | Sensitive — off the shared board |
Fire call types
| Code | Label | P | Sends | Tones out |
|---|---|---|---|---|
STRUCT | Structure fire | 1 | 2 engine, 1 ladder, 1 rescue, 1 battalion, 1 medic | yes |
VEHFIRE | Vehicle fire | 2 | 1 engine | yes |
BRUSHFIRE | Brush fire | 2 | 2 brush, 1 engine, 1 tanker | yes |
ALARMFIRE | Fire alarm activation | 3 | 1 engine | yes |
EXTRIC | Extrication | 1 | 1 rescue, 1 engine, 1 medic | yes |
HAZMAT | Hazardous materials | 1 | 2 engine, 1 battalion | yes |
PSA | Public service assist | 5 | — | — |
INSP | Inspection | 5 | — | — |
EMS call types
| Code | Label | P | Sends |
|---|---|---|---|
MEDA | Medical — life threatening | 1 | 1 medic, 1 engine |
MEDB | Medical — urgent | 2 | 1 medic |
MEDC | Medical — routine | 4 | 1 medic |
CARDIAC | Cardiac arrest | 1 | 2 medic, 1 engine |
GSW | Gunshot wound | 1 | 1 medic, 2 patrol |
TRANSFER | Interfacility transfer | 5 | — |
Service call types
| Code | Label | P | Goes to |
|---|---|---|---|
TOW | Tow requested | 4 | Tow |
IMPOUND | Impound | 4 | Tow |
CLOSURE | Road closure | 4 | DOT |
DEBRIS | Debris in roadway | 4 | DOT, state police |
ANIMAL | Animal call | 5 | Game wardens, police |
MARINE | Marine assist | 2 | Marine, EMS |
OTHER | Other | 4 | Anyone |
Your community gets the call types matching the agencies you turned on, so a server with no fire department is not looking at a list of structure fire codes. All of them are editable, and you can add your own.
Appendix: fire & EMS coding
Only relevant with Detailed fire and EMS coding on.
What it adds
- NFIRS-style coding sections on the fire incident report: incident type, property use, area of origin, heat source and cause of ignition.
- The fuller vitals layout on patient care reports — repeating vitals rows with times rather than a single set.
The code picker
Type a number or a word. 111, cooking,
alarm — all three work, because a firefighter who has coded a thousand
building fires types 111 and one who has not types what they saw.
Why some codes carry a hint
The distinctions NFIRS makes are genuinely easy to get wrong, and the codes most often confused carry a one-line note in the picker:
| These two | Differ by |
|---|---|
111 building fire113 cooking fire confined to the container |
Whether it stayed in the pot. A contained cooking fire is not a building fire, and coding it as one makes your fire loss statistics meaningless. |
745 alarm system activation, no fire733 smoke detector malfunction |
Whether the detector was working correctly and had something to detect. |
Coding a run wrong is worse than not coding it, because a wrong code is indistinguishable from a right one once it is in the statistics. If a firefighter is unsure, the honest answer is to leave it for somebody who knows.
The three fields that are not the same thing
Repeating the distinction from Run cards & response plans, because this is where it bites:
- Call type — what was reported. "Structure fire."
- Response — what rolled. "Residential box, first alarm."
- Incident type — what it turned out to be. "113 — cooking fire, confined."
The gap between the first and the third is one of the more interesting numbers on the fire statistics page, and it only exists because they are three separate fields.
What the statistics are measured against
NFPA 1710, at the 90th percentile: 80 seconds turnout for fire suppression, 60 for EMS, four minutes travel for the first arriving engine. See Statistics for why the 90th percentile and not the average.
Support
Support, release news and coupon codes are on Discord, and by email at support@salskewlscripts.com.
Before you write in
Four things answer most questions in one message:
- your community slug — the first part of your CAD address;
- the bridge version from the console banner;
- whether
Config.Debug.Enabledwas on; - the exact line from the console, rather than a description of it.
The rest of the catalog
Sal's Kewl CAD is designed to work with the other Sal's Kewl resources, and each has its own manual at salskewlkorner.com/documentation:
| Resource | What it adds to the CAD |
|---|---|
| Sal's Kewl Dispatch | In-game radio, a fire/EMS pager, and 9-1-1 call-taking that dumps worked calls straight into the CAD. |
| Sal's Kewl Station Alert | The firehouse: lights, tones, and a live CAD call board on the wall. |
| Sal's Kewl Signage | Highway boards that show the CAD's amber alerts, BOLOs and closures. |
| Sal's Kewl Toll System | Toll gantries that report passes and violations to the CAD. |
Each is sold separately and none is required — the CAD works on its own, and every integration is detected at runtime rather than depended on.
Keeping up to date
The bridge ships with its own README and CHANGELOG, and the CHANGELOG calls out breaking configuration changes explicitly. This manual is restamped with every release; the version it documents is on the front and in the pill at the top of the web copy.
The live copy, always current, is at salskewlkorner.com/documentation/sals-kewl-cad.html.