Skip to content

Permissions

A role is a named set of players. Roles are configuration rather than code, and they drive the manifest, so a player who lacks a role never sees the command exists.

luau
Operator.Start({
	Roles = {
		owner = { UserIds = { 1234567 }, Inherits = { "moderator" } },
		moderator = { GroupId = 7654321, MinRank = 200 },
	},
})

That is the whole wiring. Start uses the same predicate for what a player is shown and what they are allowed to run, so the two can never disagree.

Defining a role

FieldMeaning
UserIdsa list of user ids that hold the role
GroupIda group to check membership in
MinRank / MaxRankthe rank window inside that group, defaulting to 1 and 255
Resolve(player) -> boolean, for anything else: a DataStore, an HTTP call, your own data
Inheritsother roles this one also grants

A role matches if any of its conditions match. Resolve may yield, and is wrapped in a timeout.

Inherits is the field that makes the example above behave the way you would expect: an owner is not in the moderator group, but holding owner grants moderator, so owners can run moderator commands without being listed twice. Inheritance is transitive, and a cycle is handled rather than hanging.

How permissions are decided

  • 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.

With no roles configured

Roles.new() with no config falls back to place owner only and logs a one-line warning saying so. The console works out of the box for you and nobody else. There is no hook you must write first, and no state where everything is silently open.

For a group-owned place the owner is whoever holds rank 255 in the owning group.

Custom providers

Anything that can answer "which roles does this player hold" can be plugged in, alongside or instead of the config:

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

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.

Caching, and changing roles mid-session

Roles are resolved once per player and cached, so there is never a group or DataStore call per command. Concurrent callers share a single lookup rather than each starting their own, and a player's entry is released when they leave.

When something changes what a player is allowed to 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.

The full method surface, covering querying, invalidating, and the predicate the dispatcher uses, is in api/roles.

Released under the MIT Licence.