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

# Permissions

> Grant permission nodes from Discord roles and server ACEs. A two-step label → role-id, label → node mapping that resolves with one Has() check.

`Bridge.Permissions` answers one question: **does this player hold this permission node?** A node is granted if a server **ACE** allows it, **or** if the player has a **Discord role** you mapped to that node. No config files, no per-script admin lists.

The thing to understand up front: you wire it with **labels**, not raw Discord role IDs scattered through your code. A label like `'admin'` is a friendly name. You define what role ID(s) the label means in one place, and what permission node(s) it grants in another. The real Discord role snowflake lives in exactly one call.

## The two-step flow

<Steps>
  <Step title="Map labels → Discord role IDs">
    `Bridge.Discord.RegisterRoles({ label = 'roleId' })` — tells the bridge that the label `admin` means Discord role `1234…`. A label can map to **several** role IDs (any of them grants it).
  </Step>

  <Step title="Map labels → permission nodes">
    `Bridge.Permissions.RegisterNodes({ label = { 'node', ... } })` — tells the bridge that the same label `admin` grants the nodes `myscript.admin`, `myscript.manage`, etc.
  </Step>

  <Step title="Check a node">
    `Bridge.Permissions.Has(src, 'myscript.admin')` — the bridge resolves the node back to the label(s) that grant it, then checks whether the player holds that label's Discord role.
  </Step>
</Steps>

<Warning>
  In `RegisterNodes`, the key (`admin`) is the **label**, not a role ID. The actual Discord role ID is defined in `RegisterRoles`. Mixing these up is the most common mistake — there is no role ID anywhere in your `RegisterNodes` call.
</Warning>

## Step 1 — register your roles

Map each label to its Discord role snowflake. You can pass a table, or a path to a `roles.json` shipped in **your** resource. A label may map to one ID or a list.

<CodeGroup>
  ```lua Table theme={null}
  -- server
  Bridge.Discord.RegisterRoles({
      admin = '111111111111111111',                              -- one role ID
      mod   = { '222222222222222222', '333333333333333333' },    -- several — any grants it
      vip   = '444444444444444444',
  })
  ```

  ```json roles.json theme={null}
  {
    "admin": "111111111111111111",
    "mod":   ["222222222222222222", "333333333333333333"],
    "vip":   "444444444444444444"
  }
  ```
</CodeGroup>

```lua theme={null}
Bridge.Discord.RegisterRoles('roles.json')   -- loads the file from your resource
```

<Tip>
  This is where — and the **only** place where — your real Discord role IDs live. To re-point a label to a different role later, an owner edits this one map; nothing else changes.
</Tip>

## Step 2 — map roles to nodes

Map the **same labels** to the permission node strings your script checks. A node string is yours to name (`myscript.admin`, `garage.impound`, …).

```lua theme={null}
-- server
Bridge.Permissions.RegisterNodes({
    admin = { 'myscript.admin', 'myscript.manage' },   -- the 'admin' LABEL → these nodes
    mod   = 'myscript.kick',
    vip   = { 'myscript.vip' },
})
```

A node value can be a single string or a list. Wildcards work: registering `myscript.*` for a label grants `myscript.admin`, `myscript.manage`, and any other `myscript.*` node; `*` grants everything.

## Step 3 — check a node

```lua theme={null}
if Bridge.Permissions.Has(src, 'myscript.admin') then
    -- granted via a server ACE, OR via the 'admin' Discord role
end
```

`Has` is the resolution that ties the two maps together:

1. If a server **ACE** allows the node (`IsPlayerAceAllowed`), grant it immediately. ACEs are honored **first**.
2. Otherwise, find every label whose nodes include that node (wildcards considered), and check whether the player holds any of those labels' Discord roles.
3. Otherwise, deny. Fail-closed.

| Method                                    | Purpose                                                      |
| ----------------------------------------- | ------------------------------------------------------------ |
| `Bridge.Discord.RegisterRoles(map\|path)` | Map labels → Discord role ID(s).                             |
| `Bridge.Permissions.RegisterNodes(map)`   | Map labels → permission node(s).                             |
| `Bridge.Permissions.Has(src, node)`       | Resolve a node to its label(s) and check ACE + Discord role. |

## Discord setup

The Discord side is what makes role-granted nodes work. The bridge talks to the Discord REST API directly with a server-only bot token — there is no separate bot process. It caches each player's guild profile (roles, avatar, username, …) on join.

Enable it with two **non-replicated** convars. Use `set`, **never** `setr`, so the token never reaches clients:

```cfg theme={null}
# server.cfg
set atlas:discord:token "YOUR_BOT_TOKEN"
set atlas:discord:guild "YOUR_GUILD_ID"
```

<Warning>
  Never use `setr` for the bot token. `setr` replicates the value to every client, leaking your token. `set` keeps it server-only — it is used only in an `Authorization` header server-side.
</Warning>

If you only use **ACE** permissions (no Discord roles), you can skip the Discord setup entirely — `Has` still works via `IsPlayerAceAllowed`.

## End-to-end example

A garage script that gates impound on staff, granting it to anyone with the `admin` or `mod` Discord role (or a matching ACE).

```lua theme={null}
-- server — once, at resource start
Bridge.Discord.RegisterRoles({
    admin = '111111111111111111',
    mod   = { '222222222222222222', '333333333333333333' },
})

Bridge.Permissions.RegisterNodes({
    admin = { 'garage.impound', 'garage.manage' },
    mod   = 'garage.impound',
})

-- gate a protected net handler on the node
Bridge.Net.On('garage:impound', {
    permission = 'garage.impound',          -- the net layer checks Has() before your handler runs
    payload    = Bridge.Validate.obj({ netId = Bridge.Validate.int() }),
    handle = function(src, data)
        impoundVehicle(data.netId)
    end,
})

-- or check inline anywhere
if Bridge.Permissions.Has(src, 'garage.manage') then
    openManagementMenu(src)
end
```

A player with the `admin` Discord role, the `mod` role, or a server ACE granting `garage.impound` passes. Everyone else is denied — the handler never runs.

<Note>
  `Bridge.Net.On` accepts a `permission` field that calls `Has` for you before the handler executes — the cleanest place to gate a client→server action. See [Networking](/atlas-bridge/developer/networking).
</Note>

## Two real gaps to know about

<Warning>
  **`Bridge.IsAdmin` is not this system.** `Bridge.IsAdmin(src)` is a **framework** admin check (ESX/QB group), entirely separate from these Discord-role nodes. Use `Bridge.Permissions.Has` for node-based gating; use `Bridge.IsAdmin` only when you specifically want the framework's own admin flag.
</Warning>

<Warning>
  **There is no built-in default admin.** The bridge ships with no roles and no nodes registered. Until **you** call `RegisterRoles` and `RegisterNodes` (or grant ACEs in `server.cfg`), `Has` denies everything. Register your own roles and nodes during setup.
</Warning>

## Client side

Clients can read only their **own** Discord profile (for NUI avatars/banners/tags) — never role checks, which are server-authoritative.

```lua theme={null}
-- client
Bridge.Discord.GetAvatar()
Bridge.Discord.GetUsername()
Bridge.Discord.GetDisplayName()
Bridge.Discord.GetGuildTag()
Bridge.Discord.GetBanner()
```
