Integrate, 3 minute read

Lua plugins on the shared message bus

Rift exposes the same broker to Rust plugins, Lua callbacks, and authenticated QUIC clients. The local broker exists without a network listener. Set messaging = {} to customize local limits; omit messaging or use false for default local limits and no UDP listener.

A Lua plugin receives messages through persistent subscriptions. Rift dispatches each delivery asynchronously into on_message, with a fresh sandboxed VM. Payloads are binary Lua strings. Replies use the message's optional reply subject.

return {
    listeners = { public = '0.0.0.0:25565' },
    backends = { lobby = '127.0.0.1:25566' },
    routes = { public = 'lobby' },
    messaging = {
        subscription_capacity = 1024,
        max_payload_bytes = 1024 * 1024,
        subscriptions = {
            { subject = 'plugins.echo', queue = 'echo-workers' },
            { subject = 'rift.events.>' },
        },
    },
    on_message = function(message)
        if message.subject == 'plugins.echo' and message.reply then
            rift.publish(message.reply, message.payload)
        end
    end,
    on_route = function(connection)
        rift.publish('plugins.connections', connection.peer_ip)
        return nil
    end,
    on_http = function(request)
        if request.path == '/ext/notify' then
            local sent = rift.publish('plugins.notifications', request.body)
            return { status = 202, body = tostring(sent.delivered) }
        end
    end,
}

rift.publish(subject, payload, optional_reply_subject) is available inside on_route, on_http, and on_message. It returns a table containing delivered and slow_consumers. Delivery counts describe enqueueing, not completed handler execution. Publishing never waits for receivers or disk. Subject tokens are separated by dots; subscriptions support * for one token and a terminal > for one or more remaining tokens. Queue subscribers divide matching deliveries; ordinary subscribers each receive a copy.

Callbacks share their existing instruction, memory, and 50 ms execution limits. A callback may publish at most 256 messages totaling 1 MiB. HTTP and connection hooks retain their usual bounded worker admission, while message handlers wait for a message worker slot with subsequent deliveries remaining in bounded subscription queues. An overflowing subscription discards pending messages and re-subscribes; at-most-once messaging does not replay those lost messages. Handler errors are logged and later messages continue. Use retained streams and explicit acknowledgements for recoverable work.

Lua globals and upvalues do not persist between deliveries. Store shared state in an application consumer or retained stream. rift is installed after evaluating the configuration and before invoking callbacks; refer to it inside callbacks. This ensures configuration validation cannot publish messages. Configuration reload updates on_message while retaining subscriptions and queued messages. Changing messaging settings requires a restart.

For the lowest latency, use the public Rust Broker directly from an async plugin. Lua VM creation and sandbox checks add work per callback. Neither API promises zero latency; benchmark the complete workload and payload sizes.

Authenticated QUIC configuration

Add these fields to messaging to accept external server plugins. Paths are relative to Rift's working directory. The private key and certificate must be PEM files. Each principal reads its token from an environment variable at startup. Certificates are verified by clients; configure the matching trusted CA.

messaging = {
    listen = '127.0.0.1:4222',
    certificate = 'certs/server.pem',
    private_key = 'certs/server-key.pem',
    principals = {
        {
            name = 'paper',
            token_env = 'RIFT_PAPER_TOKEN',
            publish = { 'plugins.>', 'events.>', 'rift.control.status' },
            subscribe = { 'plugins.>', 'rift.events.>', '_INBOX.>' },
            control = true,
        },
    },
    max_connections = 1024,
    max_streams_per_connection = 128,
    handshake_timeout_ms = 5000,
    io_timeout_ms = 30000,
    streams = {
        events = {
            subjects = { 'events.>' },
            storage_path = 'data/message-events',
            max_messages = 100000,
            max_bytes = 64 * 1024 * 1024,
            max_payload_bytes = 1024 * 1024,
            max_consumers = 128,
            sync_on_write = true,
        },
    },
}

Publication and subscription ACLs are explicit and default to empty. Control publication also requires control = true. Local Lua configuration is trusted and uses the local broker directly. Remote clients cannot impersonate Rift's rift.events publisher. Remote reply subjects must be authorized _INBOX.* subjects. Keep publish and subscribe grants as narrow as the plugin needs.

A stream with storage_path stores records and durable consumer state on disk; omitting the path selects bounded memory retention. Stream publication is an explicit API separate from ordinary rift.publish, keeping local pub/sub free of disk waits. The wire protocol documents the portable QUIC operations for pub/sub, requests, stream publication, replay, and acknowledgements.

Rust embedders inject a broker with Router::with_messaging and HttpScript::execute_with_messaging. MessageHandler::new subscribes synchronously, script_updates() returns a watch sender for hot reload, and run() drives bounded asynchronous workers. Dropping the run task closes its subscriptions; already-running Lua jobs finish within their sandbox budget.

Local Lua modules and folder-based plugins can register this callback with rift.on("message", function(message) ... end). Multiple message handlers run in registration order. See the Lua API and modular example.