Plugins

Plugins run within a client's browser in the context of the supervisor. They are intended to facilitate interactions between user interfaces and services.

sequenceDiagram 

box rgb(85,85,85) client-side
actor Alice
participant UI
participant Plugin
end

box rgb(85,85,85) server-side
participant psinode
end

Alice->>UI: Click
UI->>Plugin: Call action
note over Plugin: Internal logic
Plugin->>psinode: Submit transaction

Communication with plugins

Apps built on psibase make use of shared infrastructure, and as such have the option to interoperate in ways that are difficult or impossible for traditional web apps. On the server side, psibase services can simply call synchronous actions on each other. On the client-side, plugins can also make calls between each other.

On the client-side, an app may call a function defined in another app's plugin. When this happens, a message is passed from the user interface of one app to the supervisor, which then instantiates the target plugin in its own app domain, which ensures that local app data remains isolated except that which is intentionally exposed through an plugin.

sequenceDiagram 

box rgb(85,85,85) client-side, Domain 1
actor Alice
participant UI
participant Plugin
end

box rgb(85,85,85) client-side, Domain 2
participant Plugin2
end

box rgb(85,85,85) server-side
participant psinode
end

Alice->>UI: Click
UI->>Plugin: Action
Note over Plugin: Requires client-side<br>data from App 2
Plugin->>Plugin2: Query
Plugin2-->>Plugin: Response
Plugin->>psinode: Submit transaction

Cross-domain communication

Calls made across domains are done so via Window.PostMessage.

According to the Window.postMessage documentation:

After postMessage() is called, the MessageEvent will be dispatched only after all pending execution contexts have finished.

Therefore postMessage does not immediately (synchronously) post to the other domain. Instead it schedules a payload to be dispatched after the completion of all remaining execution contexts. In order for cross-domain calls to appear synchronous, the caller should await the asynchronous call.

Cross-domain security considerations

Cross-domain messaging can be dangerous if the proper checks are not in place to ensure that the messages are going to/from whoever is intended.

The supervisor listens for cross-domain messages from its parent window and from the plugins it instantiates, and plugins will only listen for messages that come directly from the supervisor.

If plugins make use of the standard psibase development libraries, then many of the security concerns are handled automatically. For example, it is automatically enforced that messages into your plugin are only allowed to come from the supervisor.

Transaction packing

Transactions contain the data and authentication payload necessary to execute a service action on the server. Transactions may contain multiple actions. Plugins are responsible for filling the transaction objects with actions.

A plugin is by default ignorant of all actions added into a transaction except any that it was itself responsible for adding. This improves privacy on the client-side by ensuring that plugins are only aware of the user's actions that are relevant to it.

Actions are added to transactions in a FIFO queue. For example, the following sequence diagram will finally submit a transaction containing actions in the order: A, B, C.

sequenceDiagram 

box rgb(85,85,85) client-side, Domain 1
actor Alice
participant UI
participant Plugin
end

box rgb(85,85,85) client-side, Domain 2
participant Plugin2
end

box rgb(85,85,85) server-side
participant psinode
end

Alice->>UI: Click
UI->>Plugin: Do C
Note over Plugin: Action A
Plugin->>Plugin2: Action
Note over Plugin2: Action B
Plugin2-->>Plugin: Response
Note over Plugin: Action C
Plugin->>psinode: Submit transaction

Transaction submission

For simplicity, the previous diagrams have shown the plugin as the component that submits the final transaction. But that would conflict with the principle of only allowing a plugin to know about the actions that it was itself responsible for adding to the transaction.

Instead, that responsibility is given to the supervisor.

The sequence looks more like the following, if we include the Root domain:

sequenceDiagram 

box rgb(85,85,85) client-side, Domain 1
actor Alice
participant UI
participant Plugin
end

box rgb(85,85,85) client-side, Supervisor domain
participant supervisor
end

box rgb(85,85,85) server-side
participant psinode
end

Alice->>UI: Click
UI->>supervisor: Call action
supervisor->>Plugin: Call action
activate Plugin
Plugin->>supervisor: Action A
activate supervisor
note over supervisor: transaction { A }
supervisor-->>Plugin: Ok
deactivate supervisor
Plugin-->>supervisor: Ok
deactivate Plugin
supervisor->>psinode: Submit transaction

A complete interaction

The following sequence diagram shows an example of a complete interaction involving an app, the supervisor, as well as multiple plugins. The diagram also includes transaction packing and submission. It attempts to show how the cross-domain messaging only occurs after the completion of all active execution contexts (EC). A key interaction that is intentionally left out of the following diagram for simplicity is the supervisor's process for the aggregation of digital signatures and other authorization payloads.

%%{init: { 'sequence': {'noteAlign': 'left'} }}%%
sequenceDiagram 

box rgb(85,85,85) client-side, Domain 1
participant UI
participant plugin
end

box rgb(85,85,85) client-side, Supervisor domain
participant supervisor
end

box rgb(85,85,85) client-side, Domain 2
participant plugin_2
end

box rgb(85,85,85) server-side
participant psinode
end

Note over UI: [IAC - Wait on EC]
UI->>supervisor: Call action@plugin
activate supervisor

supervisor->>psinode: Get plugin
psinode-->>supervisor: reply
note over supervisor: [IAC - Wait on EC]
supervisor->>plugin: action
activate plugin

note over plugin: [IAC - Wait on EC]
plugin->>supervisor: Call action2@plugin2
activate supervisor

supervisor->>psinode: Get plugin2
psinode-->>supervisor: reply
note over supervisor: [IAC - Wait on EC]
supervisor->>plugin_2: action2
activate plugin_2

note over plugin_2: [IAC - Wait on EC]
plugin_2->>supervisor: Call action@serviceA
activate supervisor

note over supervisor: transaction { action@serviceA }

note over supervisor: [IAC - Wait on EC]
supervisor-->>plugin_2: reply
deactivate supervisor
note over plugin_2: [IAC - Wait on EC]
plugin_2-->>supervisor: reply
deactivate plugin_2

note over supervisor: [IAC - Wait on EC]
supervisor-->>plugin: reply
deactivate supervisor

note over plugin: [IAC - Wait on EC]
plugin->>supervisor: Call action@serviceB
activate supervisor
note over supervisor: transaction { <br> action@serviceA,<br> action@serviceB<br>}
note over supervisor: [IAC - Wait on EC]
supervisor-->>plugin: reply
deactivate supervisor
note over plugin: [IAC - Wait on EC]
plugin-->>supervisor: reply
deactivate plugin

note over supervisor: [IAC - Wait on EC]
supervisor->>psinode: Submit transaction
psinode-->>supervisor: reply
supervisor-->>UI: reply
deactivate supervisor

Simplified mental model

Plugin communication is a complex coordination process facilitated by the supervisor. However, for most purposes, UI and plugin developers do not need to understand this complexity. For most purposes, it is sufficient to imagine that calls into plugins are simple and direct.

With this simplified mental model, it is easier to see how these capabilities lead to powerful client-side app composability. Consider the following example of an app that manages the creation of a token using some of the psibase example Token and Symbol services & plugins.

sequenceDiagram

box rgb(85,85,85) client-side
participant TokenCreator app
participant Token plugin
participant Symbol plugin
end
participant psinode

Note over TokenCreator app: Alice submits form with new token<br>characteristics and symbol name

TokenCreator app->>Token plugin: CreateToken
    Token plugin->>Symbol plugin: BuySymbol
        Symbol plugin->>psinode: [Action] credit@tokens
        Symbol plugin->>psinode: [Action] buySymbol@symbol
        Symbol plugin-->>Token plugin: reply
    Token plugin->>psinode: [Action] mapToNewToken@symbol
    Token plugin-->>TokenCreator app: reply

As you can see, someone creating an app to facilitate the creation of tokens would simply need to call the correct Token plugin action. The Token plugin allows you to specify a symbol and will automatically purchase a symbol from a symbol market and map it to the new token. All of the various interactions result in calling three separate actions in psibase services. Those three actions will automatically be packaged into one single transaction, enabling maximally efficient processing of the action on the server (authentication logic only runs once to verify the sender is authorized for the whole transaction, rather than authorization logic executing once for each action submitted in separate transactions).

Plugin developers

Although the code executes client-side, plugins are very similar to services. For example, just as in the context of the execution of a service action the service has full control over its own database, plugins are permitted to silently call actions on their own service (Without additional confirmation prompts to the end user). Therefore, failing to include the proper security checks in the plugin could allow the service to be exploited, corrupting shared data. This is unlike the more familiar concerns of UI developers which are traditionally much more limited.

Furthermore, writing a plugin often requires detailed knowledge about how to correctly call service actions, and in what order.

For these reasons, plugins should be thought of as the responsibility of the back-end / service developer. Correspondingly, they can be written in the same programming language as services.

Updating plugins

The intention is that the only code bundled in with a front-end app is the plugin API, not the entire plugin binary itself. The binary is requested at run-time by the client's browser. This is similar to how calls made to services will be bundled in with user interfaces and other client-code, but the implementation of the service actions are server side.

This implies that any changes to the plugin API can break client code, just as changes to the service API can break client code. However, just as service implementations can be seamlessly updated to fix bugs or make improvements, plugin implementations can also change, and all users of the plugin will automatically use the updated code.