How Integrations Work#
The panel itself never talks to your framework, inventory or weather script directly. Each one is reached through a small, open bridge file, and the right bridge is picked at start by looking at which resources are running.
Layer | Supported | Without it |
|---|---|---|
Framework |
| Standalone: moderation, bans, reports, logs and server tools work; money, jobs, items, garages and group-based access do not. Grant access with |
Inventory |
| Give Item has no item list and Open Inventory stands down. |
Vehicle keys |
| Spawned and given vehicles are unlocked but handed over without a key. |
Weather |
| The menu sets weather and time itself and keeps every player in sync. |
Medical |
| Revive and heal use the game's own natives; hunger and thirst go through the framework. |
Fuel |
| Spawned cars are filled with the game's own fuel level. |
Voice | Any Mumble-based voice, such as | Live View voice listening is unavailable. |
Screenshots |
| Discord logs are sent without a screenshot. |
Database |
| Required. The only one, with |
Every optional integration falls back quietly. A missing resource makes its feature stand down, not throw. The console line
[qs-adminmenu] Framework loaded:tells you which framework bridge was picked.
Overriding Detection#
Detection only sees resources that are already started, so anything you want found must sit above ensure qs-adminmenu. When two candidates for the same layer run at once — two inventories, say — or a resource has been renamed on your server, set the value by hand in config/main.lua:
-- Auto, the default
Config.Inventory = DependencyCheck(inventories) or 'standalone'
-- Manual: skip detection entirely
Config.Inventory = 'ox' -- qs · ox · qb · tgiann · codem · standalone
Config.Framework = 'qb' -- esx · qb (QBCore and Qbox) · none
Config.KeySystem = 'qs' -- qs · qb · cd_garage
Config.MedicalSystem = 'qs' -- qs · tk · standaloneWeather#
The weather layer is the one exception: it is re-checked every time a weather resource starts or stops, so its place in server.cfg does not matter. Config.WeatherResources lists the candidates in priority order — qs-zoneweather first on purpose, because it also answers to the name qb-weathersync. To force one, set Config.ForceWeatherSystem to 'zoneweather', 'qb', 'codem' or 'standalone'.
Garages#
Given vehicles and the Player Garage read and write your framework's own owned-vehicle table. A car given from the panel is stored in the garage named in Config.DefaultGarage — central on ESX, pillboxgarage on QBCore. Change it to a garage that exists on your server.
Your Duty System#
With Config.RequireDuty.enabled on, the menu asks one function whether a staff member is on duty before opening the panel, noclip or Fast Actions. Wire it to your own script in custom/server.lua:
-- custom/server.lua
---@param src number
---@param feature 'menu' | 'noclip' | 'fastAction'
function IsAdminOnDuty(src, feature)
return exports['your-duty-script']:IsOnDuty(src) == true
endIf the function errors — its resource stopped, say — the feature is blocked, never silently opened. Turning noclip off is always allowed.
Discord Logs#
Every logged event can also be posted to Discord as an embed, with the player, their identifiers and the details. The URLs live in server/webhooks.lua, grouped by category, with one URL per event or a _default for the whole category:
-- server/webhooks.lua
_G['webhook'] = {
player_actions = {
_default = 'https://discord.com/api/webhooks/...', -- every player action
ban = 'https://discord.com/api/webhooks/...', -- bans get their own channel
},
admin_self = { _default = '' },
resources = { _default = '' },
console = { _default = '' },
reports = { created = '', updated = '', closed = '' },
_global = '', -- anything with no URL of its own
}A log goes to the most specific URL it finds: its own event, then its category's _default, then _global. Leave all three empty and that event is kept in the database but not posted.
Category | What goes there |
|---|---|
| Warn, kick, ban, unban, blocked connections, freeze, goto, bring, spectate, Live View, items, money, jobs, revive, heal. |
| Staff powers, revive all, kick all, spawned vehicles, announcements, world clean-up, teleports. |
| Given cars, garage spawns and deletions, catalogue edits. |
| Resource actions, console commands, file saves and reloads. |
| Queue settings, whitelist and tiers; weather and time; density and suppression. |
| Joins, leaves, chat, deaths; explosions, suspicious weapon damage, server errors. |
| Reports created, updated and closed. Add the |
Config.Logs.Events in config/main.lua switches each event's Discord post (webhook), its place in the Logs page (ui) and its embed colour. Config.Logs.Webhook sets the bot's name and avatar and whether the player's IP is shown (showIp, off by default). Weapon damage above WeaponDamageSuspiciousThreshold (200) is flagged as suspicious.
Screenshots in logs#
With screenshot-basic running and a Fivemanage token in _G['fivemanage'].webhook at the bottom of server/webhooks.lua, logs about a player carry a screenshot of their screen.
Screen Watch, Live View and TURN#
Screen Watch and Live View stream video directly from the player's game to the admin's (WebRTC). The server only passes the two of them each other's address. Google's public STUN server is configured out of the box, which is enough on most home connections. Players behind strict NAT, CGNAT or some mobile connections need a TURN relay, or their stream stays on Connecting.
The recommended route: credentials are fetched on start and refreshed before they expire.
Open Cloudflare → Realtime, go to the TURN tab and Create TURN App — the TURN product, not SFU.
Copy the TURN Token ID and the API Token (shown only once).
Fill them in and switch it on:
-- config/main.lua
Config.ScreenWebRTC.cloudflare = {
enabled = true,
turnKeyId = 'your TURN Token ID',
apiToken = 'your API Token',
ttlSeconds = 86400,
refreshSkewSeconds = 600,
retryOnFailMs = 30000,
}Run adminmenu:turn:status in the server console to see the cached servers and when they expire.
Any TURN provider (Metered, coturn on your own machine…). Keep UDP, TCP and TLS endpoints for the best chance of getting through:
-- config/main.lua
Config.ScreenWebRTC.iceServers = {
{ urls = 'stun:stun.l.google.com:19302' },
{
urls = {
'turn:turn.example.com:3478?transport=udp',
'turn:turn.example.com:3478?transport=tcp',
'turns:turn.example.com:5349?transport=tcp',
},
username = 'user',
credential = 'pass',
},
}Image quality is set in two places: Config.ScreenWebRTC.video for the single Screen Watch window (1600×900, 30 fps by default) and Config.LiveView.gridVideo for the smaller Live View tiles (640×360, 15 fps). Live View lets one admin watch up to 9 players at once, and one player be watched by up to 3 admins.
Voice listening#
Listen to voice raises the player's voice to full volume on the admin's client and mutes the admin towards that player, through FiveM's Mumble natives — which is what pma-voice and most voice resources use. Switch it off with Config.LiveView.Voice.enabled = false. For a voice resource with its own API, edit the two open files server/custom/liveview_voice.lua and client/custom/liveview_voice.lua.
Adding a Backend#
Supporting a resource that is not on the list takes one file and one line. Every bridge is small, unencrypted and guarded so it only runs when it is the chosen one.
Copy the closest bridge
Frameworks live in
custom/<name>/; inventories, weather, medical and garages inserver/custom/<kind>/andclient/custom/<kind>/. Copy the one nearest to yours —server/custom/inventory/ox.lua, say — and rename it.Change the calls inside
Keep the guard on the first line and swap the exports for your resource's own:
lua if Config.Inventory ~= 'myinv' then return end function sv_inventory:formatItemList() ... end function sv_inventory.openInventory(adminSource, targetId) ... endRegister it for detection
Add your resource to the matching table in
config/main.lua—['my-inventory'] = 'myinv'ininventories— or setConfig.Inventory = 'myinv'directly.
If that is more than you want to take on, open a ticket with the name of the resource.
Languages#
41 translations ship in locales/: ar bg bn ca cs da de el en es et fa fi fr he hi hr hu id it ja ko lt lv ms nl no pl pt ro ru sk sl sv sw th tr uk ur vi zh. Set Config.Locale to one of those codes; it covers the panel, the Report Center, the notices players see and the ban screen. Config.Intl.locales sets how numbers, dates and durations are formatted — 'es-ES', 'de-DE' and so on.
To add another language, copy en.json to the new code, translate it and set the locale to that code. Anything left untranslated falls back to English.



