Skip to main content
This guide walks you through enabling RBAC on your iii instance using the WorkerModule. Workers connect via WebSocket to a dedicated RBAC port, separate from the internal engine bridge.

1. Add RBAC Config to the WorkerModule

Add a modules::worker::WorkerModule entry with RBAC config to your engine config file. At minimum you need a port and an expose_functions list inside rbac:
iii-config.yaml
modules:
  # ... your existing modules ...

  - class: modules::worker::WorkerModule
    config:
      port: 49135
      rbac:
        expose_functions:
          - match("api::*")
This exposes all functions whose ID starts with api:: on port 49135 with no authentication.

2. Write an Auth Function

For production, authenticate workers by registering a function that validates credentials from the WebSocket upgrade request. The function receives an AuthInput and must return an AuthResult.
import type { AuthInput, AuthResult } from 'iii-sdk'
import { registerWorker } from 'iii-sdk'

const iii = registerWorker('ws://localhost:49134')

iii.registerFunction(
  { id: 'my-project::auth-function' },
  async (input: AuthInput): Promise<AuthResult> => {
    const token = input.headers?.['authorization']?.replace(/^Bearer\s+/i, '')
    const apiKey = input.query_params?.['api_key']?.[0]

    if (!token && !apiKey) {
      throw new Error('Missing credentials')
    }

    const user = await validateCredentials(token || apiKey)

    return {
      allowed_functions: [],
      forbidden_functions: user.role === 'readonly'
        ? ['api::users::delete', 'api::users::update']
        : [],
      allowed_trigger_types: user.role === 'admin'
        ? ['cron', 'webhook']
        : undefined,
      allow_trigger_type_registration: user.role === 'admin',
      context: {
        user_id: user.id,
        role: user.role,
      },
    }
  },
)

Function Registration Prefix

The auth function can optionally return a function_registration_prefix string. When present, the engine automatically prefixes every function ID registered by this worker with {prefix}::. Trigger registrations also auto-prefix the function_id they reference. When the engine dispatches a function invocation back to the worker, the prefix is stripped so the worker SDK finds the correct local handler. This provides transparent namespace isolation per session without the worker needing to manage prefixes. Update your config to reference the auth function:
iii-config.yaml
modules:
  - class: modules::worker::WorkerModule
    config:
      port: 49135
      rbac:
        auth_function_id: my-project::auth-function
        expose_functions:
          - match("api::*")

3. Write a Middleware Function (Optional)

A middleware sits between the worker and the target function. Use it for validation, rate limiting, audit logging, or enriching the payload with auth context. The function receives a MiddlewareFunctionInput.
import type { MiddlewareFunctionInput } from 'iii-sdk'

iii.registerFunction(
  { id: 'my-project::middleware-function' },
  async (input: MiddlewareFunctionInput) => {
    console.log(`[audit] user=${input.context.user_id} invoking ${input.function_id}`)

    const enrichedPayload = {
      ...input.payload,
      _caller_id: input.context.user_id,
      _caller_role: input.context.role,
    }

    return iii.trigger({
      function_id: input.function_id,
      payload: enrichedPayload,
    })
  },
)
Add it to your config. Note that middleware_function_id sits at the config level, not inside rbac:
iii-config.yaml
modules:
  - class: modules::worker::WorkerModule
    config:
      port: 49135
      middleware_function_id: my-project::middleware-function
      rbac:
        auth_function_id: my-project::auth-function
        expose_functions:
          - match("api::*")

4. Connect a Worker

The RBAC port speaks the standard iii engine protocol. Connect using the SDK:
import { registerWorker } from 'iii-sdk'

const worker = registerWorker('ws://localhost:49135', {
  headers: { authorization: 'Bearer my-token' },
})

const result = await worker.trigger({
  function_id: 'api::users::list',
  payload: { limit: 10 },
})

console.log(result)

5. Use Channels Through the RBAC Port

Channels work on the RBAC port exactly as they do on the main engine port. The SDK’s createChannel() works without changes:
const worker = registerWorker('ws://localhost:49135', {
  headers: { authorization: 'Bearer my-token' },
})

const channel = await worker.createChannel()

const result = await worker.trigger({
  function_id: 'api::files::process',
  payload: { reader: channel.readerRef },
})

channel.writer.stream.write(Buffer.from('hello'))
channel.writer.stream.end()
The RBAC port mounts the channel WebSocket endpoint at /ws/channels/{channel_id} on the same port, so channel data flows through the secure port without exposing the main engine’s channel endpoint.

6. RBAC for Trigger Registration

Workers connecting through the RBAC port can register trigger types and triggers, subject to access control.

Auth Result Fields

The auth function controls trigger access via two fields in AuthResult:
  • allowed_trigger_types — List of trigger type IDs this worker can register triggers for. When omitted, all types are allowed.
  • allow_trigger_type_registration — Whether this worker can register new trigger types.

Registration Hook Functions

For fine-grained control, configure hook functions that are called before each registration:
iii-config.yaml
modules:
  - class: modules::worker::WorkerModule
    config:
      port: 49135
      middleware_function_id: my-project::middleware-function
      rbac:
        auth_function_id: my-project::auth-function
        on_trigger_registration_function_id: my-project::on-trigger-reg
        on_trigger_type_registration_function_id: my-project::on-trigger-type-reg
        on_function_registration_function_id: my-project::on-function-reg
        expose_functions:
          - match("api::*")
Each hook receives the registration details and the auth context. Return a result object with the (possibly mapped) fields to allow the registration, or throw an error to deny it. Any fields omitted from the result keep their original values.
import type {
  OnTriggerRegistrationInput,
  OnTriggerRegistrationResult,
  OnTriggerTypeRegistrationInput,
  OnTriggerTypeRegistrationResult,
  OnFunctionRegistrationInput,
  OnFunctionRegistrationResult,
} from 'iii-sdk'

iii.registerFunction(
  { id: 'my-project::on-trigger-reg' },
  async (input: OnTriggerRegistrationInput): Promise<OnTriggerRegistrationResult> => {
    const role = input.context.role as string
    if (!input.function_id.startsWith(`${role}::`)) {
      throw new Error('Function ID must be prefixed with the role')
    }
    return { function_id: `${role}::${input.function_id}` }
  },
)

iii.registerFunction(
  { id: 'my-project::on-trigger-type-reg' },
  async (input: OnTriggerTypeRegistrationInput): Promise<OnTriggerTypeRegistrationResult> => {
    if (input.context.role !== 'admin') {
      throw new Error('Only admins can register trigger types')
    }
    return {}
  },
)

iii.registerFunction(
  { id: 'my-project::on-function-reg' },
  async (input: OnFunctionRegistrationInput): Promise<OnFunctionRegistrationResult> => {
    if (input.function_id.startsWith('internal::')) {
      throw new Error('Cannot register internal functions')
    }
    return { function_id: input.function_id }
  },
)

Expose Functions by Metadata

If your functions register metadata, you can use metadata filters instead of (or in addition to) wildcard patterns:
iii.registerFunction({
  id: 'api::users::list',
  metadata: { public: true, tier: 'free' },
}, async (input) => {
  // ...
})
Then filter by metadata in your config:
iii-config.yaml
rbac:
  expose_functions:
    - metadata:
        public: true
    - metadata:
        tier: "free"

Types Reference

All three SDKs export the following types for RBAC functions.

AuthInput

Input passed to the auth function during the WebSocket upgrade.
FieldTypeDescription
headersRecord<string, string>HTTP headers from the upgrade request.
query_paramsRecord<string, string[]>Query parameters. Each key maps to an array of values to support repeated keys.
ip_addressstringIP address of the connecting client.

AuthResult

Return value from the auth function. Controls access and passes context to middleware.
FieldTypeDefaultDescription
allowed_functionsstring[][]Additional function IDs to allow beyond expose_functions.
forbidden_functionsstring[][]Function IDs to deny even if they match expose_functions. Takes precedence.
allowed_trigger_typesstring[] or omittedomitted (permissive)Trigger type IDs the worker may register triggers for. When omitted, all types are allowed.
allow_trigger_type_registrationbooleanfalseWhether the worker may register new trigger types.
allow_function_registrationbooleantrueWhether the worker may register new functions.
function_registration_prefixstring or omittedomittedWhen set, all function IDs registered by this worker are internally prefixed with {prefix}::. Triggers auto-prefix their function_id reference. The prefix is stripped when invoking the worker, so the worker SDK never sees it.
contextRecord<string, unknown>{}Arbitrary context forwarded to the middleware function on every invocation.

MiddlewareFunctionInput

Input passed to the middleware function on every invocation through the RBAC port.
FieldTypeDescription
function_idstringID of the function being invoked.
payloadRecord<string, unknown>Payload sent by the caller.
actionTriggerAction or omittedRouting action (enqueue, void), if any.
contextRecord<string, unknown>Auth context from AuthResult.context for this session.

OnTriggerTypeRegistrationInput

Input passed to the on_trigger_type_registration_function_id hook. Return an OnTriggerTypeRegistrationResult or throw to deny.
FieldTypeDescription
trigger_type_idstringID of the trigger type being registered.
descriptionstringHuman-readable description of the trigger type.
contextRecord<string, unknown>Auth context from AuthResult.context.

OnTriggerTypeRegistrationResult

Result returned from the on_trigger_type_registration_function_id hook. Omitted fields keep the original value.
FieldTypeDescription
trigger_type_idstring or omittedMapped trigger type ID.
descriptionstring or omittedMapped description.

OnTriggerRegistrationInput

Input passed to the on_trigger_registration_function_id hook. Return an OnTriggerRegistrationResult or throw to deny.
FieldTypeDescription
trigger_idstringID of the trigger being registered.
trigger_typestringTrigger type identifier.
function_idstringID of the function this trigger is bound to.
configunknownTrigger-specific configuration.
contextRecord<string, unknown>Auth context from AuthResult.context.

OnTriggerRegistrationResult

Result returned from the on_trigger_registration_function_id hook. Omitted fields keep the original value.
FieldTypeDescription
trigger_idstring or omittedMapped trigger ID.
trigger_typestring or omittedMapped trigger type.
function_idstring or omittedMapped function ID.
configunknown or omittedMapped trigger configuration.

OnFunctionRegistrationInput

Input passed to the on_function_registration_function_id hook. Return an OnFunctionRegistrationResult or throw to deny.
FieldTypeDescription
function_idstringID of the function being registered.
descriptionstring or omittedHuman-readable description of the function.
metadataRecord<string, unknown> or omittedArbitrary metadata attached to the function.
contextRecord<string, unknown>Auth context from AuthResult.context.

OnFunctionRegistrationResult

Result returned from the on_function_registration_function_id hook. Omitted fields keep the original value.
FieldTypeDescription
function_idstring or omittedMapped function ID.
descriptionstring or omittedMapped description.
metadataRecord<string, unknown> or omittedMapped metadata.

Full Example Config

iii-config.yaml
modules:
  - class: modules::worker::WorkerModule
    config:
      port: 49134

  - class: modules::stream::StreamModule
    config:
      port: 3112

  - class: modules::worker::WorkerModule
    config:
      host: 0.0.0.0
      port: 49135
      middleware_function_id: my-project::middleware-function
      rbac:
        auth_function_id: my-project::auth-function
        on_trigger_registration_function_id: my-project::on-trigger-reg
        on_trigger_type_registration_function_id: my-project::on-trigger-type-reg
        on_function_registration_function_id: my-project::on-function-reg
        expose_functions:
          - match("api::*")
          - match("*::public")
          - metadata:
              public: true
Only expose the RBAC port (49135) to external networks. The main engine port (49134) and stream port (3112) should remain internal. Use firewall rules or network policies to enforce this.