Skip to content

Permissions reference ​

Every IPC call a plugin makes is checked against the capabilities listed in its manifest permissions array. Undeclared = hard reject — there is no implicit trust and no runtime workaround. Getting this array right is where most first-attempt plugins fail, so this page maps each SDK feature to the exact string it needs.

Two distinct concepts share the word "permission":

  1. Capabilities (this page) — manifest strings that gate the plugin's access to runtime services. Enforced by the runtime on every IPC call.
  2. User permissions — role/permission checks your plugin runs on its users via plugin.permissions (e.g. "can this user post?"). Registered with plugin.permissions.register(). These are application logic, not manifest declarations.

Grammar ​

resource.action[:scope]

Validated against:

js
/^[a-z][a-zA-Z0-9]*\.[a-zA-Z][a-zA-Z0-9_]*(?::[a-z0-9*][a-z0-9.*:-]*)?$/

resource and action are dotted identifiers; scope is optional and may contain dots, colons, and * wildcards. Scope conventions:

  • :self — the plugin's own resource (its own DB, its own files).
  • :<plugin>.<table> — a specific cross-plugin target.
  • :<namespace>.* — a prefix wildcard (events).
  • :<hostname> — a network target (fetch). Hostname only, no port.

Capabilities by feature ​

CapabilityUnlocks (SDK)Scope rules
data.sql:selfplugin.db — own SQLite:self only
data.kv:selfplugin.kv — own key/value store:self only
data.read:<plugin>.<table>plugin.data.read — read another plugin's published tableno wildcards — name an exact plugin.table
events.publish:<ns>.*plugin.events.publishprefix wildcard or exact topic; bare * rejected. By convention your own namespace — see the note below
events.subscribe:<pattern>plugin.events.subscribeprefix wildcard (x.*) or exact topic; bare * rejected
broadcast.clientsplugin.broadcast + scoped plugin.presenceno scope
notifications.pushplugin.notifications.send — durable notifications to up to 100 targeted usersno scope
notifications.push_allplugin.notifications.sendAll — durable notification to every memberno scope
storage.file:selfplugin.files + the /upload endpoint:self only
http.fetch:<host>plugin.fetch — outbound HTTP to that hostexact hostname, no port
runtime.scheduleplugin.schedule — recurring tasksno scope
voice.tokens:selfplugin.voice.createJoinToken:self
voice.rooms:selfplugin.voice.createRoomToken for plugin-owned LiveKit rooms; also declare managed_services: ["livekit"]:self
voice.moderation:selfplugin.voice.removeParticipant:self
proxy.http:selfreverse-proxy HTTP forwarding for a proxy_mount:self
proxy.websocket:selfreverse-proxy WebSocket forwarding (not implied by HTTP):self
resources.read:<plugin>cross-plugin plugin.resources.checkexact owner-plugin slug
net.public_portsplugin.net — Transport-backed public TCP/UDP portsno scope; requires active Transport
companions.request:<companion>brokered plugin.companions.requestexact companion id; experimental native companion lane

Capabilities that need no declaration: reading your own settings, the core user/category cache, presence connect/disconnect hooks, and registering your own user permissions. These are always available.

The companion broker and manifest declaration are experimental. The native supervisor provisions an approved companion only when the machine's operator has enabled the preview lane; otherwise installation reports that the workload did not start.

Two notes on scoping, because both cost people an afternoon:

  • http.fetch scopes on hostname only — never a port. plugin.fetch derives the capability from the URL's hostname, so a declared http.fetch:api.example.com:8080 matches nothing and every fetch is denied. Declare http.fetch:api.example.com and put the port in the URL.
  • events.publish:<ns>.* is not currently enforced against your own slug. The runtime reserves the runtime.* namespace, but nothing today stops a plugin from declaring and publishing to another plugin's namespace. Treat publishing only to your own namespace as the convention it is — other plugins' subscribers will not thank you — and don't rely on the platform to stop someone else doing it to you.

Accepted but inert ​

runtime.log and auth.currentUser are valid permission strings — they parse, they survive manifest validation, and they show up on the install consent screen — but no IPC action is gated by either one today, so declaring them grants nothing.

  • Logging needs no capability at all: the runtime captures your backend's plain stdout/stderr into the server log (lines prefixed IPC: are the transport, so keep those out of your own output).
  • Current-user data arrives as the user argument on every handler, and richer lookups go through plugin.core, which is also undeclared.

Leave both out of your manifest: the smallest honest permission set is the one reviewers and owners can actually read.

runtime_capabilities (separate array) ​

Voice features are opted into via the manifest's runtime_capabilities array, not permissions:

ValueGates
voice.mediaLiveKit-mediated audio (the voice-channels plugin).
voice.screen_sharethe plugin's ability to grant screen-share publish.
voice.moderationadmin "Stop their share" via LiveKit RemoveParticipant.

Per-user authorization (e.g. voice.screen_share.publish) is a separate user permission your plugin registers and checks — see above.

Wildcard rules at a glance ​

Patterndata.readevents.publishevents.subscribe
exact (x.y)✅✅✅
prefix (x.*)❌✅✅
bare *❌❌❌

Worked example ​

The text-channels plugin declares:

json
"permissions": [
  "data.sql:self",
  "events.publish:text-channels.*",
  "events.subscribe:runtime.cascade.*",
  "events.subscribe:runtime.presence.*",
  "events.subscribe:text-channels.*",
  "events.subscribe:core.category.*",
  "broadcast.clients",
  "notifications.push",
  "storage.file:self",
  "runtime.schedule"
]

Reading top to bottom: it owns a database, publishes its own events, listens for user-deletion cascades, presence, its own events, and category deletions, pushes real-time updates to clients, sends durable mention notifications, stores uploaded files, and runs a scheduled orphan-GC sweep. Every SDK call it makes traces back to one of these lines.

The capability checker and its test suite are the executable spec: runtime/src/capabilities/checker.ts.