Skip to content

Roles

luau
local Operator = require(game:GetService("ServerStorage").Operator)
local Roles = Operator.Roles

A declarative layer above guards. A guard is a function you write; a role is configuration, and it drives the manifest, so a player who lacks a role never sees the command exists.

Functions

Roles.new

luau
Roles.new(config: Config?, options: Options?): Roles
luau
local roles = Roles.new({
	owner = { UserIds = { 1234567 }, Inherits = { "moderator" } },
	moderator = { GroupId = 7654321, MinRank = 200 },
})

With no config this falls back to place owner only and logs a one-line warning saying so. For a group-owned place the owner is whoever holds rank 255 in the owning group.

Methods

:getRoles

luau
roles:getRoles(player: Player): RoleSet

The player's role set, as { [roleName]: true }. Resolves on first call and is cached after; concurrent callers share one lookup rather than each starting their own, and a player's entry is released when they leave.

:has

luau
roles:has(player: Player, role: string): boolean

:canRun

luau
roles:canRun(player: Player, command: Command): boolean

The predicate for both the dispatcher and the transport, so there is a single answer to "may this player use this command".

  • A player holding no role at all gets nothing: no manifest, no bundle, no console.
  • A command with no :permission(...) runs for anyone holding at least one role.
  • A command with permissions runs for a player holding any one of them, directly or inherited.

:invalidate

luau
roles:invalidate(player: Player): ()

Drops one player's cached roles, so the next lookup re-resolves.

:invalidateAll

luau
roles:invalidateAll(): ()

:getRoleNames

luau
roles:getRoleNames(): { string }

:destroy

luau
roles:destroy(): ()

Releases the cache and its connections.

Types

RoleDefinition

luau
type RoleDefinition = {
	UserIds: { number }?,
	GroupId: number?,
	MinRank: number?,
	MaxRank: number?,
	Resolve: ((player: Player) -> boolean)?,
	Inherits: { string }?,
}
FieldDefaultMeaning
UserIdsnoneuser ids that hold the role
GroupIdnonea group to check membership in
MinRank1lowest rank in that group that counts
MaxRank255highest rank in that group that counts
Resolvenoneanything else: a DataStore, an HTTP call, your own data
Inheritsnoneother roles this one also grants

A role matches if any of its conditions match. Resolve may yield and is wrapped in a timeout. Inheritance is transitive, and a cycle is handled rather than hanging.

Config

luau
type Config = { [string]: RoleDefinition }

Options

luau
type Options = {
	logger: Log.Logger?,
	providers: { RoleProvider }?,
	isPlaceOwner: ((player: Player) -> boolean)?,
	resolveTimeout: number?,
}

RoleProvider

luau
type RoleProvider = {
	resolve: (player: Player) -> { string },
}

A provider returns a list of role names and may yield. Roles from providers are unioned with roles from the config, then inheritance is expanded over the result.

A provider that raises or times out is logged and skipped, so it can never accidentally grant a role.

luau
Roles.new(config, {
	providers = {
		{ resolve = function(player) return MyDataStore.getRoles(player.UserId) end },
	},
})

RoleSet

luau
type RoleSet = { [string]: boolean }

Changing roles mid-session

Roles are resolved once per player and cached, so there is never a group or DataStore call per command. When something changes what a player may do:

luau
transport:refresh(player)   -- or transport:refreshAll()

Passing roles to Transport.new makes refresh invalidate that player's cached roles first, so one call re-resolves, rebuilds the manifest and sends it. If they lost access entirely the bundle is removed; if they lost only some commands, the bundle stays and the manifest shrinks in place.

Released under the MIT Licence.