- Implementation Owner: @eldadfux @TorstenDittmann
- Start Date: 21-02-2021
- Target Date: Unknown
- Appwrite Issue: Is this RFC inspired by an issue in appwrite
We are adding a real-time API for the Appwrite server, which allows streaming and listening of all the Appwrite system events. The new real-time API will allow new and interesting use cases of building an app with Appwrite from simple chat apps, multiplayer games, live collaboration to real-time dashboards and enhanced UIs.
Unlike other BaaS products, Appwrite real-time should be completely cross-platform and work well from both client or backend. The new API should also be completely decoupled from the Appwrite database and transmit any supported system events that Appwrite produces, including storage, users, account events, and more.
What problem are you trying to solve?
The Appwrite current REST API, and upcoming GraphQL are both HTTP based which is not suitable for realtime applications. HTTP classis request-response communication is not very well suited for listening and reporting of events.
Currently the only way to build realtime, events based application with Appwrite is by using a 3rd party server which is hard and complicated to implement espcially when data segmentatation and ACL is a requirement. An current alternative to using a 3rd party service, is to use HTTP-Polling which is costly and not optimized for performance.
What is the context or background in which this problem exists?
Allowing new use-cases for Appwrite, and giving more flexibily for end-developers relaying on Appwrite for user authentication and data proccessing.
Once the proposal is implemented, how will the system change?
We will intorduce a new server entrypoint, allowing any end-platform to connect and subscribe for Appwrite system events in realtime. Likewise, a new service will be provided in the client SDKs, which will allow developers to access these endpoints in a user-friendly way.
We will implement the realtime API, using a new entrypoint for the Appwrite main container as demostrated at our POC branch. The new entrypoint will be called realtime
and will server as the starting script for the new appwrite-realtime
container.
The source for the new entrypoint should be located at the same equilvent to our http entrypoint (./app/http.php
) at: /app/realtime.php
. We will use the Swoole implementation of Websocket for the implementation of our first realtime protocol. Like as with other Appwrite related process, we should create a bash alias for a pretty entrypoint that will start the server (./bin/realtime).
We should update appwrite Traefik loadbalancer to redirect all ws & wss traffic for /v1/realtime
to the new appwrite-realtime
container.
An example configuration can also be found on the POC branch.
As part of the Appwrite agenda of being cross-platform and tech agnostic, we should be able to design the new real-time API with multiple messaging protocols in mind. This does not mean we should support all of them, and for the scope of this RFC, we will only discuss the implementation of the Websockets protocol as the first protocol to support, while keeping in mind to avoid any coupling between the architecture and the resulting protocol.
Possible protocols to take under future considiration:
- Websocket support: https://www.swoole.co.uk/docs/modules/swoole-websocket-server
- MQTT support: https://www.swoole.co.uk/docs/modules/swoole-mqtt-server
- SSE support: https://github.com/hhxsv5/php-sse
- Socket.io support: https://github.com/shuixn/socket.io-swoole-server
Channel | Description |
---|---|
account | All account related events (session create, name update...) |
collections | Any update/delete/create events for collections where user has read access |
collections.[ID] | Any update/delete/create events to a given collection where user has read access |
collections.[ID].documents | Any update/delete/create events to any document in a given collection where user has read access |
documents | Any update/delete/create events to documents where user has read access |
documents.[ID] | Any update/delete/create events to a given document where user has read access |
files | Any update/delete/create events to file where user has read access |
files.[ID] | Any update/delete/create events to a given file where user has read access |
executions | Any update to executions where user has read access |
executions.[ID] | Any update to a given function execution where user has read access |
functions.[ID] | Any execution event to a given function where user has read access |
The message from the server to the clients should be a JSON string and reflect the following object:
Property | Description |
---|---|
event | The name of the event equivalent to the system event. |
channels | An array of channels that can receive this message. |
timestamp | To ensure consistency across all client platforms and real-time technologies, the event timestamp is included. |
payload | Payload contains the data equal to the response model. |
This will be sent to the user if there was a invalid connection established, for eample providing a wrong project id:
Property | Description |
---|---|
code | A code that imitate the Websocket close codes. |
message | Related error message. |
Subscriptions can be bound to channels and are passed as query parameters and part of the websocket URL. An example looks like this:
wss://appwrite.test/v1/realtime?project=XXXXXX&channels[]=account&channels=documents.XXXXXX
Or as SDK call in JavaScript:
let sdk = new Appwrite();
sdk
.setEndpoint('https://appwrite.test') // Will also guess the WebSocket Endpoint by replacing protocols
.setEndpointRealtime('wss://appwrite.test') // Optional: Realtime Endpoint
.setProject('5df5acd0d48c2') // Your project ID
;
sdk.subscribe('account', response => {
console.log(response); // Callback will be executed on account event.
});
const documentsUnsubscribe = sdk.realtime.subscribe('documents.XXXXXX', response => {
console.log(response); // Callback will be executed on documents.XXXXXX event
});
documentsUnsubscribe(); // the subscribe() method will return a method to unsubscribe, invalidate the callback and remove the channel
Setting the Endpoint will also guess the Realtime Endpoint by replacing the http/https Protocol with the WebSocket equivalent. Alternatively you can set the Realtime Endpoint explicitly.
The client SDK will maintain a single connection and will maintain all the channels, if a channel is added or removed - a new connection will be established with given channels.
The realtime container can easily be duplicated with Traefik load-balancing it.
References:
- List of Big-O for PHP functions: https://stackoverflow.com/a/2484455/2299554
We should implement an abuse control check to prevent people from abusing the new connection action, which also acts as a validator for JWT tokens. This will completely prevent any large-scale attack from guessing tokens (that has a slim chance, to begin with) or abuse the server.
On each new connection, we should validate that the client origin is valid similarly to what we do in the REST API. This is a good security practice to avoid project data being presented on un-authorized clients. This will force devs to list their platforms before using the real-time API.
Since we will not support 2-way communication now - we shouldn't allow/handle receiving messages at all for now. Which makes the payload size not a concern for now.
On HTTP Handshakes, cookies are usually sent along, which we can use for authentication. If a technology does not provide a native handshake - we can imitate this functionality to handle passing authentication tokens to the endpoint.
Using JWT authentication can be easier to implement and pass to the server.
We should add the following logs for easy debugging and monitoring of our realtime server. Below is a list of some of the logs we could initialy add:
- Server start (stdout - using
Console::success
) - Worker start (stdout - using
Console::success
) - Connection Open (total connections per worker)
- Connection Close (total connections per worker)
- Errors (stderr - using
Console::error
)
Debug mode logs:
- Event received (stdout - using
Console::log
) - Message sent (stdout - using
Console::log
)