Permission System
Almost every gated feature in Carplay V2 (apps, modules, autopilot, drift mode, autopark, security cam, dashcam, screen install) accepts the same pair of arrays:
{
jobs = {}, -- e.g. {"police", "ambulance"}
discordRoles = {}, -- e.g. {"123456789012345678"}
}
This page explains exactly how that gating works.
Logic
jobs | discordRoles | Result |
|---|---|---|
| Empty | Empty | Everyone allowed (no restrictions) |
| Set | Empty | Player must have a matching job |
| Empty | Set | Player must have a matching Discord role |
| Set | Set | Job match OR Discord role match — either suffices |
The OR logic in the both-set case is intentional: players with the right job get access regardless of Discord, and Discord-only members (donors, VIPs) get access regardless of in-game job.
Job Matching
Job is read from the player's framework data:
| Framework | Source |
|---|---|
| ESX | xPlayer.getJob().name |
| QBCore | Player.PlayerData.job.name |
| QBox | Player.PlayerData.job.name |
| Standalone | nil — only discordRoles works on standalone |
Match is case-sensitive exact — "police" matches only "police", not "Police" or "PoliceOfficer".
Discord Role Matching
Discord roles are fetched via a built-in bot fetcher. You don't need any external Discord script — Carplay V2 talks directly to the Discord API.
Setup
In server/config.lua:
ServerConfig.DiscordBotToken = "YOUR_BOT_TOKEN"
ServerConfig.DiscordGuildId = "YOUR_GUILD_ID"
ServerConfig.DiscordCacheDuration = 120 -- seconds
If either is empty → all discordRoles checks are silently skipped.
How It Works
- Player joins or first-uses a permission-gated feature
- The script extracts their Discord ID from FiveM identifiers (
discord:123...) - It calls
https://discord.com/api/v10/guilds/{GUILD}/members/{DISCORD_ID}with the bot token - The returned roles array is cached for
DiscordCacheDurationseconds - On any future check within that window, the cache is used (no API call)
- When the player disconnects, their cache entry is cleared
Bot Requirements
- Bot must be in your Discord server
- Bot must have Server Members Intent enabled (Discord Developer Portal → Bot tab → Privileged Gateway Intents)
- Bot needs at minimum View Server Members permission
Getting a Role ID
- Enable Discord Developer Mode: User Settings → App Settings → Advanced → Developer Mode
- Server Settings → Roles → right-click any role → Copy Role ID
Cache Tuning
The default cache is 120 seconds (2 min). For very busy servers you might want to bump this to 300-600s to reduce API calls; for servers where roles change frequently, lower to 30s for faster propagation.
ServerConfig.DiscordCacheDuration = 300 -- 5 min
Set 0 to disable caching entirely (one API call per check — not recommended).
Where Permissions Apply
| Feature | Config path | Notes |
|---|---|---|
| Each app | Config.Apps[i].jobs / discordRoles | Per-app gating |
| Autopilot | Config.AutoPilotPermissions | |
| Drift Mode | Config.DriftMode | |
| Auto Park | Config.AutoPark | |
| Security Camera | Config.SecurityCamera | |
| DashCam | Config.DashCam | |
| In-Vehicle Screen | Config.Screen | Restricts who can install screens |
Modules without permission arrays (e.g. Config.Modules.MusicPlayer) are simply on/off toggles open to everyone.
Server-Side Helper
If you want to call the permission check yourself in custom code, the helper is exported globally on the server:
-- in any server-side script that runs after code9_carplayv2 starts
local jobs = {"police"}
local roles = {"123456789012345678"}
local playerJob = GetPlayerJobServer(source) -- helper from open.lua
local playerDiscordRoles = GetPlayerDiscordRoles(source) -- cached fetch
local allowed = CheckPermission(jobs, roles, playerJob, playerDiscordRoles)
Example Configurations
"Police-only autopilot"
Config.AutoPilotPermissions = {
jobs = {"police"},
discordRoles = {},
}
"Donors get drift mode"
Config.DriftMode = {
enabled = true,
jobs = {},
discordRoles = {"123456789012345678"}, -- "Donor" role ID
}
"Mechanics OR donors can install screens"
Config.Screen.jobs = {"mechanic"}
Config.Screen.discordRoles = {"123456789012345678"} -- "Donor" role ID
"Hide Notes app from non-staff"
-- in Config.Apps, find the notes entry:
{id = "notes", icon = "./apps/note.png", enabled = true, jobs = {"admin", "moderator"}, discordRoles = {}}
