diff --git a/.github/workflows/continuous_integration.yml b/.github/workflows/continuous_integration.yml
index 1ad73027..ea67c3c1 100644
--- a/.github/workflows/continuous_integration.yml
+++ b/.github/workflows/continuous_integration.yml
@@ -39,7 +39,7 @@ jobs:
         working-directory: "messages"
 
       - name: "Build proto messages"
-        run: yarn run proto && yarn run copy-to-front && yarn run json-copy-to-front
+        run: yarn run ts-proto && yarn run copy-to-front-ts-proto && yarn run json-copy-to-front
         working-directory: "messages"
 
       - name: "Create index.html"
diff --git a/.github/workflows/end_to_end_tests.yml b/.github/workflows/end_to_end_tests.yml
index a1cb5e5d..ea9ba41c 100644
--- a/.github/workflows/end_to_end_tests.yml
+++ b/.github/workflows/end_to_end_tests.yml
@@ -12,6 +12,7 @@ on:
 jobs:
 
   start-runner:
+    if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == false)
     name: Start self-hosted EC2 runner
     runs-on: ubuntu-latest
     outputs:
@@ -109,12 +110,14 @@ jobs:
     if: ${{ always() }} # required to stop the runner even if the error happened in the previous jobs
     steps:
       - name: Configure AWS credentials
+        if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == false)
         uses: aws-actions/configure-aws-credentials@v1
         with:
           aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
           aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
           aws-region: ${{ secrets.AWS_REGION }}
       - name: Stop EC2 runner
+        if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == false)
         uses: machulav/ec2-github-runner@v2
         with:
           mode: stop
diff --git a/.github/workflows/push-to-npm.yml b/.github/workflows/push-to-npm.yml
index 71f2824f..571a16e6 100644
--- a/.github/workflows/push-to-npm.yml
+++ b/.github/workflows/push-to-npm.yml
@@ -36,7 +36,7 @@ jobs:
         working-directory: "messages"
 
       - name: "Build proto messages"
-        run: yarn run proto && yarn run copy-to-front && yarn run json-copy-to-front
+        run: yarn run ts-proto && yarn run copy-to-front-ts-proto && yarn run json-copy-to-front
         working-directory: "messages"
 
       - name: "Create index.html"
diff --git a/back/server.ts b/back/server.ts
index 4ed443b8..1e474ec9 100644
--- a/back/server.ts
+++ b/back/server.ts
@@ -1,15 +1,15 @@
 // lib/server.ts
 import App from "./src/App";
 import grpc from "grpc";
-import {roomManager} from "./src/RoomManager";
-import {IRoomManagerServer, RoomManagerService} from "./src/Messages/generated/messages_grpc_pb";
-import {HTTP_PORT, GRPC_PORT} from "./src/Enum/EnvironmentVariable";
+import { roomManager } from "./src/RoomManager";
+import { IRoomManagerServer, RoomManagerService } from "./src/Messages/generated/messages_grpc_pb";
+import { HTTP_PORT, GRPC_PORT } from "./src/Enum/EnvironmentVariable";
 
-App.listen(HTTP_PORT, () => console.log(`WorkAdventure HTTP API starting on port %d!`, HTTP_PORT))
+App.listen(HTTP_PORT, () => console.log(`WorkAdventure HTTP API starting on port %d!`, HTTP_PORT));
 
 const server = new grpc.Server();
 server.addService<IRoomManagerServer>(RoomManagerService, roomManager);
 
 server.bind(`0.0.0.0:${GRPC_PORT}`, grpc.ServerCredentials.createInsecure());
 server.start();
-console.log('WorkAdventure HTTP/2 API starting on port %d!', GRPC_PORT);
+console.log("WorkAdventure HTTP/2 API starting on port %d!", GRPC_PORT);
diff --git a/back/src/RoomManager.ts b/back/src/RoomManager.ts
index 3bb425b7..d375fbd8 100644
--- a/back/src/RoomManager.ts
+++ b/back/src/RoomManager.ts
@@ -106,11 +106,6 @@ const roomManager: IRoomManagerServer = {
                                 user,
                                 message.getWebrtcscreensharingsignaltoservermessage() as WebRtcSignalToServerMessage
                             );
-                        } else if (message.hasPlayglobalmessage()) {
-                            socketManager.emitPlayGlobalMessage(
-                                room,
-                                message.getPlayglobalmessage() as PlayGlobalMessage
-                            );
                         } else if (message.hasQueryjitsijwtmessage()) {
                             socketManager.handleQueryJitsiJwtMessage(
                                 user,
diff --git a/back/src/Services/SocketManager.ts b/back/src/Services/SocketManager.ts
index c9da7c96..9233811b 100644
--- a/back/src/Services/SocketManager.ts
+++ b/back/src/Services/SocketManager.ts
@@ -531,15 +531,6 @@ export class SocketManager {
         }
     }
 
-    emitPlayGlobalMessage(room: GameRoom, playGlobalMessage: PlayGlobalMessage) {
-        const serverToClientMessage = new ServerToClientMessage();
-        serverToClientMessage.setPlayglobalmessage(playGlobalMessage);
-
-        for (const [id, user] of room.getUsers().entries()) {
-            user.socket.write(serverToClientMessage);
-        }
-    }
-
     public getWorlds(): Map<string, PromiseLike<GameRoom>> {
         return this.roomsPromises;
     }
diff --git a/docs/maps/api-camera.md b/docs/maps/api-camera.md
new file mode 100644
index 00000000..cb1fe72d
--- /dev/null
+++ b/docs/maps/api-camera.md
@@ -0,0 +1,24 @@
+{.section-title.accent.text-primary}
+# API Camera functions Reference
+
+### Listen to camera updates
+
+```
+WA.camera.onCameraUpdate(): Subscription
+```
+
+Listens to updates of the camera viewport. It will trigger for every update of the camera's properties (position or scale for instance). An event will be sent.
+
+The event has the following attributes :
+*   **x (number):** coordinate X of the camera's world view (the area looked at by the camera).
+*   **y (number):** coordinate Y of the camera's world view.
+*   **width (number):** the width of the camera's world view.
+*   **height (number):** the height of the camera's world view.
+
+**callback:** the function that will be called when the camera is updated.
+
+Example :
+```javascript
+const subscription = WA.camera.onCameraUpdate().subscribe((worldView) => console.log(worldView));
+//later...
+subscription.unsubscribe();
\ No newline at end of file
diff --git a/docs/maps/api-player.md b/docs/maps/api-player.md
index 35d5f464..58d5701a 100644
--- a/docs/maps/api-player.md
+++ b/docs/maps/api-player.md
@@ -86,6 +86,27 @@ WA.onInit().then(() => {
 })
 ```
 
+### Get the position of the player
+```
+WA.player.getPosition(): Promise<Position>
+```
+The player's current position is available using the `WA.player.getPosition()` function.
+
+`Position` has the following attributes :
+* **x (number) :** The coordinate x of the current player's position.
+* **y (number) :** The coordinate y of the current player's position.
+
+
+{.alert.alert-info}
+You need to wait for the end of the initialization before calling `WA.player.getPosition()`
+
+```typescript
+WA.onInit().then(async () => {
+    console.log('Position: ', await WA.player.getPosition());
+})
+```
+
+
 ### Listen to player movement
 ```
 WA.player.onPlayerMove(callback: HasPlayerMovedEventCallback): void;
@@ -107,6 +128,30 @@ Example :
 WA.player.onPlayerMove(console.log);
 ```
 
+## Player specific variables
+Similarly to maps (see [API state related functions](api-state.md)), it is possible to store data **related to a specific player** in a "state". Such data will be stored using the local storage from the user's browser. Any value that is serializable in JSON can be stored.
+
+{.alert.alert-info}
+In the future, player-related variables will be stored on the WorkAdventure server if the current player is logged.
+
+Any value that is serializable in JSON can be stored.
+
+### Setting a property
+A player property can be set simply by assigning a value.
+
+Example:
+```javascript
+WA.player.state.toto = "value" //will set the "toto" key to "value"
+```
+
+### Reading a variable 
+A player variable can be read by calling its key from the player's state. 
+
+Example:
+```javascript
+WA.player.state.toto //will retrieve the variable
+```
+
 ### Set the outline color of the player
 ```
 WA.player.setOutlineColor(red: number, green: number, blue: number): Promise<void>;
diff --git a/docs/maps/api-reference.md b/docs/maps/api-reference.md
index d044668f..a0869075 100644
--- a/docs/maps/api-reference.md
+++ b/docs/maps/api-reference.md
@@ -10,5 +10,6 @@
 - [UI functions](api-ui.md)
 - [Sound functions](api-sound.md)
 - [Controls functions](api-controls.md)
+- [Camera functions](api-camera.md)
 
 - [List of deprecated functions](api-deprecated.md)
diff --git a/docs/maps/api-room.md b/docs/maps/api-room.md
index 72947df8..7d438a1f 100644
--- a/docs/maps/api-room.md
+++ b/docs/maps/api-room.md
@@ -1,8 +1,11 @@
 {.section-title.accent.text-primary}
+
 # API Room functions Reference
 
 ### Working with group layers
-If you use group layers in your map, to reference a layer in a group you will need to use a `/` to join layer names together.
+
+If you use group layers in your map, to reference a layer in a group you will need to use a `/` to join layer names
+together.
 
 Example :
 <div class="row">
@@ -12,6 +15,7 @@ Example :
 </div>
 
 The name of the layers of this map are :
+
 * `entries/start`
 * `bottom/ground/under`
 * `bottom/build/carpet`
@@ -26,29 +30,32 @@ WA.room.onLeaveLayer(name: string): Subscription
 
 Listens to the position of the current user. The event is triggered when the user enters or leaves a given layer.
 
-*   **name**: the name of the layer who as defined in Tiled.
+* **name**: the name of the layer who as defined in Tiled.
 
 Example:
 
 ```javascript
 WA.room.onEnterLayer('myLayer').subscribe(() => {
-    WA.chat.sendChatMessage("Hello!", 'Mr Robot');
+  WA.chat.sendChatMessage("Hello!", 'Mr Robot');
 });
 
 WA.room.onLeaveLayer('myLayer').subscribe(() => {
-    WA.chat.sendChatMessage("Goodbye!", 'Mr Robot');
+  WA.chat.sendChatMessage("Goodbye!", 'Mr Robot');
 });
 ```
 
 ### Show / Hide a layer
+
 ```
 WA.room.showLayer(layerName : string): void
 WA.room.hideLayer(layerName : string) : void
 ```
-These 2 methods can be used to show and hide a layer.
-if `layerName` is the name of a group layer, show/hide all the layer in that group layer.
+
+These 2 methods can be used to show and hide a layer. if `layerName` is the name of a group layer, show/hide all the
+layer in that group layer.
 
 Example :
+
 ```javascript
 WA.room.showLayer('bottom');
 //...
@@ -61,12 +68,14 @@ WA.room.hideLayer('bottom');
 WA.room.setProperty(layerName : string, propertyName : string, propertyValue : string | number | boolean | undefined) : void;
 ```
 
-Set the value of the `propertyName` property of the layer `layerName` at `propertyValue`. If the property doesn't exist, create the property `propertyName` and set the value of the property at `propertyValue`.
+Set the value of the `propertyName` property of the layer `layerName` at `propertyValue`. If the property doesn't exist,
+create the property `propertyName` and set the value of the property at `propertyValue`.
 
 Note :
 To unset a property from a layer, use `setProperty` with `propertyValue` set to `undefined`.
 
 Example :
+
 ```javascript
 WA.room.setProperty('wikiLayer', 'openWebsite', 'https://www.wikipedia.org/');
 ```
@@ -79,13 +88,12 @@ WA.room.id: string;
 
 The ID of the current room is available from the `WA.room.id` property.
 
-{.alert.alert-info}
-You need to wait for the end of the initialization before accessing `WA.room.id`
+{.alert.alert-info} You need to wait for the end of the initialization before accessing `WA.room.id`
 
 ```typescript
 WA.onInit().then(() => {
-    console.log('Room id: ', WA.room.id);
-    // Will output something like: 'https://play.workadventu.re/@/myorg/myworld/myroom', or 'https://play.workadventu.re/_/global/mymap.org/map.json"
+  console.log('Room id: ', WA.room.id);
+  // Will output something like: 'https://play.workadventu.re/@/myorg/myworld/myroom', or 'https://play.workadventu.re/_/global/mymap.org/map.json"
 })
 ```
 
@@ -97,19 +105,17 @@ WA.room.mapURL: string;
 
 The URL of the map is available from the `WA.room.mapURL` property.
 
-{.alert.alert-info}
-You need to wait for the end of the initialization before accessing `WA.room.mapURL`
+{.alert.alert-info} You need to wait for the end of the initialization before accessing `WA.room.mapURL`
 
 ```typescript
 WA.onInit().then(() => {
-    console.log('Map URL: ', WA.room.mapURL);
-    // Will output something like: 'https://mymap.org/map.json"
+  console.log('Map URL: ', WA.room.mapURL);
+  // Will output something like: 'https://mymap.org/map.json"
 })
 ```
 
-
-
 ### Getting map data
+
 ```
 WA.room.getTiledMap(): Promise<ITiledMap>
 ```
@@ -121,12 +127,16 @@ const map = await WA.room.getTiledMap();
 console.log("Map generated with Tiled version ", map.tiledversion);
 ```
 
-Check the [Tiled documentation to learn more about the format of the JSON map](https://doc.mapeditor.org/en/stable/reference/json-map-format/).
+Check
+the [Tiled documentation to learn more about the format of the JSON map](https://doc.mapeditor.org/en/stable/reference/json-map-format/)
+.
 
 ### Changing tiles
+
 ```
 WA.room.setTiles(tiles: TileDescriptor[]): void
 ```
+
 Replace the tile at the `x` and `y` coordinates in the layer named `layer` by the tile with the id `tile`.
 
 If `tile` is a string, it's not the id of the tile but the value of the property `name`.
@@ -137,43 +147,48 @@ If `tile` is a string, it's not the id of the tile but the value of the property
 </div>
 
 `TileDescriptor` has the following attributes :
+
 * **x (number) :** The coordinate x of the tile that you want to replace.
 * **y (number) :** The coordinate y of the tile that you want to replace.
 * **tile (number | string) :** The id of the tile that will be placed in the map.
 * **layer (string) :** The name of the layer where the tile will be placed.
 
-**Important !** : If you use `tile` as a number, be sure to add the `firstgid` of the tileset of the tile that you want to the id of the tile in Tiled Editor.
+**Important !** : If you use `tile` as a number, be sure to add the `firstgid` of the tileset of the tile that you want
+to the id of the tile in Tiled Editor.
 
 Note: If you want to unset a tile, use `setTiles` with `tile` set to `null`.
 
 Example :
+
 ```javascript
 WA.room.setTiles([
-                {x: 6, y: 4, tile: 'blue', layer: 'setTiles'},
-                {x: 7, y: 4, tile: 109, layer: 'setTiles'},
-                {x: 8, y: 4, tile: 109, layer: 'setTiles'},
-                {x: 9, y: 4, tile: 'blue', layer: 'setTiles'}
-                ]);
+  { x: 6, y: 4, tile: 'blue', layer: 'setTiles' },
+  { x: 7, y: 4, tile: 109, layer: 'setTiles' },
+  { x: 8, y: 4, tile: 109, layer: 'setTiles' },
+  { x: 9, y: 4, tile: 'blue', layer: 'setTiles' }
+]);
 ```
 
 ### Loading a tileset
+
 ```
 WA.room.loadTileset(url: string): Promise<number>
 ```
+
 Load a tileset in JSON format from an url and return the id of the first tile of the loaded tileset.
 
 You can create a tileset file in Tile Editor.
 
 ```javascript
 WA.room.loadTileset("Assets/Tileset.json").then((firstId) => {
-    WA.room.setTiles([{x: 4, y: 4, tile: firstId, layer: 'bottom'}]);
+  WA.room.setTiles([{ x: 4, y: 4, tile: firstId, layer: 'bottom' }]);
 })
 ```
 
-
 ## Embedding websites in a map
 
-You can use the scripting API to embed websites in a map, or to edit websites that are already embedded (using the ["website" objects](website-in-map.md)).
+You can use the scripting API to embed websites in a map, or to edit websites that are already embedded (using
+the ["website" objects](website-in-map.md)).
 
 ### Getting an instance of a website already embedded in the map
 
@@ -181,8 +196,8 @@ You can use the scripting API to embed websites in a map, or to edit websites th
 WA.room.website.get(objectName: string): Promise<EmbeddedWebsite>
 ```
 
-You can get an instance of an embedded website by using the `WA.room.website.get()` method.
-It returns a promise of an `EmbeddedWebsite` instance.
+You can get an instance of an embedded website by using the `WA.room.website.get()` method. It returns a promise of
+an `EmbeddedWebsite` instance.
 
 ```javascript
 // Get an existing website object where 'my_website' is the name of the object (on any layer object of the map)
@@ -191,7 +206,6 @@ website.url = 'https://example.com';
 website.visible = true;
 ```
 
-
 ### Adding a new website in a map
 
 ```
@@ -201,34 +215,38 @@ interface CreateEmbeddedWebsiteEvent {
     name: string;       // A unique name for this iframe
     url: string;        // The URL the iframe points to.
     position: {
-        x: number,      // In pixels, relative to the map coordinates
-        y: number,      // In pixels, relative to the map coordinates
-        width: number,  // In pixels, sensitive to zoom level
-        height: number, // In pixels, sensitive to zoom level
+        x: number,      // In "game" pixels, relative to the map or player coordinates, depending on origin
+        y: number,      // In "game" pixels, relative to the map or player coordinates, depending on origin
+        width: number,  // In "game" pixels
+        height: number, // In "game" pixels
     },
     visible?: boolean,  // Whether to display the iframe or not
     allowApi?: boolean, // Whether the scripting API should be available to the iframe
     allow?: string,     // The list of feature policies allowed
+    origin: "player" | "map" // The origin used to place the x and y coordinates of the iframe's top-left corner, defaults to "map"
+    scale: number, // A ratio used to resize the iframe
 }
 ```
 
-You can create an instance of an embedded website by using the `WA.room.website.create()` method.
-It returns an `EmbeddedWebsite` instance.
+You can create an instance of an embedded website by using the `WA.room.website.create()` method. It returns
+an `EmbeddedWebsite` instance.
 
 ```javascript
 // Create a new website object
 const website = WA.room.website.create({
-    name: "my_website",
-    url: "https://example.com",
-    position: {
-        x: 64,
-        y: 128,
-        width: 320,
-        height: 240,
-    },
-    visible: true,
-    allowApi: true,
-    allow: "fullscreen",
+  name: "my_website",
+  url: "https://example.com",
+  position: {
+    x: 64,
+    y: 128,
+    width: 320,
+    height: 240,
+  },
+  visible: true,
+  allowApi: true,
+  allow: "fullscreen",
+  origin: "map",
+  scale: 1,
 });
 ```
 
@@ -240,30 +258,28 @@ WA.room.website.delete(name: string): Promise<void>
 
 Use `WA.room.website.delete` to completely remove an embedded website from your map.
 
-
 ### The EmbeddedWebsite class
 
 Instances of the `EmbeddedWebsite` class represent the website displayed on the map.
 
 ```typescript
 class EmbeddedWebsite {
-    readonly name: string;
-    url: string;
-    visible: boolean;
-    allow: string;
-    allowApi: boolean;
-    x: number;         // In pixels, relative to the map coordinates
-    y: number;         // In pixels, relative to the map coordinates
-    width: number;     // In pixels, sensitive to zoom level
-    height: number;    // In pixels, sensitive to zoom level
+  readonly name: string;
+  url: string;
+  visible: boolean;
+  allow: string;
+  allowApi: boolean;
+  x: number;         // In "game" pixels, relative to the map or player coordinates, depending on origin
+  y: number;         // In "game" pixels, relative to the map or player coordinates, depending on origin
+  width: number;     // In "game" pixels
+  height: number;    // In "game" pixels
+  origin: "player" | "map";
+  scale: number;
 }
 ```
 
 When you modify a property of an `EmbeddedWebsite` instance, the iframe is automatically modified in the map.
 
-
-{.alert.alert-warning}
-The websites you add/edit/delete via the scripting API are only shown locally. If you want them
-to be displayed for every player, you can use [variables](api-start.md) to share a common state
-between all users.
+{.alert.alert-warning} The websites you add/edit/delete via the scripting API are only shown locally. If you want them
+to be displayed for every player, you can use [variables](api-start.md) to share a common state between all users.
 
diff --git a/front/.eslintrc.js b/front/.eslintrc.js
index 117cb7e6..dc2b6bd6 100644
--- a/front/.eslintrc.js
+++ b/front/.eslintrc.js
@@ -35,7 +35,6 @@ module.exports = {
         "no-unused-vars": "off",
         "@typescript-eslint/no-explicit-any": "error",
         // TODO: remove those ignored rules and write a stronger code!
-        "@typescript-eslint/no-floating-promises": "off",
         "@typescript-eslint/no-unsafe-call": "off",
         "@typescript-eslint/restrict-plus-operands": "off",
         "@typescript-eslint/no-unsafe-assignment": "off",
diff --git a/front/Dockerfile b/front/Dockerfile
index f781a37c..49cf6046 100644
--- a/front/Dockerfile
+++ b/front/Dockerfile
@@ -1,13 +1,14 @@
 FROM node:14.15.4-buster-slim@sha256:cbae886186467bbfd274b82a234a1cdfbbd31201c2a6ee63a6893eefcf3c6e76 as builder
 WORKDIR /usr/src
 COPY messages .
-RUN yarn install && yarn proto
+RUN yarn install && yarn ts-proto
 
 # we are rebuilding on each deploy to cope with the PUSHER_URL environment URL
 FROM thecodingmachine/nodejs:14-apache
 
 COPY --chown=docker:docker front .
-COPY --from=builder --chown=docker:docker /usr/src/generated /var/www/html/src/Messages/generated
+COPY --from=builder --chown=docker:docker /usr/src/ts-proto-generated/protos /var/www/html/src/Messages/ts-proto-generated
+RUN sed -i 's/import { Observable } from "rxjs";/import type { Observable } from "rxjs";/g' /var/www/html/src/Messages/ts-proto-generated/messages.ts
 COPY --from=builder --chown=docker:docker /usr/src/JsonMessages /var/www/html/src/Messages/JsonMessages
 
 # Removing the iframe.html file from the final image as this adds a XSS attack.
diff --git a/front/package.json b/front/package.json
index eae92cd2..935c254f 100644
--- a/front/package.json
+++ b/front/package.json
@@ -62,6 +62,7 @@
     "simple-peer": "^9.11.0",
     "socket.io-client": "^2.3.0",
     "standardized-audio-context": "^25.2.4",
+    "ts-proto": "^1.96.0",
     "uuidv4": "^6.2.10"
   },
   "scripts": {
diff --git a/front/src/Administration/AnalyticsClient.ts b/front/src/Administration/AnalyticsClient.ts
index fb2b604b..4c1ca93a 100644
--- a/front/src/Administration/AnalyticsClient.ts
+++ b/front/src/Administration/AnalyticsClient.ts
@@ -18,64 +18,84 @@ class AnalyticsClient {
     }
 
     identifyUser(uuid: string, email: string | null) {
-        this.posthogPromise?.then((posthog) => {
-            posthog.identify(uuid, { uuid, email, wa: true });
-        });
+        this.posthogPromise
+            ?.then((posthog) => {
+                posthog.identify(uuid, { uuid, email, wa: true });
+            })
+            .catch((e) => console.error(e));
     }
 
     loggedWithSso() {
-        this.posthogPromise?.then((posthog) => {
-            posthog.capture("wa-logged-sso");
-        });
+        this.posthogPromise
+            ?.then((posthog) => {
+                posthog.capture("wa-logged-sso");
+            })
+            .catch((e) => console.error(e));
     }
 
     loggedWithToken() {
-        this.posthogPromise?.then((posthog) => {
-            posthog.capture("wa-logged-token");
-        });
+        this.posthogPromise
+            ?.then((posthog) => {
+                posthog.capture("wa-logged-token");
+            })
+            .catch((e) => console.error(e));
     }
 
     enteredRoom(roomId: string, roomGroup: string | null) {
-        this.posthogPromise?.then((posthog) => {
-            posthog.capture("$pageView", { roomId, roomGroup });
-            posthog.capture("enteredRoom");
-        });
+        this.posthogPromise
+            ?.then((posthog) => {
+                posthog.capture("$pageView", { roomId, roomGroup });
+                posthog.capture("enteredRoom");
+            })
+            .catch((e) => console.error(e));
     }
 
     openedMenu() {
-        this.posthogPromise?.then((posthog) => {
-            posthog.capture("wa-opened-menu");
-        });
+        this.posthogPromise
+            ?.then((posthog) => {
+                posthog.capture("wa-opened-menu");
+            })
+            .catch((e) => console.error(e));
     }
 
     launchEmote(emote: string) {
-        this.posthogPromise?.then((posthog) => {
-            posthog.capture("wa-emote-launch", { emote });
-        });
+        this.posthogPromise
+            ?.then((posthog) => {
+                posthog.capture("wa-emote-launch", { emote });
+            })
+            .catch((e) => console.error(e));
     }
 
     enteredJitsi(roomName: string, roomId: string) {
-        this.posthogPromise?.then((posthog) => {
-            posthog.capture("wa-entered-jitsi", { roomName, roomId });
-        });
+        this.posthogPromise
+            ?.then((posthog) => {
+                posthog.capture("wa-entered-jitsi", { roomName, roomId });
+            })
+            .catch((e) => console.error(e));
     }
 
     validationName() {
-        this.posthogPromise?.then((posthog) => {
-            posthog.capture("wa-name-validation");
-        });
+        this.posthogPromise
+            ?.then((posthog) => {
+                posthog.capture("wa-name-validation");
+            })
+            .catch((e) => console.error(e));
     }
 
     validationWoka(scene: string) {
-        this.posthogPromise?.then((posthog) => {
-            posthog.capture("wa-woka-validation", { scene });
-        });
+        this.posthogPromise
+            ?.then((posthog) => {
+                posthog.capture("wa-woka-validation", { scene });
+            })
+            .catch((e) => console.error(e));
     }
 
     validationVideo() {
-        this.posthogPromise?.then((posthog) => {
-            posthog.capture("wa-video-validation");
-        });
+        this.posthogPromise
+            ?.then((posthog) => {
+                posthog.capture("wa-video-validation");
+            })
+            .catch((e) => console.error(e));
     }
 }
 export const analyticsClient = new AnalyticsClient();
diff --git a/front/src/Api/Events/EmbeddedWebsiteEvent.ts b/front/src/Api/Events/EmbeddedWebsiteEvent.ts
index 42630be1..57c24853 100644
--- a/front/src/Api/Events/EmbeddedWebsiteEvent.ts
+++ b/front/src/Api/Events/EmbeddedWebsiteEvent.ts
@@ -22,6 +22,8 @@ export const isEmbeddedWebsiteEvent = new tg.IsInterface()
         y: tg.isNumber,
         width: tg.isNumber,
         height: tg.isNumber,
+        origin: tg.isSingletonStringUnion("player", "map"),
+        scale: tg.isNumber,
     })
     .get();
 
@@ -35,6 +37,8 @@ export const isCreateEmbeddedWebsiteEvent = new tg.IsInterface()
         visible: tg.isBoolean,
         allowApi: tg.isBoolean,
         allow: tg.isString,
+        origin: tg.isSingletonStringUnion("player", "map"),
+        scale: tg.isNumber,
     })
     .get();
 
diff --git a/front/src/Api/Events/GameStateEvent.ts b/front/src/Api/Events/GameStateEvent.ts
index 1f0f36ed..9755ba9e 100644
--- a/front/src/Api/Events/GameStateEvent.ts
+++ b/front/src/Api/Events/GameStateEvent.ts
@@ -10,6 +10,7 @@ export const isGameStateEvent = new tg.IsInterface()
         tags: tg.isArray(tg.isString),
         variables: tg.isObject,
         userRoomToken: tg.isUnion(tg.isString, tg.isUndefined),
+        playerVariables: tg.isObject,
     })
     .get();
 /**
diff --git a/front/src/Api/Events/IframeEvent.ts b/front/src/Api/Events/IframeEvent.ts
index 2871b93c..8fb488dc 100644
--- a/front/src/Api/Events/IframeEvent.ts
+++ b/front/src/Api/Events/IframeEvent.ts
@@ -30,6 +30,8 @@ import type { MenuRegisterEvent, UnregisterMenuEvent } from "./ui/MenuRegisterEv
 import type { ChangeLayerEvent } from "./ChangeLayerEvent";
 import type { ChangeZoneEvent } from "./ChangeZoneEvent";
 import { isColorEvent } from "./ColorEvent";
+import { isPlayerPosition } from "./PlayerPosition";
+import type { WasCameraUpdatedEvent } from "./WasCameraUpdatedEvent";
 
 export interface TypedMessageEvent<T> extends MessageEvent {
     data: T;
@@ -50,6 +52,7 @@ export type IframeEventMap = {
     displayBubble: null;
     removeBubble: null;
     onPlayerMove: undefined;
+    onCameraUpdate: undefined;
     showLayer: LayerEvent;
     hideLayer: LayerEvent;
     setProperty: SetPropertyEvent;
@@ -82,6 +85,7 @@ export interface IframeResponseEventMap {
     leaveZoneEvent: ChangeZoneEvent;
     buttonClickedEvent: ButtonClickedEvent;
     hasPlayerMoved: HasPlayerMovedEvent;
+    wasCameraUpdated: WasCameraUpdatedEvent;
     menuItemClicked: MenuItemClickedEvent;
     setVariable: SetVariableEvent;
     messageTriggered: MessageReferenceEvent;
@@ -161,6 +165,10 @@ export const iframeQueryMapTypeGuards = {
         query: tg.isUndefined,
         answer: tg.isUndefined,
     },
+    getPlayerPosition: {
+        query: tg.isUndefined,
+        answer: isPlayerPosition,
+    },
 };
 
 type GuardedType<T> = T extends (x: unknown) => x is infer T ? T : never;
diff --git a/front/src/Api/Events/PlayerPosition.ts b/front/src/Api/Events/PlayerPosition.ts
new file mode 100644
index 00000000..54fac6fe
--- /dev/null
+++ b/front/src/Api/Events/PlayerPosition.ts
@@ -0,0 +1,10 @@
+import * as tg from "generic-type-guard";
+
+export const isPlayerPosition = new tg.IsInterface()
+    .withProperties({
+        x: tg.isNumber,
+        y: tg.isNumber,
+    })
+    .get();
+
+export type PlayerPosition = tg.GuardedType<typeof isPlayerPosition>;
diff --git a/front/src/Api/Events/SetVariableEvent.ts b/front/src/Api/Events/SetVariableEvent.ts
index 3e2303b3..80ac6f6e 100644
--- a/front/src/Api/Events/SetVariableEvent.ts
+++ b/front/src/Api/Events/SetVariableEvent.ts
@@ -4,6 +4,7 @@ export const isSetVariableEvent = new tg.IsInterface()
     .withProperties({
         key: tg.isString,
         value: tg.isUnknown,
+        target: tg.isSingletonStringUnion("global", "player"),
     })
     .get();
 /**
diff --git a/front/src/Api/Events/WasCameraUpdatedEvent.ts b/front/src/Api/Events/WasCameraUpdatedEvent.ts
new file mode 100644
index 00000000..34e39a84
--- /dev/null
+++ b/front/src/Api/Events/WasCameraUpdatedEvent.ts
@@ -0,0 +1,19 @@
+import * as tg from "generic-type-guard";
+
+export const isWasCameraUpdatedEvent = new tg.IsInterface()
+    .withProperties({
+        x: tg.isNumber,
+        y: tg.isNumber,
+        width: tg.isNumber,
+        height: tg.isNumber,
+        zoom: tg.isNumber,
+    })
+    .get();
+
+/**
+ * A message sent from the game to the iFrame to notify a movement from the camera.
+ */
+
+export type WasCameraUpdatedEvent = tg.GuardedType<typeof isWasCameraUpdatedEvent>;
+
+export type WasCameraUpdatedEventCallback = (event: WasCameraUpdatedEvent) => void;
diff --git a/front/src/Api/IframeListener.ts b/front/src/Api/IframeListener.ts
index 67b49344..216a9510 100644
--- a/front/src/Api/IframeListener.ts
+++ b/front/src/Api/IframeListener.ts
@@ -31,6 +31,7 @@ import type { SetVariableEvent } from "./Events/SetVariableEvent";
 import { ModifyEmbeddedWebsiteEvent, isEmbeddedWebsiteEvent } from "./Events/EmbeddedWebsiteEvent";
 import { handleMenuRegistrationEvent, handleMenuUnregisterEvent } from "../Stores/MenuStore";
 import type { ChangeLayerEvent } from "./Events/ChangeLayerEvent";
+import type { WasCameraUpdatedEvent } from "./Events/WasCameraUpdatedEvent";
 import type { ChangeZoneEvent } from "./Events/ChangeZoneEvent";
 
 type AnswererCallback<T extends keyof IframeQueryMap> = (
@@ -85,6 +86,9 @@ class IframeListener {
     private readonly _loadSoundStream: Subject<LoadSoundEvent> = new Subject();
     public readonly loadSoundStream = this._loadSoundStream.asObservable();
 
+    private readonly _trackCameraUpdateStream: Subject<LoadSoundEvent> = new Subject();
+    public readonly trackCameraUpdateStream = this._trackCameraUpdateStream.asObservable();
+
     private readonly _setTilesStream: Subject<SetTilesEvent> = new Subject();
     public readonly setTilesStream = this._setTilesStream.asObservable();
 
@@ -226,6 +230,8 @@ class IframeListener {
                         this._removeBubbleStream.next();
                     } else if (payload.type == "onPlayerMove") {
                         this.sendPlayerMove = true;
+                    } else if (payload.type == "onCameraUpdate") {
+                        this._trackCameraUpdateStream.next();
                     } else if (payload.type == "setTiles" && isSetTilesEvent(payload.data)) {
                         this._setTilesStream.next(payload.data);
                     } else if (payload.type == "modifyEmbeddedWebsite" && isEmbeddedWebsiteEvent(payload.data)) {
@@ -442,6 +448,13 @@ class IframeListener {
         }
     }
 
+    sendCameraUpdated(event: WasCameraUpdatedEvent) {
+        this.postMessage({
+            type: "wasCameraUpdated",
+            data: event,
+        });
+    }
+
     sendButtonClickedEvent(popupId: number, buttonId: number): void {
         this.postMessage({
             type: "buttonClickedEvent",
diff --git a/front/src/Api/iframe/Room/EmbeddedWebsite.ts b/front/src/Api/iframe/Room/EmbeddedWebsite.ts
index 7b16890e..d9c2d986 100644
--- a/front/src/Api/iframe/Room/EmbeddedWebsite.ts
+++ b/front/src/Api/iframe/Room/EmbeddedWebsite.ts
@@ -12,6 +12,8 @@ export class EmbeddedWebsite {
     private _allow: string;
     private _allowApi: boolean;
     private _position: Rectangle;
+    private readonly origin: "map" | "player" | undefined;
+    private _scale: number;
 
     constructor(private config: CreateEmbeddedWebsiteEvent) {
         this.name = config.name;
@@ -20,6 +22,12 @@ export class EmbeddedWebsite {
         this._allow = config.allow ?? "";
         this._allowApi = config.allowApi ?? false;
         this._position = config.position;
+        this.origin = config.origin;
+        this._scale = config.scale ?? 1;
+    }
+
+    public get url() {
+        return this._url;
     }
 
     public set url(url: string) {
@@ -33,6 +41,10 @@ export class EmbeddedWebsite {
         });
     }
 
+    public get visible() {
+        return this._visible;
+    }
+
     public set visible(visible: boolean) {
         this._visible = visible;
         sendToWorkadventure({
@@ -44,6 +56,10 @@ export class EmbeddedWebsite {
         });
     }
 
+    public get x() {
+        return this._position.x;
+    }
+
     public set x(x: number) {
         this._position.x = x;
         sendToWorkadventure({
@@ -55,6 +71,10 @@ export class EmbeddedWebsite {
         });
     }
 
+    public get y() {
+        return this._position.y;
+    }
+
     public set y(y: number) {
         this._position.y = y;
         sendToWorkadventure({
@@ -66,6 +86,10 @@ export class EmbeddedWebsite {
         });
     }
 
+    public get width() {
+        return this._position.width;
+    }
+
     public set width(width: number) {
         this._position.width = width;
         sendToWorkadventure({
@@ -77,6 +101,10 @@ export class EmbeddedWebsite {
         });
     }
 
+    public get height() {
+        return this._position.height;
+    }
+
     public set height(height: number) {
         this._position.height = height;
         sendToWorkadventure({
@@ -87,4 +115,19 @@ export class EmbeddedWebsite {
             },
         });
     }
+
+    public get scale(): number {
+        return this._scale;
+    }
+
+    public set scale(scale: number) {
+        this._scale = scale;
+        sendToWorkadventure({
+            type: "modifyEmbeddedWebsite",
+            data: {
+                name: this.name,
+                scale: this._scale,
+            },
+        });
+    }
 }
diff --git a/front/src/Api/iframe/Ui/ActionMessage.ts b/front/src/Api/iframe/Ui/ActionMessage.ts
index 912603b9..ff0908ff 100644
--- a/front/src/Api/iframe/Ui/ActionMessage.ts
+++ b/front/src/Api/iframe/Ui/ActionMessage.ts
@@ -26,7 +26,7 @@ export class ActionMessage {
         this.message = actionMessageOptions.message;
         this.type = actionMessageOptions.type ?? "message";
         this.callback = actionMessageOptions.callback;
-        this.create();
+        this.create().catch((e) => console.error(e));
     }
 
     private async create() {
diff --git a/front/src/Api/iframe/camera.ts b/front/src/Api/iframe/camera.ts
new file mode 100644
index 00000000..a832290e
--- /dev/null
+++ b/front/src/Api/iframe/camera.ts
@@ -0,0 +1,29 @@
+import { IframeApiContribution, sendToWorkadventure } from "./IframeApiContribution";
+import { Subject } from "rxjs";
+import type { WasCameraUpdatedEvent } from "../Events/WasCameraUpdatedEvent";
+import { apiCallback } from "./registeredCallbacks";
+import { isWasCameraUpdatedEvent } from "../Events/WasCameraUpdatedEvent";
+
+const moveStream = new Subject<WasCameraUpdatedEvent>();
+
+export class WorkAdventureCameraCommands extends IframeApiContribution<WorkAdventureCameraCommands> {
+    callbacks = [
+        apiCallback({
+            type: "wasCameraUpdated",
+            typeChecker: isWasCameraUpdatedEvent,
+            callback: (payloadData) => {
+                moveStream.next(payloadData);
+            },
+        }),
+    ];
+
+    onCameraUpdate(): Subject<WasCameraUpdatedEvent> {
+        sendToWorkadventure({
+            type: "onCameraUpdate",
+            data: null,
+        });
+        return moveStream;
+    }
+}
+
+export default new WorkAdventureCameraCommands();
diff --git a/front/src/Api/iframe/player.ts b/front/src/Api/iframe/player.ts
index 2d187bf5..0c71ae33 100644
--- a/front/src/Api/iframe/player.ts
+++ b/front/src/Api/iframe/player.ts
@@ -3,6 +3,7 @@ import type { HasPlayerMovedEvent, HasPlayerMovedEventCallback } from "../Events
 import { Subject } from "rxjs";
 import { apiCallback } from "./registeredCallbacks";
 import { isHasPlayerMovedEvent } from "../Events/HasPlayerMovedEvent";
+import { createState } from "./state";
 
 const moveStream = new Subject<HasPlayerMovedEvent>();
 
@@ -31,6 +32,8 @@ export const setUuid = (_uuid: string | undefined) => {
 };
 
 export class WorkadventurePlayerCommands extends IframeApiContribution<WorkadventurePlayerCommands> {
+    readonly state = createState("player");
+
     callbacks = [
         apiCallback({
             type: "hasPlayerMoved",
@@ -74,6 +77,13 @@ export class WorkadventurePlayerCommands extends IframeApiContribution<Workadven
         return uuid;
     }
 
+    async getPosition(): Promise<Position> {
+        return await queryWorkadventure({
+            type: "getPlayerPosition",
+            data: undefined,
+        });
+    }
+
     get userRoomToken(): string | undefined {
         if (userRoomToken === undefined) {
             throw new Error(
@@ -102,4 +112,9 @@ export class WorkadventurePlayerCommands extends IframeApiContribution<Workadven
     }
 }
 
+export type Position = {
+    x: number;
+    y: number;
+};
+
 export default new WorkadventurePlayerCommands();
diff --git a/front/src/Api/iframe/state.ts b/front/src/Api/iframe/state.ts
index a875f3e0..278b208e 100644
--- a/front/src/Api/iframe/state.ts
+++ b/front/src/Api/iframe/state.ts
@@ -8,93 +8,101 @@ import { isSetVariableEvent, SetVariableEvent } from "../Events/SetVariableEvent
 
 import type { ITiledMap } from "../../Phaser/Map/ITiledMap";
 
-const setVariableResolvers = new Subject<SetVariableEvent>();
-const variables = new Map<string, unknown>();
-const variableSubscribers = new Map<string, Subject<unknown>>();
-
-export const initVariables = (_variables: Map<string, unknown>): void => {
-    for (const [name, value] of _variables.entries()) {
-        // In case the user already decided to put values in the variables (before onInit), let's make sure onInit does not override this.
-        if (!variables.has(name)) {
-            variables.set(name, value);
-        }
-    }
-};
-
-setVariableResolvers.subscribe((event) => {
-    const oldValue = variables.get(event.key);
-    // If we are setting the same value, no need to do anything.
-    // No need to do this check since it is already performed in SharedVariablesManager
-    /*if (JSON.stringify(oldValue) === JSON.stringify(event.value)) {
-        return;
-    }*/
-
-    variables.set(event.key, event.value);
-    const subject = variableSubscribers.get(event.key);
-    if (subject !== undefined) {
-        subject.next(event.value);
-    }
-});
-
 export class WorkadventureStateCommands extends IframeApiContribution<WorkadventureStateCommands> {
+    private setVariableResolvers = new Subject<SetVariableEvent>();
+    private variables = new Map<string, unknown>();
+    private variableSubscribers = new Map<string, Subject<unknown>>();
+
+    constructor(private target: "global" | "player") {
+        super();
+
+        this.setVariableResolvers.subscribe((event) => {
+            const oldValue = this.variables.get(event.key);
+            // If we are setting the same value, no need to do anything.
+            // No need to do this check since it is already performed in SharedVariablesManager
+            /*if (JSON.stringify(oldValue) === JSON.stringify(event.value)) {
+                return;
+            }*/
+
+            this.variables.set(event.key, event.value);
+            const subject = this.variableSubscribers.get(event.key);
+            if (subject !== undefined) {
+                subject.next(event.value);
+            }
+        });
+    }
+
     callbacks = [
         apiCallback({
             type: "setVariable",
             typeChecker: isSetVariableEvent,
             callback: (payloadData) => {
-                setVariableResolvers.next(payloadData);
+                if (payloadData.target === this.target) {
+                    this.setVariableResolvers.next(payloadData);
+                }
             },
         }),
     ];
 
+    // TODO: see how we can remove this method from types exposed to WA.state object
+    initVariables(_variables: Map<string, unknown>): void {
+        for (const [name, value] of _variables.entries()) {
+            // In case the user already decided to put values in the variables (before onInit), let's make sure onInit does not override this.
+            if (!this.variables.has(name)) {
+                this.variables.set(name, value);
+            }
+        }
+    }
+
     saveVariable(key: string, value: unknown): Promise<void> {
-        variables.set(key, value);
+        this.variables.set(key, value);
         return queryWorkadventure({
             type: "setVariable",
             data: {
                 key,
                 value,
+                target: this.target,
             },
         });
     }
 
     loadVariable(key: string): unknown {
-        return variables.get(key);
+        return this.variables.get(key);
     }
 
     hasVariable(key: string): boolean {
-        return variables.has(key);
+        return this.variables.has(key);
     }
 
     onVariableChange(key: string): Observable<unknown> {
-        let subject = variableSubscribers.get(key);
+        let subject = this.variableSubscribers.get(key);
         if (subject === undefined) {
             subject = new Subject<unknown>();
-            variableSubscribers.set(key, subject);
+            this.variableSubscribers.set(key, subject);
         }
         return subject.asObservable();
     }
 }
 
-const proxyCommand = new Proxy(new WorkadventureStateCommands(), {
-    get(target: WorkadventureStateCommands, p: PropertyKey, receiver: unknown): unknown {
-        if (p in target) {
-            return Reflect.get(target, p, receiver);
-        }
-        return target.loadVariable(p.toString());
-    },
-    set(target: WorkadventureStateCommands, p: PropertyKey, value: unknown, receiver: unknown): boolean {
-        // Note: when using "set", there is no way to wait, so we ignore the return of the promise.
-        // User must use WA.state.saveVariable to have error message.
-        target.saveVariable(p.toString(), value);
-        return true;
-    },
-    has(target: WorkadventureStateCommands, p: PropertyKey): boolean {
-        if (p in target) {
+export function createState(target: "global" | "player"): WorkadventureStateCommands & { [key: string]: unknown } {
+    return new Proxy(new WorkadventureStateCommands(target), {
+        get(target: WorkadventureStateCommands, p: PropertyKey, receiver: unknown): unknown {
+            if (p in target) {
+                return Reflect.get(target, p, receiver);
+            }
+            return target.loadVariable(p.toString());
+        },
+        set(target: WorkadventureStateCommands, p: PropertyKey, value: unknown, receiver: unknown): boolean {
+            // Note: when using "set", there is no way to wait, so we ignore the return of the promise.
+            // User must use WA.state.saveVariable to have error message.
+            target.saveVariable(p.toString(), value).catch((e) => console.error(e));
             return true;
-        }
-        return target.hasVariable(p.toString());
-    },
-}) as WorkadventureStateCommands & { [key: string]: unknown };
-
-export default proxyCommand;
+        },
+        has(target: WorkadventureStateCommands, p: PropertyKey): boolean {
+            if (p in target) {
+                return true;
+            }
+            return target.hasVariable(p.toString());
+        },
+    }) as WorkadventureStateCommands & { [key: string]: unknown };
+}
diff --git a/front/src/Api/iframe/website.ts b/front/src/Api/iframe/website.ts
index 28abb19a..eab1bce3 100644
--- a/front/src/Api/iframe/website.ts
+++ b/front/src/Api/iframe/website.ts
@@ -1,8 +1,4 @@
-import type { LoadSoundEvent } from "../Events/LoadSoundEvent";
-import type { PlaySoundEvent } from "../Events/PlaySoundEvent";
-import type { StopSoundEvent } from "../Events/StopSoundEvent";
 import { IframeApiContribution, queryWorkadventure, sendToWorkadventure } from "./IframeApiContribution";
-import { Sound } from "./Sound/Sound";
 import { EmbeddedWebsite } from "./Room/EmbeddedWebsite";
 import type { CreateEmbeddedWebsiteEvent } from "../Events/EmbeddedWebsiteEvent";
 
diff --git a/front/src/Components/App.svelte b/front/src/Components/App.svelte
index e9db2b77..a1277ed2 100644
--- a/front/src/Components/App.svelte
+++ b/front/src/Components/App.svelte
@@ -23,6 +23,9 @@
     import { chatVisibilityStore } from "../Stores/ChatStore";
     import { helpCameraSettingsVisibleStore } from "../Stores/HelpCameraSettingsStore";
     import HelpCameraSettingsPopup from "./HelpCameraSettings/HelpCameraSettingsPopup.svelte";
+    import { showLimitRoomModalStore, showShareLinkMapModalStore } from "../Stores/ModalStore";
+    import LimitRoomModal from "./Modal/LimitRoomModal.svelte";
+    import ShareLinkMapModal from "./Modal/ShareLinkMapModal.svelte";
     import AudioPlaying from "./UI/AudioPlaying.svelte";
     import { soundPlayingStore } from "../Stores/SoundPlayingStore";
     import ErrorDialog from "./UI/ErrorDialog.svelte";
@@ -136,6 +139,16 @@
             <HelpCameraSettingsPopup />
         </div>
     {/if}
+    {#if $showLimitRoomModalStore}
+        <div>
+            <LimitRoomModal />
+        </div>
+    {/if}
+    {#if $showShareLinkMapModalStore}
+        <div>
+            <ShareLinkMapModal />
+        </div>
+    {/if}
     {#if $requestVisitCardsStore}
         <VisitCard visitCardUrl={$requestVisitCardsStore} />
     {/if}
diff --git a/front/src/Components/AudioManager/AudioManager.svelte b/front/src/Components/AudioManager/AudioManager.svelte
index 4422d002..b62d8fbe 100644
--- a/front/src/Components/AudioManager/AudioManager.svelte
+++ b/front/src/Components/AudioManager/AudioManager.svelte
@@ -19,12 +19,13 @@
         audioManagerVolumeStore.setVolume(volume);
         audioManagerVolumeStore.setMuted(localUserStore.getAudioPlayerMuted());
 
-        unsubscriberFileStore = audioManagerFileStore.subscribe(() => {
+        unsubscriberFileStore = audioManagerFileStore.subscribe((src) => {
             HTMLAudioPlayer.pause();
+            HTMLAudioPlayer.src = src;
             HTMLAudioPlayer.loop = get(audioManagerVolumeStore).loop;
             HTMLAudioPlayer.volume = get(audioManagerVolumeStore).volume;
             HTMLAudioPlayer.muted = get(audioManagerVolumeStore).muted;
-            HTMLAudioPlayer.play();
+            void HTMLAudioPlayer.play();
         });
         unsubscriberVolumeStore = audioManagerVolumeStore.subscribe((audioManager: audioManagerVolume) => {
             const reduceVolume = audioManager.talking && audioManager.decreaseWhileTalking;
@@ -148,9 +149,7 @@
         </label>
         <section class="audio-manager-file">
             <!-- svelte-ignore a11y-media-has-caption -->
-            <audio class="audio-manager-audioplayer" bind:this={HTMLAudioPlayer}>
-                <source src={$audioManagerFileStore} />
-            </audio>
+            <audio class="audio-manager-audioplayer" bind:this={HTMLAudioPlayer} />
         </section>
     </div>
 </div>
diff --git a/front/src/Components/Chat/ChatElement.svelte b/front/src/Components/Chat/ChatElement.svelte
index 231e7260..4ac9c275 100644
--- a/front/src/Components/Chat/ChatElement.svelte
+++ b/front/src/Components/Chat/ChatElement.svelte
@@ -67,6 +67,7 @@
         .messagePart {
             flex-grow: 1;
             max-width: 100%;
+            user-select: text;
 
             span.date {
                 font-size: 80%;
diff --git a/front/src/Components/FollowMenu/FollowMenu.svelte b/front/src/Components/FollowMenu/FollowMenu.svelte
index 264b27ed..aaa23b3e 100644
--- a/front/src/Components/FollowMenu/FollowMenu.svelte
+++ b/front/src/Components/FollowMenu/FollowMenu.svelte
@@ -14,14 +14,11 @@ vim: ft=typescript
     }
 
     function sendFollowRequest() {
-        gameScene.connection?.emitFollowRequest();
-        followRoleStore.set("leader");
-        followStateStore.set("active");
+        gameScene.CurrentPlayer.sendFollowRequest();
     }
 
     function acceptFollowRequest() {
-        gameScene.CurrentPlayer.enableFollowing();
-        gameScene.connection?.emitFollowConfirmation();
+        gameScene.CurrentPlayer.startFollowing();
     }
 
     function abortEnding() {
@@ -42,23 +39,15 @@ vim: ft=typescript
 
 <svelte:window on:keydown={onKeyDown} />
 
-{#if $followStateStore === "requesting"}
+{#if $followStateStore === "requesting" && $followRoleStore === "follower"}
     <div class="interact-menu nes-container is-rounded">
-        {#if $followRoleStore === "follower"}
-            <section class="interact-menu-title">
-                <h2>Do you want to follow {name($followUsersStore[0])}?</h2>
-            </section>
-            <section class="interact-menu-action">
-                <button type="button" class="nes-btn is-success" on:click|preventDefault={acceptFollowRequest}
-                    >Yes</button
-                >
-                <button type="button" class="nes-btn is-error" on:click|preventDefault={reset}>No</button>
-            </section>
-        {:else if $followRoleStore === "leader"}
-            <section class="interact-menu-question">
-                <p>Should never be displayed</p>
-            </section>
-        {/if}
+        <section class="interact-menu-title">
+            <h2>Do you want to follow {name($followUsersStore[0])}?</h2>
+        </section>
+        <section class="interact-menu-action">
+            <button type="button" class="nes-btn is-success" on:click|preventDefault={acceptFollowRequest}>Yes</button>
+            <button type="button" class="nes-btn is-error" on:click|preventDefault={reset}>No</button>
+        </section>
     </div>
 {/if}
 
diff --git a/front/src/Components/Menu/GlobalMessagesSubMenu.svelte b/front/src/Components/Menu/GlobalMessagesSubMenu.svelte
index 524e5e50..e755a243 100644
--- a/front/src/Components/Menu/GlobalMessagesSubMenu.svelte
+++ b/front/src/Components/Menu/GlobalMessagesSubMenu.svelte
@@ -19,12 +19,12 @@
         uploadAudioActive = true;
     }
 
-    function send() {
+    async function send(): Promise<void> {
         if (inputSendTextActive) {
-            handleSendText.sendTextMessage(broadcastToWorld);
+            return handleSendText.sendTextMessage(broadcastToWorld);
         }
         if (uploadAudioActive) {
-            handleSendAudio.sendAudioMessage(broadcastToWorld);
+            return handleSendAudio.sendAudioMessage(broadcastToWorld);
         }
     }
 </script>
diff --git a/front/src/Components/Menu/GuestSubMenu.svelte b/front/src/Components/Menu/GuestSubMenu.svelte
index 0ae25b75..408dcbce 100644
--- a/front/src/Components/Menu/GuestSubMenu.svelte
+++ b/front/src/Components/Menu/GuestSubMenu.svelte
@@ -21,12 +21,12 @@
 <div class="guest-main">
     <section class="container-overflow">
         <section class="share-url not-mobile">
-            <h3>Share the link of the room !</h3>
+            <h3>Share the link of the room!</h3>
             <input type="text" readonly id="input-share-link" value={location.toString()} />
             <button type="button" class="nes-btn is-primary" on:click={copyLink}>Copy</button>
         </section>
         <section class="is-mobile">
-            <h3>Share the link of the room !</h3>
+            <h3>Share the link of the room!</h3>
             <input type="hidden" readonly id="input-share-link" value={location.toString()} />
             <button type="button" class="nes-btn is-primary" on:click={shareLink}>Share</button>
         </section>
diff --git a/front/src/Components/Menu/MenuIcon.svelte b/front/src/Components/Menu/MenuIcon.svelte
index bb5a2df2..9d3f72dd 100644
--- a/front/src/Components/Menu/MenuIcon.svelte
+++ b/front/src/Components/Menu/MenuIcon.svelte
@@ -1,9 +1,14 @@
 <script lang="typescript">
     import logoTalk from "../images/logo-message-pixel.png";
     import logoWA from "../images/logo-WA-pixel.png";
+    import logoInvite from "../images/logo-invite-pixel.png";
+    import logoRegister from "../images/logo-register-pixel.png";
     import { menuVisiblilityStore } from "../../Stores/MenuStore";
     import { chatVisibilityStore } from "../../Stores/ChatStore";
+    import { limitMapStore } from "../../Stores/GameStore";
     import { get } from "svelte/store";
+    import { ADMIN_URL } from "../../Enum/EnvironmentVariable";
+    import { showShareLinkMapModalStore } from "../../Stores/ModalStore";
 
     function showMenu() {
         menuVisiblilityStore.set(!get(menuVisiblilityStore));
@@ -11,13 +16,25 @@
     function showChat() {
         chatVisibilityStore.set(true);
     }
+
+    function register() {
+        window.open(`${ADMIN_URL}/second-step-register`, "_self");
+    }
+    function showInvite() {
+        showShareLinkMapModalStore.set(true);
+    }
 </script>
 
 <svelte:window />
 
 <main class="menuIcon">
-    <img src={logoWA} alt="open menu" class="nes-pointer" on:click|preventDefault={showMenu} />
-    <img src={logoTalk} alt="open menu" class="nes-pointer" on:click|preventDefault={showChat} />
+    {#if $limitMapStore}
+        <img src={logoInvite} alt="open menu" class="nes-pointer" on:click|preventDefault={showInvite} />
+        <img src={logoRegister} alt="open menu" class="nes-pointer" on:click|preventDefault={register} />
+    {:else}
+        <img src={logoWA} alt="open menu" class="nes-pointer" on:click|preventDefault={showMenu} />
+        <img src={logoTalk} alt="open menu" class="nes-pointer" on:click|preventDefault={showChat} />
+    {/if}
 </main>
 
 <style lang="scss">
diff --git a/front/src/Components/Menu/ProfileSubMenu.svelte b/front/src/Components/Menu/ProfileSubMenu.svelte
index 07356f6c..87bf57c9 100644
--- a/front/src/Components/Menu/ProfileSubMenu.svelte
+++ b/front/src/Components/Menu/ProfileSubMenu.svelte
@@ -41,10 +41,10 @@
         gameManager.leaveGame(SelectCharacterSceneName, new SelectCharacterScene());
     }
 
-    function logOut() {
+    async function logOut() {
         disableMenuStores();
         loginSceneVisibleStore.set(true);
-        connectionManager.logout();
+        return connectionManager.logout();
     }
 
     function getProfileUrl() {
diff --git a/front/src/Components/Menu/SettingsSubMenu.svelte b/front/src/Components/Menu/SettingsSubMenu.svelte
index 1db14036..1ad1ac8b 100644
--- a/front/src/Components/Menu/SettingsSubMenu.svelte
+++ b/front/src/Components/Menu/SettingsSubMenu.svelte
@@ -33,9 +33,9 @@
         const body = HtmlUtils.querySelectorOrFail("body");
         if (body) {
             if (document.fullscreenElement !== null && !fullscreen) {
-                document.exitFullscreen();
+                document.exitFullscreen().catch((e) => console.error(e));
             } else {
-                body.requestFullscreen();
+                body.requestFullscreen().catch((e) => console.error(e));
             }
             localUserStore.setFullscreen(fullscreen);
         }
@@ -45,14 +45,16 @@
         if (Notification.permission === "granted") {
             localUserStore.setNotification(notification ? "granted" : "denied");
         } else {
-            Notification.requestPermission().then((response) => {
-                if (response === "granted") {
-                    localUserStore.setNotification(notification ? "granted" : "denied");
-                } else {
-                    localUserStore.setNotification("denied");
-                    notification = false;
-                }
-            });
+            Notification.requestPermission()
+                .then((response) => {
+                    if (response === "granted") {
+                        localUserStore.setNotification(notification ? "granted" : "denied");
+                    } else {
+                        localUserStore.setNotification("denied");
+                        notification = false;
+                    }
+                })
+                .catch((e) => console.error(e));
         }
     }
 
diff --git a/front/src/Components/Modal/LimitRoomModal.svelte b/front/src/Components/Modal/LimitRoomModal.svelte
new file mode 100644
index 00000000..43941a6b
--- /dev/null
+++ b/front/src/Components/Modal/LimitRoomModal.svelte
@@ -0,0 +1,47 @@
+<script lang="typescript">
+    import { fly } from "svelte/transition";
+    import { ADMIN_URL } from "../../Enum/EnvironmentVariable";
+
+    function register() {
+        window.open(`${ADMIN_URL}/second-step-register`, "_self");
+    }
+</script>
+
+<div class="limit-map nes-container" transition:fly={{ y: -900, duration: 500 }}>
+    <section>
+        <h2>Limit of your room</h2>
+        <p>Register your account!</p>
+        <p>
+            This map is limited in the time and to continue to use WorkAdventure, you must register your account in our
+            back office.
+        </p>
+    </section>
+    <section>
+        <button class="nes-btn is-primary" on:click|preventDefault={register}>Register</button>
+    </section>
+</div>
+
+<style lang="scss">
+    .limit-map {
+        pointer-events: auto;
+        background: #eceeee;
+        margin-left: auto;
+        margin-right: auto;
+        margin-top: 10vh;
+        max-height: 80vh;
+        max-width: 80vw;
+        overflow: auto;
+        text-align: center;
+
+        h2 {
+            font-family: "Press Start 2P";
+        }
+
+        section {
+            p {
+                margin: 15px;
+                font-family: "Press Start 2P";
+            }
+        }
+    }
+</style>
diff --git a/front/src/Components/Modal/ShareLinkMapModal.svelte b/front/src/Components/Modal/ShareLinkMapModal.svelte
new file mode 100644
index 00000000..42ac1294
--- /dev/null
+++ b/front/src/Components/Modal/ShareLinkMapModal.svelte
@@ -0,0 +1,90 @@
+<script lang="typescript">
+    import { fly } from "svelte/transition";
+    import { showShareLinkMapModalStore } from "../../Stores/ModalStore";
+
+    interface ExtNavigator extends Navigator {
+        canShare?(data?: ShareData): Promise<boolean>;
+    }
+
+    const myNavigator: ExtNavigator = window.navigator;
+    const haveNavigatorSharingFeature: boolean =
+        myNavigator && myNavigator.canShare != null && myNavigator.share != null;
+
+    let copied: boolean = false;
+
+    function copyLink() {
+        try {
+            const input: HTMLInputElement = document.getElementById("input-share-link") as HTMLInputElement;
+            input.focus();
+            input.select();
+            document.execCommand("copy");
+            copied = true;
+        } catch (e) {
+            console.error(e);
+            copied = false;
+        }
+    }
+
+    async function shareLink() {
+        const shareData = { url: location.toString() };
+
+        try {
+            await myNavigator.share(shareData);
+        } catch (err) {
+            console.error("Error: " + err);
+            copyLink();
+        }
+    }
+
+    function close() {
+        showShareLinkMapModalStore.set(false);
+        copied = false;
+    }
+</script>
+
+<div class="share-link-map nes-container" transition:fly={{ y: -900, duration: 500 }}>
+    <section>
+        <h2>Invite your friends or colleagues</h2>
+        <p>Share the link of the room!</p>
+    </section>
+    <section>
+        {#if haveNavigatorSharingFeature}
+            <input type="hidden" readonly id="input-share-link" value={location.toString()} />
+            <button type="button" class="nes-btn is-primary" on:click={shareLink}>Share</button>
+        {:else}
+            <input type="text" readonly id="input-share-link" value={location.toString()} />
+            <button type="button" class="nes-btn is-primary" on:click={copyLink}>Copy</button>
+        {/if}
+        {#if copied}
+            <p>Copied!</p>
+        {/if}
+    </section>
+    <section>
+        <button class="nes-btn" on:click|preventDefault={close}>Close</button>
+    </section>
+</div>
+
+<style lang="scss">
+    div.share-link-map {
+        pointer-events: auto;
+        background: #eceeee;
+        margin-left: auto;
+        margin-right: auto;
+        margin-top: 10vh;
+        max-height: 80vh;
+        max-width: 80vw;
+        overflow: auto;
+        text-align: center;
+
+        h2 {
+            font-family: "Press Start 2P";
+        }
+
+        section {
+            p {
+                margin: 15px;
+                font-family: "Press Start 2P";
+            }
+        }
+    }
+</style>
diff --git a/front/src/Components/ReportMenu/ReportMenu.svelte b/front/src/Components/ReportMenu/ReportMenu.svelte
index 8479a5b0..92601774 100644
--- a/front/src/Components/ReportMenu/ReportMenu.svelte
+++ b/front/src/Components/ReportMenu/ReportMenu.svelte
@@ -6,12 +6,11 @@
     import type { Unsubscriber } from "svelte/store";
     import { playersStore } from "../../Stores/PlayersStore";
     import { connectionManager } from "../../Connexion/ConnectionManager";
-    import { GameConnexionTypes } from "../../Url/UrlManager";
     import { get } from "svelte/store";
 
     let blockActive = true;
     let reportActive = !blockActive;
-    let anonymous: boolean = false;
+    let disableReport: boolean = false;
     let userUUID: string | undefined = playersStore.getPlayerById(get(showReportScreenStore).userId)?.userUuid;
     let userName = "No name";
     let unsubscriber: Unsubscriber;
@@ -26,7 +25,7 @@
                 }
             }
         });
-        anonymous = connectionManager.getConnexionType === GameConnexionTypes.anonymous;
+        disableReport = !connectionManager.currentRoom?.canReport ?? true;
     });
 
     onDestroy(() => {
@@ -65,7 +64,7 @@
             <button type="button" class="nes-btn" on:click|preventDefault={close}>X</button>
         </section>
     </section>
-    <section class="report-menu-action {anonymous ? 'hidden' : ''}">
+    <section class="report-menu-action {disableReport ? 'hidden' : ''}">
         <section class="justify-center">
             <button
                 type="button"
diff --git a/front/src/Components/UI/AudioPlaying.svelte b/front/src/Components/UI/AudioPlaying.svelte
index 09ffd639..a8d12ec9 100644
--- a/front/src/Components/UI/AudioPlaying.svelte
+++ b/front/src/Components/UI/AudioPlaying.svelte
@@ -12,7 +12,7 @@
     }
 
     afterUpdate(() => {
-        audio.play();
+        audio.play().catch((e) => console.error(e));
     });
 </script>
 
diff --git a/front/src/Components/WarningContainer/WarningContainer.svelte b/front/src/Components/WarningContainer/WarningContainer.svelte
index dd61d8fc..f75050fc 100644
--- a/front/src/Components/WarningContainer/WarningContainer.svelte
+++ b/front/src/Components/WarningContainer/WarningContainer.svelte
@@ -1,20 +1,26 @@
 <script lang="typescript">
     import { fly } from "svelte/transition";
-    import { userIsAdminStore } from "../../Stores/GameStore";
+    import { userIsAdminStore, limitMapStore } from "../../Stores/GameStore";
     import { ADMIN_URL } from "../../Enum/EnvironmentVariable";
 
     const upgradeLink = ADMIN_URL + "/pricing";
+    const registerLink = ADMIN_URL + "/second-step-register";
 </script>
 
 <main class="warningMain" transition:fly={{ y: -200, duration: 500 }}>
-    <h2>Warning!</h2>
     {#if $userIsAdminStore}
+        <h2>Warning!</h2>
         <p>
             This world is close to its limit!. You can upgrade its capacity <a href={upgradeLink} target="_blank"
                 >here</a
             >
         </p>
+    {:else if $limitMapStore}
+        <p>
+            This map is available for 2 days. You can register your domain <a href={registerLink}>here</a>!
+        </p>
     {:else}
+        <h2>Warning!</h2>
         <p>This world is close to its limit!</p>
     {/if}
 </main>
diff --git a/front/src/Components/images/logo-invite-pixel.png b/front/src/Components/images/logo-invite-pixel.png
new file mode 100644
index 00000000..d8ae6fff
Binary files /dev/null and b/front/src/Components/images/logo-invite-pixel.png differ
diff --git a/front/src/Components/images/logo-register-pixel.png b/front/src/Components/images/logo-register-pixel.png
new file mode 100644
index 00000000..f9a45d3c
Binary files /dev/null and b/front/src/Components/images/logo-register-pixel.png differ
diff --git a/front/src/Connexion/AdminMessagesService.ts b/front/src/Connexion/AdminMessagesService.ts
index 0b217760..4b7030ed 100644
--- a/front/src/Connexion/AdminMessagesService.ts
+++ b/front/src/Connexion/AdminMessagesService.ts
@@ -1,5 +1,5 @@
 import { Subject } from "rxjs";
-import type { BanUserMessage, SendUserMessage } from "../Messages/generated/messages_pb";
+import type { BanUserMessage, SendUserMessage } from "../Messages/ts-proto-generated/messages";
 
 export enum AdminMessageEventTypes {
     admin = "message",
@@ -26,8 +26,8 @@ class AdminMessagesService {
 
     onSendusermessage(message: SendUserMessage | BanUserMessage) {
         this._messageStream.next({
-            type: message.getType() as unknown as AdminMessageEventTypes,
-            text: message.getMessage(),
+            type: message.type as unknown as AdminMessageEventTypes,
+            text: message.message,
         });
     }
 }
diff --git a/front/src/Connexion/ConnectionManager.ts b/front/src/Connexion/ConnectionManager.ts
index 026cc20a..9c81fb25 100644
--- a/front/src/Connexion/ConnectionManager.ts
+++ b/front/src/Connexion/ConnectionManager.ts
@@ -8,12 +8,14 @@ import { CharacterTexture, LocalUser } from "./LocalUser";
 import { Room } from "./Room";
 import { _ServiceWorker } from "../Network/ServiceWorker";
 import { loginSceneVisibleIframeStore } from "../Stores/LoginSceneStore";
-import { userIsConnected } from "../Stores/MenuStore";
+import { userIsConnected, warningContainerStore } from "../Stores/MenuStore";
 import { analyticsClient } from "../Administration/AnalyticsClient";
 import { axiosWithRetry } from "./AxiosUtils";
 import axios from "axios";
 import { isRegisterData } from "../Messages/JsonMessages/RegisterData";
 import { isAdminApiData } from "../Messages/JsonMessages/AdminApiData";
+import { limitMapStore } from "../Stores/GameStore";
+import { showLimitRoomModalStore } from "../Stores/ModalStore";
 
 class ConnectionManager {
     private localUser!: LocalUser;
@@ -152,11 +154,7 @@ class ConnectionManager {
                 )
             );
             urlManager.pushRoomIdToUrl(this._currentRoom);
-        } else if (
-            connexionType === GameConnexionTypes.organization ||
-            connexionType === GameConnexionTypes.anonymous ||
-            connexionType === GameConnexionTypes.empty
-        ) {
+        } else if (connexionType === GameConnexionTypes.room || connexionType === GameConnexionTypes.empty) {
             this.authToken = localUserStore.getAuthToken();
 
             let roomPath: string;
@@ -188,7 +186,7 @@ class ConnectionManager {
 
             //Set last room visited! (connected or nor, must to be saved in localstorage and cache API)
             //use href to keep # value
-            localUserStore.setLastRoomUrl(this._currentRoom.href);
+            await localUserStore.setLastRoomUrl(this._currentRoom.href);
 
             //todo: add here some kind of warning if authToken has expired.
             if (!this.authToken && !this._currentRoom.authenticationMandatory) {
@@ -237,6 +235,17 @@ class ConnectionManager {
             analyticsClient.identifyUser(this.localUser.uuid, this.localUser.email);
         }
 
+        //if limit room active test headband
+        if (this._currentRoom.expireOn !== undefined) {
+            warningContainerStore.activateWarningContainer();
+            limitMapStore.set(true);
+
+            //check time of map
+            if (new Date() > this._currentRoom.expireOn) {
+                showLimitRoomModalStore.set(true);
+            }
+        }
+
         this.serviceWorker = new _ServiceWorker();
         return Promise.resolve(this._currentRoom);
     }
@@ -280,7 +289,7 @@ class ConnectionManager {
                 reject(error);
             });
 
-            connection.onConnectingError((event: CloseEvent) => {
+            connection.connectionErrorStream.subscribe((event: CloseEvent) => {
                 console.log("An error occurred while connecting to socket server. Retrying");
                 reject(
                     new Error(
@@ -292,7 +301,7 @@ class ConnectionManager {
                 );
             });
 
-            connection.onConnect((connect: OnConnectInterface) => {
+            connection.roomJoinedMessageStream.subscribe((connect: OnConnectInterface) => {
                 resolve(connect);
             });
         }).catch((err) => {
@@ -301,7 +310,7 @@ class ConnectionManager {
                 this.reconnectingTimeout = setTimeout(() => {
                     //todo: allow a way to break recursion?
                     //todo: find a way to avoid recursive function. Otherwise, the call stack will grow indefinitely.
-                    this.connectToRoomSocket(roomUrl, name, characterLayers, position, viewport, companion).then(
+                    void this.connectToRoomSocket(roomUrl, name, characterLayers, position, viewport, companion).then(
                         (connection) => resolve(connection)
                     );
                 }, 4000 + Math.floor(Math.random() * 2000));
diff --git a/front/src/Connexion/ConnexionModels.ts b/front/src/Connexion/ConnexionModels.ts
index 6200e0c9..bf834a02 100644
--- a/front/src/Connexion/ConnexionModels.ts
+++ b/front/src/Connexion/ConnexionModels.ts
@@ -1,44 +1,12 @@
 import type { SignalData } from "simple-peer";
 import type { RoomConnection } from "./RoomConnection";
 import type { BodyResourceDescriptionInterface } from "../Phaser/Entity/PlayerTextures";
-
-export enum EventMessage {
-    CONNECT = "connect",
-    WEBRTC_SIGNAL = "webrtc-signal",
-    WEBRTC_SCREEN_SHARING_SIGNAL = "webrtc-screen-sharing-signal",
-    WEBRTC_START = "webrtc-start",
-    //START_ROOM = "start-room", // From server to client: list of all room users/groups/items
-    JOIN_ROOM = "join-room", // bi-directional
-    USER_POSITION = "user-position", // From client to server
-    USER_MOVED = "user-moved", // From server to client
-    USER_LEFT = "user-left", // From server to client
-    MESSAGE_ERROR = "message-error",
-    WEBRTC_DISCONNECT = "webrtc-disconect",
-    GROUP_CREATE_UPDATE = "group-create-update",
-    GROUP_DELETE = "group-delete",
-    SET_PLAYER_DETAILS = "set-player-details", // Send the name and character to the server (on connect), receive back the id.
-    ITEM_EVENT = "item-event",
-    USER_DETAILS_UPDATED = "user-details-updated",
-
-    CONNECT_ERROR = "connect_error",
-    CONNECTING_ERROR = "connecting_error",
-    SET_SILENT = "set_silent", // Set or unset the silent mode for this user.
-    SET_VIEWPORT = "set-viewport",
-    BATCH = "batch",
-
-    PLAY_GLOBAL_MESSAGE = "play-global-message",
-    STOP_GLOBAL_MESSAGE = "stop-global-message",
-
-    TELEPORT = "teleport",
-    USER_MESSAGE = "user-message",
-    START_JITSI_ROOM = "start-jitsi-room",
-    SET_VARIABLE = "set-variable",
-}
+import { PositionMessage_Direction } from "../Messages/ts-proto-generated/messages";
 
 export interface PointInterface {
     x: number;
     y: number;
-    direction: string;
+    direction: string; // TODO: modify this to the enum from ts-proto
     moving: boolean;
 }
 
diff --git a/front/src/Connexion/EmoteEventStream.ts b/front/src/Connexion/EmoteEventStream.ts
deleted file mode 100644
index 32f1daa0..00000000
--- a/front/src/Connexion/EmoteEventStream.ts
+++ /dev/null
@@ -1,17 +0,0 @@
-import { Subject } from "rxjs";
-
-interface EmoteEvent {
-    userId: number;
-    emote: string;
-}
-
-class EmoteEventStream {
-    private _stream: Subject<EmoteEvent> = new Subject();
-    public stream = this._stream.asObservable();
-
-    fire(userId: number, emote: string) {
-        this._stream.next({ userId, emote });
-    }
-}
-
-export const emoteEventStream = new EmoteEventStream();
diff --git a/front/src/Connexion/LocalUserStore.ts b/front/src/Connexion/LocalUserStore.ts
index 4dce6924..cc84f043 100644
--- a/front/src/Connexion/LocalUserStore.ts
+++ b/front/src/Connexion/LocalUserStore.ts
@@ -22,8 +22,8 @@ const nonce = "nonce";
 const notification = "notificationPermission";
 const code = "code";
 const cameraSetup = "cameraSetup";
-
 const cacheAPIIndex = "workavdenture-cache";
+const userProperties = "user-properties";
 
 class LocalUserStore {
     saveUser(localUser: LocalUser) {
@@ -136,13 +136,12 @@ class LocalUserStore {
         return localStorage.getItem(ignoreFollowRequests) === "true";
     }
 
-    setLastRoomUrl(roomUrl: string): void {
+    async setLastRoomUrl(roomUrl: string): Promise<void> {
         localStorage.setItem(lastRoomUrl, roomUrl.toString());
         if ("caches" in window) {
-            caches.open(cacheAPIIndex).then((cache) => {
-                const stringResponse = new Response(JSON.stringify({ roomUrl }));
-                cache.put(`/${lastRoomUrl}`, stringResponse);
-            });
+            const cache = await caches.open(cacheAPIIndex);
+            const stringResponse = new Response(JSON.stringify({ roomUrl }));
+            await cache.put(`/${lastRoomUrl}`, stringResponse);
         }
     }
     getLastRoomUrl(): string {
@@ -220,6 +219,27 @@ class LocalUserStore {
         const cameraSetupValues = localStorage.getItem(cameraSetup);
         return cameraSetupValues != undefined ? JSON.parse(cameraSetupValues) : undefined;
     }
+
+    getAllUserProperties(): Map<string, unknown> {
+        const result = new Map<string, string>();
+        for (let i = 0; i < localStorage.length; i++) {
+            const key = localStorage.key(i);
+            if (key) {
+                if (key.startsWith(userProperties + "_")) {
+                    const value = localStorage.getItem(key);
+                    if (value) {
+                        const userKey = key.substr((userProperties + "_").length);
+                        result.set(userKey, JSON.parse(value));
+                    }
+                }
+            }
+        }
+        return result;
+    }
+
+    setUserProperty(name: string, value: unknown): void {
+        localStorage.setItem(userProperties + "_" + name, JSON.stringify(value));
+    }
 }
 
 export const localUserStore = new LocalUserStore();
diff --git a/front/src/Connexion/Room.ts b/front/src/Connexion/Room.ts
index e5e2227f..74dd06de 100644
--- a/front/src/Connexion/Room.ts
+++ b/front/src/Connexion/Room.ts
@@ -18,7 +18,10 @@ export interface RoomRedirect {
 
 export class Room {
     public readonly id: string;
-    public readonly isPublic: boolean;
+    /**
+     * @deprecated
+     */
+    private readonly isPublic: boolean;
     private _authenticationMandatory: boolean = DISABLE_ANONYMOUS;
     private _iframeAuthentication?: string = OPID_LOGIN_SCREEN_PROVIDER;
     private _mapUrl: string | undefined;
@@ -27,6 +30,8 @@ export class Room {
     private readonly _search: URLSearchParams;
     private _contactPage: string | undefined;
     private _group: string | null = null;
+    private _expireOn: Date | undefined;
+    private _canReport: boolean = false;
 
     private constructor(private roomUrl: URL) {
         this.id = roomUrl.pathname;
@@ -34,7 +39,7 @@ export class Room {
         if (this.id.startsWith("/")) {
             this.id = this.id.substr(1);
         }
-        if (this.id.startsWith("_/")) {
+        if (this.id.startsWith("_/") || this.id.startsWith("*/")) {
             this.isPublic = true;
         } else if (this.id.startsWith("@/")) {
             this.isPublic = false;
@@ -121,6 +126,10 @@ export class Room {
                     data.authenticationMandatory != null ? data.authenticationMandatory : DISABLE_ANONYMOUS;
                 this._iframeAuthentication = data.iframeAuthentication || OPID_LOGIN_SCREEN_PROVIDER;
                 this._contactPage = data.contactPage || CONTACT_URL;
+                if (data.expireOn) {
+                    this._expireOn = new Date(data.expireOn);
+                }
+                this._canReport = data.canReport ?? false;
                 return new MapDetail(data.mapUrl, data.textures);
             } else {
                 throw new Error("Data received by the /map endpoint of the Pusher is not in a valid format.");
@@ -143,6 +152,8 @@ export class Room {
      * Instance name is:
      * - In a public URL: the second part of the URL ( _/[instance]/map.json)
      * - In a private URL: [organizationId/worldId]
+     *
+     * @deprecated
      */
     public getInstance(): string {
         if (this.instance !== undefined) {
@@ -150,7 +161,7 @@ export class Room {
         }
 
         if (this.isPublic) {
-            const match = /_\/([^/]+)\/.+/.exec(this.id);
+            const match = /[_*]\/([^/]+)\/.+/.exec(this.id);
             if (!match) throw new Error('Could not extract instance from "' + this.id + '"');
             this.instance = match[1];
             return this.instance;
@@ -222,4 +233,12 @@ export class Room {
     get group(): string | null {
         return this._group;
     }
+
+    get expireOn(): Date | undefined {
+        return this._expireOn;
+    }
+
+    get canReport(): boolean {
+        return this._canReport;
+    }
 }
diff --git a/front/src/Connexion/RoomConnection.ts b/front/src/Connexion/RoomConnection.ts
index 328f1aec..d360c705 100644
--- a/front/src/Connexion/RoomConnection.ts
+++ b/front/src/Connexion/RoomConnection.ts
@@ -1,50 +1,9 @@
 import { PUSHER_URL, UPLOADER_URL } from "../Enum/EnvironmentVariable";
 import Axios from "axios";
-import {
-    BatchMessage,
-    ClientToServerMessage,
-    GroupDeleteMessage,
-    GroupUpdateMessage,
-    ItemEventMessage,
-    PlayGlobalMessage,
-    PositionMessage,
-    RoomJoinedMessage,
-    ServerToClientMessage,
-    SetPlayerDetailsMessage,
-    SilentMessage,
-    StopGlobalMessage,
-    UserJoinedMessage,
-    UserLeftMessage,
-    UserMovedMessage,
-    UserMovesMessage,
-    ViewportMessage,
-    WebRtcDisconnectMessage,
-    WebRtcSignalToClientMessage,
-    WebRtcSignalToServerMessage,
-    WebRtcStartMessage,
-    ReportPlayerMessage,
-    TeleportMessageMessage,
-    QueryJitsiJwtMessage,
-    SendJitsiJwtMessage,
-    CharacterLayerMessage,
-    PingMessage,
-    EmoteEventMessage,
-    EmotePromptMessage,
-    FollowRequestMessage,
-    FollowConfirmationMessage,
-    FollowAbortMessage,
-    SendUserMessage,
-    BanUserMessage,
-    VariableMessage,
-    ErrorMessage,
-    PlayerDetailsUpdatedMessage,
-} from "../Messages/generated/messages_pb";
 
 import type { UserSimplePeerInterface } from "../WebRtc/SimplePeer";
-import Direction = PositionMessage.Direction;
 import { ProtobufClientUtils } from "../Network/ProtobufClientUtils";
-import {
-    EventMessage,
+import type {
     GroupCreatedUpdatedMessageInterface,
     ItemEventMessageInterface,
     MessageUserJoined,
@@ -59,13 +18,44 @@ import {
 } from "./ConnexionModels";
 import type { BodyResourceDescriptionInterface } from "../Phaser/Entity/PlayerTextures";
 import { adminMessagesService } from "./AdminMessagesService";
-import { worldFullMessageStream } from "./WorldFullMessageStream";
 import { connectionManager } from "./ConnectionManager";
-import { emoteEventStream } from "./EmoteEventStream";
 import { get } from "svelte/store";
 import { warningContainerStore } from "../Stores/MenuStore";
 import { followStateStore, followRoleStore, followUsersStore } from "../Stores/FollowStore";
 import { localUserStore } from "./LocalUserStore";
+import {
+    RefreshRoomMessage,
+    ServerToClientMessage as ServerToClientMessageTsProto,
+    TokenExpiredMessage,
+    WorldConnexionMessage,
+    WorldFullMessage,
+    ErrorMessage as ErrorMessageTsProto,
+    UserMovedMessage as UserMovedMessageTsProto,
+    GroupUpdateMessage as GroupUpdateMessageTsProto,
+    GroupDeleteMessage as GroupDeleteMessageTsProto,
+    UserJoinedMessage as UserJoinedMessageTsProto,
+    UserLeftMessage as UserLeftMessageTsProto,
+    ItemEventMessage as ItemEventMessageTsProto,
+    EmoteEventMessage as EmoteEventMessageTsProto,
+    VariableMessage as VariableMessageTsProto,
+    PlayerDetailsUpdatedMessage as PlayerDetailsUpdatedMessageTsProto,
+    WorldFullWarningMessage,
+    WebRtcDisconnectMessage as WebRtcDisconnectMessageTsProto,
+    PlayGlobalMessage as PlayGlobalMessageTsProto,
+    StopGlobalMessage as StopGlobalMessageTsProto,
+    SendJitsiJwtMessage as SendJitsiJwtMessageTsProto,
+    SendUserMessage as SendUserMessageTsProto,
+    BanUserMessage as BanUserMessageTsProto,
+    ClientToServerMessage as ClientToServerMessageTsProto,
+    PositionMessage as PositionMessageTsProto,
+    ViewportMessage as ViewportMessageTsProto,
+    PositionMessage_Direction,
+    SetPlayerDetailsMessage as SetPlayerDetailsMessageTsProto,
+    PingMessage as PingMessageTsProto,
+} from "../Messages/ts-proto-generated/messages";
+import { Subject } from "rxjs";
+import { OpenPopupEvent } from "../Api/Events/OpenPopupEvent";
+import { match } from "assert";
 
 const manualPingDelay = 20000;
 
@@ -78,6 +68,79 @@ export class RoomConnection implements RoomConnection {
     private tags: string[] = [];
     private _userRoomToken: string | undefined;
 
+    private readonly _errorMessageStream = new Subject<ErrorMessageTsProto>();
+    public readonly errorMessageStream = this._errorMessageStream.asObservable();
+
+    private readonly _roomJoinedMessageStream = new Subject<{
+        connection: RoomConnection;
+        room: RoomJoinedMessageInterface;
+    }>();
+    public readonly roomJoinedMessageStream = this._roomJoinedMessageStream.asObservable();
+
+    private readonly _webRtcStartMessageStream = new Subject<UserSimplePeerInterface>();
+    public readonly webRtcStartMessageStream = this._webRtcStartMessageStream.asObservable();
+
+    private readonly _webRtcSignalToClientMessageStream = new Subject<WebRtcSignalReceivedMessageInterface>();
+    public readonly webRtcSignalToClientMessageStream = this._webRtcSignalToClientMessageStream.asObservable();
+
+    private readonly _webRtcScreenSharingSignalToClientMessageStream =
+        new Subject<WebRtcSignalReceivedMessageInterface>();
+    public readonly webRtcScreenSharingSignalToClientMessageStream =
+        this._webRtcScreenSharingSignalToClientMessageStream.asObservable();
+
+    private readonly _webRtcDisconnectMessageStream = new Subject<WebRtcDisconnectMessageTsProto>();
+    public readonly webRtcDisconnectMessageStream = this._webRtcDisconnectMessageStream.asObservable();
+
+    private readonly _teleportMessageMessageStream = new Subject<string>();
+    public readonly teleportMessageMessageStream = this._teleportMessageMessageStream.asObservable();
+
+    private readonly _sendJitsiJwtMessageStream = new Subject<SendJitsiJwtMessageTsProto>();
+    public readonly sendJitsiJwtMessageStream = this._sendJitsiJwtMessageStream.asObservable();
+
+    private readonly _worldFullMessageStream = new Subject<string | null>();
+    public readonly worldFullMessageStream = this._worldFullMessageStream.asObservable();
+
+    private readonly _worldConnexionMessageStream = new Subject<WorldConnexionMessage>();
+    public readonly worldConnexionMessageStream = this._worldConnexionMessageStream.asObservable();
+
+    private readonly _tokenExpiredMessageStream = new Subject<TokenExpiredMessage>();
+    public readonly tokenExpiredMessageStream = this._tokenExpiredMessageStream.asObservable();
+
+    private readonly _userMovedMessageStream = new Subject<UserMovedMessageTsProto>();
+    public readonly userMovedMessageStream = this._userMovedMessageStream.asObservable();
+
+    private readonly _groupUpdateMessageStream = new Subject<GroupCreatedUpdatedMessageInterface>();
+    public readonly groupUpdateMessageStream = this._groupUpdateMessageStream.asObservable();
+
+    private readonly _groupDeleteMessageStream = new Subject<GroupDeleteMessageTsProto>();
+    public readonly groupDeleteMessageStream = this._groupDeleteMessageStream.asObservable();
+
+    private readonly _userJoinedMessageStream = new Subject<MessageUserJoined>();
+    public readonly userJoinedMessageStream = this._userJoinedMessageStream.asObservable();
+
+    private readonly _userLeftMessageStream = new Subject<UserLeftMessageTsProto>();
+    public readonly userLeftMessageStream = this._userLeftMessageStream.asObservable();
+
+    private readonly _itemEventMessageStream = new Subject<{
+        itemId: number;
+        event: string;
+        parameters: unknown;
+        state: unknown;
+    }>();
+    public readonly itemEventMessageStream = this._itemEventMessageStream.asObservable();
+
+    private readonly _emoteEventMessageStream = new Subject<EmoteEventMessageTsProto>();
+    public readonly emoteEventMessageStream = this._emoteEventMessageStream.asObservable();
+
+    private readonly _variableMessageStream = new Subject<{ name: string; value: unknown }>();
+    public readonly variableMessageStream = this._variableMessageStream.asObservable();
+
+    private readonly _playerDetailsUpdatedMessageStream = new Subject<PlayerDetailsUpdatedMessageTsProto>();
+    public readonly playerDetailsUpdatedMessageStream = this._playerDetailsUpdatedMessageStream.asObservable();
+
+    private readonly _connectionErrorStream = new Subject<CloseEvent>();
+    public readonly connectionErrorStream = this._connectionErrorStream.asObservable();
+
     // eslint-disable-next-line @typescript-eslint/no-explicit-any
     public static setWebsocketFactory(websocketFactory: (url: string) => any): void {
         RoomConnection.websocketFactory = websocketFactory;
@@ -136,8 +199,8 @@ export class RoomConnection implements RoomConnection {
 
         this.socket.onopen = (ev) => {
             //we manually ping every 20s to not be logged out by the server, even when the game is in background.
-            const pingMessage = new PingMessage();
-            interval = setInterval(() => this.socket.send(pingMessage.serializeBinary().buffer), manualPingDelay);
+            const pingMessage = PingMessageTsProto.encode({}).finish();
+            interval = setInterval(() => this.socket.send(pingMessage), manualPingDelay);
         };
 
         this.socket.addEventListener("close", (event) => {
@@ -147,147 +210,252 @@ export class RoomConnection implements RoomConnection {
 
             // If we are not connected yet (if a JoinRoomMessage was not sent), we need to retry.
             if (this.userId === null && !this.closed) {
-                this.dispatch(EventMessage.CONNECTING_ERROR, event);
+                this._connectionErrorStream.next(event);
             }
         });
 
         this.socket.onmessage = (messageEvent) => {
             const arrayBuffer: ArrayBuffer = messageEvent.data;
-            const message = ServerToClientMessage.deserializeBinary(new Uint8Array(arrayBuffer));
 
-            if (message.hasBatchmessage()) {
-                for (const subMessage of (message.getBatchmessage() as BatchMessage).getPayloadList()) {
-                    let event: string | null = null;
-                    let payload;
-                    if (subMessage.hasUsermovedmessage()) {
-                        event = EventMessage.USER_MOVED;
-                        payload = subMessage.getUsermovedmessage();
-                    } else if (subMessage.hasGroupupdatemessage()) {
-                        event = EventMessage.GROUP_CREATE_UPDATE;
-                        payload = subMessage.getGroupupdatemessage();
-                    } else if (subMessage.hasGroupdeletemessage()) {
-                        event = EventMessage.GROUP_DELETE;
-                        payload = subMessage.getGroupdeletemessage();
-                    } else if (subMessage.hasUserjoinedmessage()) {
-                        event = EventMessage.JOIN_ROOM;
-                        payload = subMessage.getUserjoinedmessage();
-                    } else if (subMessage.hasUserleftmessage()) {
-                        event = EventMessage.USER_LEFT;
-                        payload = subMessage.getUserleftmessage();
-                    } else if (subMessage.hasItemeventmessage()) {
-                        event = EventMessage.ITEM_EVENT;
-                        payload = subMessage.getItemeventmessage();
-                    } else if (subMessage.hasEmoteeventmessage()) {
-                        const emoteMessage = subMessage.getEmoteeventmessage() as EmoteEventMessage;
-                        emoteEventStream.fire(emoteMessage.getActoruserid(), emoteMessage.getEmote());
-                    } else if (subMessage.hasPlayerdetailsupdatedmessage()) {
-                        event = EventMessage.USER_DETAILS_UPDATED;
-                        payload = subMessage.getPlayerdetailsupdatedmessage();
-                    } else if (subMessage.hasErrormessage()) {
-                        const errorMessage = subMessage.getErrormessage() as ErrorMessage;
-                        console.error("An error occurred server side: " + errorMessage.getMessage());
-                    } else if (subMessage.hasVariablemessage()) {
-                        event = EventMessage.SET_VARIABLE;
-                        payload = subMessage.getVariablemessage();
+            const serverToClientMessage = ServerToClientMessageTsProto.decode(new Uint8Array(arrayBuffer));
+            //const message = ServerToClientMessage.deserializeBinary(new Uint8Array(arrayBuffer));
+
+            const message = serverToClientMessage.message;
+            if (message === undefined) {
+                return;
+            }
+
+            switch (message.$case) {
+                case "batchMessage": {
+                    for (const subMessageWrapper of message.batchMessage.payload) {
+                        const subMessage = subMessageWrapper.message;
+                        if (subMessage === undefined) {
+                            return;
+                        }
+                        switch (subMessage.$case) {
+                            case "errorMessage": {
+                                this._errorMessageStream.next(subMessage.errorMessage);
+                                console.error("An error occurred server side: " + subMessage.errorMessage.message);
+                                break;
+                            }
+                            case "userJoinedMessage": {
+                                this._userJoinedMessageStream.next(
+                                    this.toMessageUserJoined(subMessage.userJoinedMessage)
+                                );
+                                break;
+                            }
+                            case "userLeftMessage": {
+                                this._userLeftMessageStream.next(subMessage.userLeftMessage);
+                                break;
+                            }
+                            case "userMovedMessage": {
+                                this._userMovedMessageStream.next(subMessage.userMovedMessage);
+                                break;
+                            }
+                            case "groupUpdateMessage": {
+                                this._groupUpdateMessageStream.next(
+                                    this.toGroupCreatedUpdatedMessage(subMessage.groupUpdateMessage)
+                                );
+                                break;
+                            }
+                            case "groupDeleteMessage": {
+                                this._groupDeleteMessageStream.next(subMessage.groupDeleteMessage);
+                                break;
+                            }
+                            case "itemEventMessage": {
+                                this._itemEventMessageStream.next({
+                                    itemId: subMessage.itemEventMessage.itemId,
+                                    event: subMessage.itemEventMessage.event,
+                                    parameters: JSON.parse(subMessage.itemEventMessage.parametersJson),
+                                    state: JSON.parse(subMessage.itemEventMessage.stateJson),
+                                });
+                                break;
+                            }
+                            case "emoteEventMessage": {
+                                this._emoteEventMessageStream.next(subMessage.emoteEventMessage);
+                                break;
+                            }
+                            case "playerDetailsUpdatedMessage": {
+                                this._playerDetailsUpdatedMessageStream.next(subMessage.playerDetailsUpdatedMessage);
+                                break;
+                            }
+                            case "variableMessage": {
+                                const name = subMessage.variableMessage.name;
+                                const serializedValue = subMessage.variableMessage.value;
+                                let value: unknown = undefined;
+                                if (serializedValue) {
+                                    try {
+                                        value = JSON.parse(serializedValue);
+                                    } catch (e) {
+                                        console.error(
+                                            'Unable to unserialize value received from server for variable "' +
+                                                name +
+                                                '". Value received: "' +
+                                                serializedValue +
+                                                '". Error: ',
+                                            e
+                                        );
+                                    }
+                                }
+
+                                this._variableMessageStream.next({ name, value });
+                                break;
+                            }
+                            default: {
+                                // Security check: if we forget a "case", the line below will catch the error at compile-time.
+                                const tmp: never = subMessage;
+                            }
+                        }
+                    }
+                    break;
+                }
+                case "roomJoinedMessage": {
+                    const roomJoinedMessage = message.roomJoinedMessage;
+
+                    const items: { [itemId: number]: unknown } = {};
+                    for (const item of roomJoinedMessage.item) {
+                        items[item.itemId] = JSON.parse(item.stateJson);
+                    }
+
+                    const variables = new Map<string, unknown>();
+                    for (const variable of roomJoinedMessage.variable) {
+                        try {
+                            variables.set(variable.name, JSON.parse(variable.value));
+                        } catch (e) {
+                            console.error(
+                                'Unable to unserialize value received from server for variable "' +
+                                    variable.name +
+                                    '". Value received: "' +
+                                    variable.value +
+                                    '". Error: ',
+                                e
+                            );
+                        }
+                    }
+
+                    this.userId = roomJoinedMessage.currentUserId;
+                    this.tags = roomJoinedMessage.tag;
+                    this._userRoomToken = roomJoinedMessage.userRoomToken;
+
+                    this._roomJoinedMessageStream.next({
+                        connection: this,
+                        room: {
+                            items,
+                            variables,
+                        } as RoomJoinedMessageInterface,
+                    });
+                    break;
+                }
+                case "worldFullMessage": {
+                    this._worldFullMessageStream.next(null);
+                    this.closed = true;
+                    break;
+                }
+                case "tokenExpiredMessage": {
+                    connectionManager.logout().catch((e) => console.error(e));
+                    this.closed = true; //technically, this isn't needed since loadOpenIDScreen() will do window.location.assign() but I prefer to leave it for consistency
+                    break;
+                }
+                case "worldConnexionMessage": {
+                    this._worldFullMessageStream.next(message.worldConnexionMessage.message);
+                    this.closed = true;
+                    break;
+                }
+                case "webRtcSignalToClientMessage": {
+                    this._webRtcSignalToClientMessageStream.next({
+                        userId: message.webRtcSignalToClientMessage.userId,
+                        signal: JSON.parse(message.webRtcSignalToClientMessage.signal),
+                        webRtcUser: message.webRtcSignalToClientMessage.webrtcUserName
+                            ? message.webRtcSignalToClientMessage.webrtcUserName
+                            : undefined,
+                        webRtcPassword: message.webRtcSignalToClientMessage.webrtcPassword
+                            ? message.webRtcSignalToClientMessage.webrtcPassword
+                            : undefined,
+                    });
+                    break;
+                }
+                case "webRtcScreenSharingSignalToClientMessage": {
+                    this._webRtcScreenSharingSignalToClientMessageStream.next({
+                        userId: message.webRtcScreenSharingSignalToClientMessage.userId,
+                        signal: JSON.parse(message.webRtcScreenSharingSignalToClientMessage.signal),
+                        webRtcUser: message.webRtcScreenSharingSignalToClientMessage.webrtcUserName
+                            ? message.webRtcScreenSharingSignalToClientMessage.webrtcUserName
+                            : undefined,
+                        webRtcPassword: message.webRtcScreenSharingSignalToClientMessage.webrtcPassword
+                            ? message.webRtcScreenSharingSignalToClientMessage.webrtcPassword
+                            : undefined,
+                    });
+                    break;
+                }
+                case "webRtcStartMessage": {
+                    this._webRtcStartMessageStream.next({
+                        userId: message.webRtcStartMessage.userId,
+                        initiator: message.webRtcStartMessage.initiator,
+                        webRtcUser: message.webRtcStartMessage.webrtcUserName
+                            ? message.webRtcStartMessage.webrtcUserName
+                            : undefined,
+                        webRtcPassword: message.webRtcStartMessage.webrtcPassword
+                            ? message.webRtcStartMessage.webrtcPassword
+                            : undefined,
+                    });
+                    break;
+                }
+                case "webRtcDisconnectMessage": {
+                    this._webRtcDisconnectMessageStream.next(message.webRtcDisconnectMessage);
+                    break;
+                }
+                case "teleportMessageMessage": {
+                    // FIXME: WHY IS THIS UNUSED? CAN WE REMOVE THIS???
+                    this._teleportMessageMessageStream.next(message.teleportMessageMessage.map);
+                    break;
+                }
+                case "sendJitsiJwtMessage": {
+                    this._sendJitsiJwtMessageStream.next(message.sendJitsiJwtMessage);
+                    break;
+                }
+                case "sendUserMessage": {
+                    adminMessagesService.onSendusermessage(message.sendUserMessage);
+                    break;
+                }
+                case "banUserMessage": {
+                    adminMessagesService.onSendusermessage(message.banUserMessage);
+                    break;
+                }
+                case "worldFullWarningMessage": {
+                    warningContainerStore.activateWarningContainer();
+                    break;
+                }
+                case "refreshRoomMessage": {
+                    //todo: implement a way to notify the user the room was refreshed.
+                    break;
+                }
+                case "followRequestMessage": {
+                    if (!localUserStore.getIgnoreFollowRequests()) {
+                        followUsersStore.addFollowRequest(message.followRequestMessage.leader);
+                    }
+                    break;
+                }
+                case "followConfirmationMessage": {
+                    followUsersStore.addFollower(message.followConfirmationMessage.follower);
+                    break;
+                }
+                case "followAbortMessage": {
+                    if (get(followRoleStore) === "follower") {
+                        followUsersStore.stopFollowing();
                     } else {
-                        throw new Error("Unexpected batch message type");
-                    }
-
-                    if (event) {
-                        this.dispatch(event, payload);
+                        followUsersStore.removeFollower(message.followAbortMessage.follower);
                     }
+                    break;
                 }
-            } else if (message.hasRoomjoinedmessage()) {
-                const roomJoinedMessage = message.getRoomjoinedmessage() as RoomJoinedMessage;
-
-                const items: { [itemId: number]: unknown } = {};
-                for (const item of roomJoinedMessage.getItemList()) {
-                    items[item.getItemid()] = JSON.parse(item.getStatejson());
+                case "errorMessage": {
+                    this._errorMessageStream.next(message.errorMessage);
+                    console.error("An error occurred server side: " + message.errorMessage.message);
+                    break;
                 }
-
-                const variables = new Map<string, unknown>();
-                for (const variable of roomJoinedMessage.getVariableList()) {
-                    try {
-                        variables.set(variable.getName(), JSON.parse(variable.getValue()));
-                    } catch (e) {
-                        console.error(
-                            'Unable to unserialize value received from server for variable "' +
-                                variable.getName() +
-                                '". Value received: "' +
-                                variable.getValue() +
-                                '". Error: ',
-                            e
-                        );
-                    }
+                default: {
+                    // Security check: if we forget a "case", the line below will catch the error at compile-time.
+                    const tmp: never = message;
                 }
-
-                this.userId = roomJoinedMessage.getCurrentuserid();
-                this.tags = roomJoinedMessage.getTagList();
-                this._userRoomToken = roomJoinedMessage.getUserroomtoken();
-
-                this.dispatch(EventMessage.CONNECT, {
-                    connection: this,
-                    room: {
-                        items,
-                        variables,
-                    } as RoomJoinedMessageInterface,
-                });
-            } else if (message.hasWorldfullmessage()) {
-                worldFullMessageStream.onMessage();
-                this.closed = true;
-            } else if (message.hasTokenexpiredmessage()) {
-                connectionManager.logout();
-                this.closed = true; //technically, this isn't needed since loadOpenIDScreen() will do window.location.assign() but I prefer to leave it for consistency
-            } else if (message.hasWorldconnexionmessage()) {
-                worldFullMessageStream.onMessage(message.getWorldconnexionmessage()?.getMessage());
-                this.closed = true;
-            } else if (message.hasWebrtcsignaltoclientmessage()) {
-                this.dispatch(EventMessage.WEBRTC_SIGNAL, message.getWebrtcsignaltoclientmessage());
-            } else if (message.hasWebrtcscreensharingsignaltoclientmessage()) {
-                this.dispatch(
-                    EventMessage.WEBRTC_SCREEN_SHARING_SIGNAL,
-                    message.getWebrtcscreensharingsignaltoclientmessage()
-                );
-            } else if (message.hasWebrtcstartmessage()) {
-                this.dispatch(EventMessage.WEBRTC_START, message.getWebrtcstartmessage());
-            } else if (message.hasWebrtcdisconnectmessage()) {
-                this.dispatch(EventMessage.WEBRTC_DISCONNECT, message.getWebrtcdisconnectmessage());
-            } else if (message.hasPlayglobalmessage()) {
-                this.dispatch(EventMessage.PLAY_GLOBAL_MESSAGE, message.getPlayglobalmessage());
-            } else if (message.hasStopglobalmessage()) {
-                this.dispatch(EventMessage.STOP_GLOBAL_MESSAGE, message.getStopglobalmessage());
-            } else if (message.hasTeleportmessagemessage()) {
-                this.dispatch(EventMessage.TELEPORT, message.getTeleportmessagemessage());
-            } else if (message.hasSendjitsijwtmessage()) {
-                this.dispatch(EventMessage.START_JITSI_ROOM, message.getSendjitsijwtmessage());
-            } else if (message.hasSendusermessage()) {
-                adminMessagesService.onSendusermessage(message.getSendusermessage() as SendUserMessage);
-            } else if (message.hasBanusermessage()) {
-                adminMessagesService.onSendusermessage(message.getBanusermessage() as BanUserMessage);
-            } else if (message.hasWorldfullwarningmessage()) {
-                warningContainerStore.activateWarningContainer();
-            } else if (message.hasRefreshroommessage()) {
-                //todo: implement a way to notify the user the room was refreshed.
-            } else if (message.hasFollowrequestmessage()) {
-                const requestMessage = message.getFollowrequestmessage() as FollowRequestMessage;
-                if (!localUserStore.getIgnoreFollowRequests()) {
-                    followUsersStore.addFollowRequest(requestMessage.getLeader());
-                }
-            } else if (message.hasFollowconfirmationmessage()) {
-                const responseMessage = message.getFollowconfirmationmessage() as FollowConfirmationMessage;
-                followUsersStore.addFollower(responseMessage.getFollower());
-            } else if (message.hasFollowabortmessage()) {
-                const abortMessage = message.getFollowabortmessage() as FollowAbortMessage;
-                if (get(followRoleStore) === "follower") {
-                    followUsersStore.stopFollowing();
-                } else {
-                    followUsersStore.removeFollower(abortMessage.getFollower());
-                }
-            } else if (message.hasErrormessage()) {
-                const errorMessage = message.getErrormessage() as ErrorMessage;
-                console.error("An error occurred server side: " + errorMessage.getMessage());
-            } else {
-                throw new Error("Unknown message received");
             }
         };
     }
@@ -314,17 +482,25 @@ export class RoomConnection implements RoomConnection {
     }*/
 
     public emitPlayerOutlineColor(color: number | null) {
-        const message = new SetPlayerDetailsMessage();
+        let message: SetPlayerDetailsMessageTsProto;
         if (color === null) {
-            message.setRemoveoutlinecolor(true);
+            message = SetPlayerDetailsMessageTsProto.fromPartial({
+                removeOutlineColor: true,
+            });
         } else {
-            message.setOutlinecolor(color);
+            message = SetPlayerDetailsMessageTsProto.fromPartial({
+                outlineColor: color,
+            });
         }
 
-        const clientToServerMessage = new ClientToServerMessage();
-        clientToServerMessage.setSetplayerdetailsmessage(message);
+        const bytes = ClientToServerMessageTsProto.encode({
+            message: {
+                $case: "setPlayerDetailsMessage",
+                setPlayerDetailsMessage: message,
+            },
+        }).finish();
 
-        this.socket.send(clientToServerMessage.serializeBinary().buffer);
+        this.socket.send(bytes);
     }
 
     public closeConnection(): void {
@@ -332,41 +508,35 @@ export class RoomConnection implements RoomConnection {
         this.closed = true;
     }
 
-    private toPositionMessage(x: number, y: number, direction: string, moving: boolean): PositionMessage {
-        const positionMessage = new PositionMessage();
-        positionMessage.setX(Math.floor(x));
-        positionMessage.setY(Math.floor(y));
-        let directionEnum: Direction;
-        switch (direction) {
-            case "up":
-                directionEnum = Direction.UP;
-                break;
-            case "down":
-                directionEnum = Direction.DOWN;
-                break;
-            case "left":
-                directionEnum = Direction.LEFT;
-                break;
-            case "right":
-                directionEnum = Direction.RIGHT;
-                break;
-            default:
-                throw new Error("Unexpected direction");
-        }
-        positionMessage.setDirection(directionEnum);
-        positionMessage.setMoving(moving);
-
-        return positionMessage;
+    private toPositionMessage(x: number, y: number, direction: string, moving: boolean): PositionMessageTsProto {
+        return {
+            x: Math.floor(x),
+            y: Math.floor(y),
+            moving,
+            direction: (() => {
+                switch (direction) {
+                    case "up":
+                        return PositionMessage_Direction.UP;
+                    case "down":
+                        return PositionMessage_Direction.DOWN;
+                    case "left":
+                        return PositionMessage_Direction.LEFT;
+                    case "right":
+                        return PositionMessage_Direction.RIGHT;
+                    default:
+                        throw new Error("Unexpected direction");
+                }
+            })(),
+        };
     }
 
-    private toViewportMessage(viewport: ViewportInterface): ViewportMessage {
-        const viewportMessage = new ViewportMessage();
-        viewportMessage.setLeft(Math.floor(viewport.left));
-        viewportMessage.setRight(Math.floor(viewport.right));
-        viewportMessage.setTop(Math.floor(viewport.top));
-        viewportMessage.setBottom(Math.floor(viewport.bottom));
-
-        return viewportMessage;
+    private toViewportMessage(viewport: ViewportInterface): ViewportMessageTsProto {
+        return {
+            left: Math.floor(viewport.left),
+            right: Math.floor(viewport.right),
+            top: Math.floor(viewport.top),
+            bottom: Math.floor(viewport.bottom),
+        };
     }
 
     public sharePosition(x: number, y: number, direction: string, moving: boolean, viewport: ViewportInterface): void {
@@ -378,81 +548,77 @@ export class RoomConnection implements RoomConnection {
 
         const viewportMessage = this.toViewportMessage(viewport);
 
-        const userMovesMessage = new UserMovesMessage();
-        userMovesMessage.setPosition(positionMessage);
-        userMovesMessage.setViewport(viewportMessage);
+        const bytes = ClientToServerMessageTsProto.encode({
+            message: {
+                $case: "userMovesMessage",
+                userMovesMessage: {
+                    position: positionMessage,
+                    viewport: viewportMessage,
+                },
+            },
+        }).finish();
 
-        //console.log('Sending position ', positionMessage.getX(), positionMessage.getY());
-        const clientToServerMessage = new ClientToServerMessage();
-        clientToServerMessage.setUsermovesmessage(userMovesMessage);
-
-        this.socket.send(clientToServerMessage.serializeBinary().buffer);
+        this.socket.send(bytes);
     }
 
     public setSilent(silent: boolean): void {
-        const silentMessage = new SilentMessage();
-        silentMessage.setSilent(silent);
+        const bytes = ClientToServerMessageTsProto.encode({
+            message: {
+                $case: "silentMessage",
+                silentMessage: {
+                    silent,
+                },
+            },
+        }).finish();
 
-        const clientToServerMessage = new ClientToServerMessage();
-        clientToServerMessage.setSilentmessage(silentMessage);
-
-        this.socket.send(clientToServerMessage.serializeBinary().buffer);
+        this.socket.send(bytes);
     }
 
     public setViewport(viewport: ViewportInterface): void {
-        const viewportMessage = new ViewportMessage();
-        viewportMessage.setTop(Math.round(viewport.top));
-        viewportMessage.setBottom(Math.round(viewport.bottom));
-        viewportMessage.setLeft(Math.round(viewport.left));
-        viewportMessage.setRight(Math.round(viewport.right));
+        const bytes = ClientToServerMessageTsProto.encode({
+            message: {
+                $case: "viewportMessage",
+                viewportMessage: this.toViewportMessage(viewport),
+            },
+        }).finish();
 
-        const clientToServerMessage = new ClientToServerMessage();
-        clientToServerMessage.setViewportmessage(viewportMessage);
-
-        this.socket.send(clientToServerMessage.serializeBinary().buffer);
+        this.socket.send(bytes);
     }
 
-    public onUserJoins(callback: (message: MessageUserJoined) => void): void {
+    /*    public onUserJoins(callback: (message: MessageUserJoined) => void): void {
         this.onMessage(EventMessage.JOIN_ROOM, (message: UserJoinedMessage) => {
             callback(this.toMessageUserJoined(message));
         });
-    }
+    }*/
 
     // TODO: move this to protobuf utils
-    private toMessageUserJoined(message: UserJoinedMessage): MessageUserJoined {
-        const position = message.getPosition();
+    private toMessageUserJoined(message: UserJoinedMessageTsProto): MessageUserJoined {
+        const position = message.position;
         if (position === undefined) {
             throw new Error("Invalid JOIN_ROOM message");
         }
 
-        const characterLayers = message
-            .getCharacterlayersList()
-            .map((characterLayer: CharacterLayerMessage): BodyResourceDescriptionInterface => {
-                return {
-                    name: characterLayer.getName(),
-                    img: characterLayer.getUrl(),
-                };
-            });
+        const characterLayers = message.characterLayers.map((characterLayer): BodyResourceDescriptionInterface => {
+            return {
+                name: characterLayer.name,
+                img: characterLayer.url,
+            };
+        });
 
-        const companion = message.getCompanion();
+        const companion = message.companion;
 
         return {
-            userId: message.getUserid(),
-            name: message.getName(),
+            userId: message.userId,
+            name: message.name,
             characterLayers,
-            visitCardUrl: message.getVisitcardurl(),
+            visitCardUrl: message.visitCardUrl,
             position: ProtobufClientUtils.toPointInterface(position),
-            companion: companion ? companion.getName() : null,
-            userUuid: message.getUseruuid(),
-            outlineColor: message.getHasoutline() ? message.getOutlinecolor() : undefined,
+            companion: companion ? companion.name : null,
+            userUuid: message.userUuid,
+            outlineColor: message.hasOutline ? message.outlineColor : undefined,
         };
     }
 
-    public onUserMoved(callback: (message: UserMovedMessage) => void): void {
-        this.onMessage(EventMessage.USER_MOVED, callback);
-        //this.socket.on(EventMessage.USER_MOVED, callback);
-    }
-
     /**
      * Registers a listener on a message that is part of a batch
      */
@@ -465,114 +631,49 @@ export class RoomConnection implements RoomConnection {
         callbacks.push(callback);
     }
 
-    public onUserLeft(callback: (userId: number) => void): void {
-        this.onMessage(EventMessage.USER_LEFT, (message: UserLeftMessage) => {
-            callback(message.getUserid());
-        });
-    }
-
-    public onGroupUpdatedOrCreated(
-        callback: (groupCreateUpdateMessage: GroupCreatedUpdatedMessageInterface) => void
-    ): void {
-        this.onMessage(EventMessage.GROUP_CREATE_UPDATE, (message: GroupUpdateMessage) => {
-            callback(this.toGroupCreatedUpdatedMessage(message));
-        });
-    }
-
-    private toGroupCreatedUpdatedMessage(message: GroupUpdateMessage): GroupCreatedUpdatedMessageInterface {
-        const position = message.getPosition();
+    private toGroupCreatedUpdatedMessage(message: GroupUpdateMessageTsProto): GroupCreatedUpdatedMessageInterface {
+        const position = message.position;
         if (position === undefined) {
             throw new Error("Missing position in GROUP_CREATE_UPDATE");
         }
 
         return {
-            groupId: message.getGroupid(),
-            position: position.toObject(),
-            groupSize: message.getGroupsize(),
+            groupId: message.groupId,
+            position: position,
+            groupSize: message.groupSize,
         };
     }
 
-    public onGroupDeleted(callback: (groupId: number) => void): void {
-        this.onMessage(EventMessage.GROUP_DELETE, (message: GroupDeleteMessage) => {
-            callback(message.getGroupid());
-        });
-    }
-
-    public onConnectingError(callback: (event: CloseEvent) => void): void {
-        this.onMessage(EventMessage.CONNECTING_ERROR, (event: CloseEvent) => {
-            callback(event);
-        });
-    }
-
     public onConnectError(callback: (error: Event) => void): void {
         this.socket.addEventListener("error", callback);
     }
 
-    public onConnect(callback: (roomConnection: OnConnectInterface) => void): void {
-        //this.socket.addEventListener('open', callback)
-        this.onMessage(EventMessage.CONNECT, callback);
-    }
-
-    /**
-     * Triggered when we receive all the details of a room (users, groups, ...)
-     */
-    /*public onStartRoom(callback: (event: RoomJoinedMessageInterface) => void): void {
-        this.onMessage(EventMessage.START_ROOM, callback);
-    }*/
-
     public sendWebrtcSignal(signal: unknown, receiverId: number) {
-        const webRtcSignal = new WebRtcSignalToServerMessage();
-        webRtcSignal.setReceiverid(receiverId);
-        webRtcSignal.setSignal(JSON.stringify(signal));
+        const bytes = ClientToServerMessageTsProto.encode({
+            message: {
+                $case: "webRtcSignalToServerMessage",
+                webRtcSignalToServerMessage: {
+                    receiverId,
+                    signal: JSON.stringify(signal),
+                },
+            },
+        }).finish();
 
-        const clientToServerMessage = new ClientToServerMessage();
-        clientToServerMessage.setWebrtcsignaltoservermessage(webRtcSignal);
-
-        this.socket.send(clientToServerMessage.serializeBinary().buffer);
+        this.socket.send(bytes);
     }
 
     public sendWebrtcScreenSharingSignal(signal: unknown, receiverId: number) {
-        const webRtcSignal = new WebRtcSignalToServerMessage();
-        webRtcSignal.setReceiverid(receiverId);
-        webRtcSignal.setSignal(JSON.stringify(signal));
+        const bytes = ClientToServerMessageTsProto.encode({
+            message: {
+                $case: "webRtcScreenSharingSignalToServerMessage",
+                webRtcScreenSharingSignalToServerMessage: {
+                    receiverId,
+                    signal: JSON.stringify(signal),
+                },
+            },
+        }).finish();
 
-        const clientToServerMessage = new ClientToServerMessage();
-        clientToServerMessage.setWebrtcscreensharingsignaltoservermessage(webRtcSignal);
-
-        this.socket.send(clientToServerMessage.serializeBinary().buffer);
-    }
-
-    public receiveWebrtcStart(callback: (message: UserSimplePeerInterface) => void) {
-        this.onMessage(EventMessage.WEBRTC_START, (message: WebRtcStartMessage) => {
-            callback({
-                userId: message.getUserid(),
-                initiator: message.getInitiator(),
-                webRtcUser: message.getWebrtcusername() ?? undefined,
-                webRtcPassword: message.getWebrtcpassword() ?? undefined,
-            });
-        });
-    }
-
-    public receiveWebrtcSignal(callback: (message: WebRtcSignalReceivedMessageInterface) => void) {
-        this.onMessage(EventMessage.WEBRTC_SIGNAL, (message: WebRtcSignalToClientMessage) => {
-            callback({
-                userId: message.getUserid(),
-                signal: JSON.parse(message.getSignal()),
-                webRtcUser: message.getWebrtcusername() ?? undefined,
-                webRtcPassword: message.getWebrtcpassword() ?? undefined,
-            });
-        });
-    }
-
-    public receiveWebrtcScreenSharingSignal(callback: (message: WebRtcSignalReceivedMessageInterface) => void) {
-        this.onMessage(EventMessage.WEBRTC_SCREEN_SHARING_SIGNAL, (message: WebRtcSignalToClientMessage) => {
-            callback({
-                userId: message.getUserid(),
-                signal: JSON.parse(message.getSignal()),
-                webRtcUser: message.getWebrtcusername() ?? undefined,
-                webRtcPassword: message.getWebrtcpassword() ?? undefined,
-            });
-        });
+        this.socket.send(bytes);
     }
 
     public onServerDisconnected(callback: () => void): void {
@@ -594,61 +695,34 @@ export class RoomConnection implements RoomConnection {
         return this.userId;
     }
 
-    disconnectMessage(callback: (message: WebRtcDisconnectMessageInterface) => void): void {
-        this.onMessage(EventMessage.WEBRTC_DISCONNECT, (message: WebRtcDisconnectMessage) => {
-            callback({
-                userId: message.getUserid(),
-            });
-        });
-    }
-
     emitActionableEvent(itemId: number, event: string, state: unknown, parameters: unknown): void {
-        const itemEventMessage = new ItemEventMessage();
-        itemEventMessage.setItemid(itemId);
-        itemEventMessage.setEvent(event);
-        itemEventMessage.setStatejson(JSON.stringify(state));
-        itemEventMessage.setParametersjson(JSON.stringify(parameters));
+        const bytes = ClientToServerMessageTsProto.encode({
+            message: {
+                $case: "itemEventMessage",
+                itemEventMessage: {
+                    itemId,
+                    event,
+                    stateJson: JSON.stringify(state),
+                    parametersJson: JSON.stringify(parameters),
+                },
+            },
+        }).finish();
 
-        const clientToServerMessage = new ClientToServerMessage();
-        clientToServerMessage.setItemeventmessage(itemEventMessage);
-
-        this.socket.send(clientToServerMessage.serializeBinary().buffer);
+        this.socket.send(bytes);
     }
 
     emitSetVariableEvent(name: string, value: unknown): void {
-        const variableMessage = new VariableMessage();
-        variableMessage.setName(name);
-        variableMessage.setValue(JSON.stringify(value));
+        const bytes = ClientToServerMessageTsProto.encode({
+            message: {
+                $case: "variableMessage",
+                variableMessage: {
+                    name,
+                    value: JSON.stringify(value),
+                },
+            },
+        }).finish();
 
-        const clientToServerMessage = new ClientToServerMessage();
-        clientToServerMessage.setVariablemessage(variableMessage);
-
-        this.socket.send(clientToServerMessage.serializeBinary().buffer);
-    }
-
-    onActionableEvent(callback: (message: ItemEventMessageInterface) => void): void {
-        this.onMessage(EventMessage.ITEM_EVENT, (message: ItemEventMessage) => {
-            callback({
-                itemId: message.getItemid(),
-                event: message.getEvent(),
-                parameters: JSON.parse(message.getParametersjson()),
-                state: JSON.parse(message.getStatejson()),
-            });
-        });
-    }
-
-    onPlayerDetailsUpdated(callback: (message: PlayerDetailsUpdatedMessageInterface) => void): void {
-        this.onMessage(EventMessage.USER_DETAILS_UPDATED, (message: PlayerDetailsUpdatedMessage) => {
-            const details = message.getDetails();
-            if (details === undefined) {
-                throw new Error("Malformed message. Missing details in PlayerDetailsUpdatedMessage");
-            }
-            callback({
-                userId: message.getUserid(),
-                outlineColor: details.getOutlinecolor(),
-                removeOutlineColor: details.getRemoveoutlinecolor(),
-            });
-        });
+        this.socket.send(bytes);
     }
 
     public uploadAudio(file: FormData) {
@@ -662,91 +736,48 @@ export class RoomConnection implements RoomConnection {
             });
     }
 
-    /*    public receivePlayGlobalMessage(callback: (message: PlayGlobalMessageInterface) => void) {
-        return this.onMessage(EventMessage.PLAY_GLOBAL_MESSAGE, (message: PlayGlobalMessage) => {
-            callback({
-                id: message.getId(),
-                type: message.getType(),
-                message: message.getMessage(),
-            });
-        });
-    }*/
-
-    public receiveStopGlobalMessage(callback: (messageId: string) => void) {
-        return this.onMessage(EventMessage.STOP_GLOBAL_MESSAGE, (message: StopGlobalMessage) => {
-            callback(message.getId());
-        });
-    }
-
-    public receiveTeleportMessage(callback: (messageId: string) => void) {
-        return this.onMessage(EventMessage.TELEPORT, (message: TeleportMessageMessage) => {
-            callback(message.getMap());
-        });
-    }
-
     public emitGlobalMessage(message: PlayGlobalMessageInterface): void {
-        const playGlobalMessage = new PlayGlobalMessage();
-        playGlobalMessage.setType(message.type);
-        playGlobalMessage.setContent(message.content);
-        playGlobalMessage.setBroadcasttoworld(message.broadcastToWorld);
+        const bytes = ClientToServerMessageTsProto.encode({
+            message: {
+                $case: "playGlobalMessage",
+                playGlobalMessage: {
+                    type: message.type,
+                    content: message.content,
+                    broadcastToWorld: message.broadcastToWorld,
+                },
+            },
+        }).finish();
 
-        const clientToServerMessage = new ClientToServerMessage();
-        clientToServerMessage.setPlayglobalmessage(playGlobalMessage);
-
-        this.socket.send(clientToServerMessage.serializeBinary().buffer);
+        this.socket.send(bytes);
     }
 
     public emitReportPlayerMessage(reportedUserUuid: string, reportComment: string): void {
-        const reportPlayerMessage = new ReportPlayerMessage();
-        reportPlayerMessage.setReporteduseruuid(reportedUserUuid);
-        reportPlayerMessage.setReportcomment(reportComment);
+        const bytes = ClientToServerMessageTsProto.encode({
+            message: {
+                $case: "reportPlayerMessage",
+                reportPlayerMessage: {
+                    reportedUserUuid,
+                    reportComment,
+                },
+            },
+        }).finish();
 
-        const clientToServerMessage = new ClientToServerMessage();
-        clientToServerMessage.setReportplayermessage(reportPlayerMessage);
-
-        this.socket.send(clientToServerMessage.serializeBinary().buffer);
+        this.socket.send(bytes);
     }
 
     public emitQueryJitsiJwtMessage(jitsiRoom: string, tag: string | undefined): void {
-        const queryJitsiJwtMessage = new QueryJitsiJwtMessage();
-        queryJitsiJwtMessage.setJitsiroom(jitsiRoom);
-        if (tag !== undefined) {
-            queryJitsiJwtMessage.setTag(tag);
-        }
+        const bytes = ClientToServerMessageTsProto.encode({
+            message: {
+                $case: "queryJitsiJwtMessage",
+                queryJitsiJwtMessage: {
+                    jitsiRoom,
+                    tag: tag ?? "", // empty string is sent as "undefined" by ts-proto
+                    // TODO: when we migrated "pusher" to ts-proto, migrate this to a StringValue
+                },
+            },
+        }).finish();
 
-        const clientToServerMessage = new ClientToServerMessage();
-        clientToServerMessage.setQueryjitsijwtmessage(queryJitsiJwtMessage);
-
-        this.socket.send(clientToServerMessage.serializeBinary().buffer);
-    }
-
-    public onStartJitsiRoom(callback: (jwt: string, room: string) => void): void {
-        this.onMessage(EventMessage.START_JITSI_ROOM, (message: SendJitsiJwtMessage) => {
-            callback(message.getJwt(), message.getJitsiroom());
-        });
-    }
-
-    public onSetVariable(callback: (name: string, value: unknown) => void): void {
-        this.onMessage(EventMessage.SET_VARIABLE, (message: VariableMessage) => {
-            const name = message.getName();
-            const serializedValue = message.getValue();
-            let value: unknown = undefined;
-            if (serializedValue) {
-                try {
-                    value = JSON.parse(serializedValue);
-                } catch (e) {
-                    console.error(
-                        'Unable to unserialize value received from server for variable "' +
-                            name +
-                            '". Value received: "' +
-                            serializedValue +
-                            '". Error: ',
-                        e
-                    );
-                }
-            }
-            callback(name, value);
-        });
+        this.socket.send(bytes);
     }
 
     public hasTag(tag: string): boolean {
@@ -758,36 +789,51 @@ export class RoomConnection implements RoomConnection {
     }
 
     public emitEmoteEvent(emoteName: string): void {
-        const emoteMessage = new EmotePromptMessage();
-        emoteMessage.setEmote(emoteName);
+        const bytes = ClientToServerMessageTsProto.encode({
+            message: {
+                $case: "emotePromptMessage",
+                emotePromptMessage: {
+                    emote: emoteName,
+                },
+            },
+        }).finish();
 
-        const clientToServerMessage = new ClientToServerMessage();
-        clientToServerMessage.setEmotepromptmessage(emoteMessage);
-
-        this.socket.send(clientToServerMessage.serializeBinary().buffer);
+        this.socket.send(bytes);
     }
 
     public emitFollowRequest(): void {
         if (!this.userId) {
             return;
         }
-        const message = new FollowRequestMessage();
-        message.setLeader(this.userId);
-        const clientToServerMessage = new ClientToServerMessage();
-        clientToServerMessage.setFollowrequestmessage(message);
-        this.socket.send(clientToServerMessage.serializeBinary().buffer);
+
+        const bytes = ClientToServerMessageTsProto.encode({
+            message: {
+                $case: "followRequestMessage",
+                followRequestMessage: {
+                    leader: this.userId,
+                },
+            },
+        }).finish();
+
+        this.socket.send(bytes);
     }
 
     public emitFollowConfirmation(): void {
         if (!this.userId) {
             return;
         }
-        const message = new FollowConfirmationMessage();
-        message.setLeader(get(followUsersStore)[0]);
-        message.setFollower(this.userId);
-        const clientToServerMessage = new ClientToServerMessage();
-        clientToServerMessage.setFollowconfirmationmessage(message);
-        this.socket.send(clientToServerMessage.serializeBinary().buffer);
+
+        const bytes = ClientToServerMessageTsProto.encode({
+            message: {
+                $case: "followConfirmationMessage",
+                followConfirmationMessage: {
+                    leader: get(followUsersStore)[0],
+                    follower: this.userId,
+                },
+            },
+        }).finish();
+
+        this.socket.send(bytes);
     }
 
     public emitFollowAbort(): void {
@@ -796,12 +842,18 @@ export class RoomConnection implements RoomConnection {
         if (!this.userId || (isLeader && !hasFollowers)) {
             return;
         }
-        const message = new FollowAbortMessage();
-        message.setLeader(isLeader ? this.userId : get(followUsersStore)[0]);
-        message.setFollower(isLeader ? 0 : this.userId);
-        const clientToServerMessage = new ClientToServerMessage();
-        clientToServerMessage.setFollowabortmessage(message);
-        this.socket.send(clientToServerMessage.serializeBinary().buffer);
+
+        const bytes = ClientToServerMessageTsProto.encode({
+            message: {
+                $case: "followAbortMessage",
+                followAbortMessage: {
+                    leader: isLeader ? this.userId : get(followUsersStore)[0],
+                    follower: isLeader ? 0 : this.userId,
+                },
+            },
+        }).finish();
+
+        this.socket.send(bytes);
     }
 
     public getAllTags(): string[] {
diff --git a/front/src/Connexion/WorldFullMessageStream.ts b/front/src/Connexion/WorldFullMessageStream.ts
deleted file mode 100644
index 01ce6f20..00000000
--- a/front/src/Connexion/WorldFullMessageStream.ts
+++ /dev/null
@@ -1,12 +0,0 @@
-import { Subject } from "rxjs";
-
-class WorldFullMessageStream {
-    private _stream: Subject<string | null> = new Subject<string | null>();
-    public stream = this._stream.asObservable();
-
-    onMessage(message?: string) {
-        this._stream.next(message);
-    }
-}
-
-export const worldFullMessageStream = new WorldFullMessageStream();
diff --git a/front/src/Messages/.gitignore b/front/src/Messages/.gitignore
deleted file mode 100644
index 9e0adcc1..00000000
--- a/front/src/Messages/.gitignore
+++ /dev/null
@@ -1 +0,0 @@
-/generated/
diff --git a/front/src/Messages/ts-proto-generated/.gitignore b/front/src/Messages/ts-proto-generated/.gitignore
new file mode 100644
index 00000000..72e8ffc0
--- /dev/null
+++ b/front/src/Messages/ts-proto-generated/.gitignore
@@ -0,0 +1 @@
+*
diff --git a/front/src/Network/ProtobufClientUtils.ts b/front/src/Network/ProtobufClientUtils.ts
index 9ba0f40b..3e172d0f 100644
--- a/front/src/Network/ProtobufClientUtils.ts
+++ b/front/src/Network/ProtobufClientUtils.ts
@@ -1,21 +1,21 @@
-import { PositionMessage } from "../Messages/generated/messages_pb";
-import Direction = PositionMessage.Direction;
+import { PositionMessage, PositionMessage_Direction } from "../Messages/ts-proto-generated/messages";
+
 import type { PointInterface } from "../Connexion/ConnexionModels";
 
 export class ProtobufClientUtils {
     public static toPointInterface(position: PositionMessage): PointInterface {
         let direction: string;
-        switch (position.getDirection()) {
-            case Direction.UP:
+        switch (position.direction) {
+            case PositionMessage_Direction.UP:
                 direction = "up";
                 break;
-            case Direction.DOWN:
+            case PositionMessage_Direction.DOWN:
                 direction = "down";
                 break;
-            case Direction.LEFT:
+            case PositionMessage_Direction.LEFT:
                 direction = "left";
                 break;
-            case Direction.RIGHT:
+            case PositionMessage_Direction.RIGHT:
                 direction = "right";
                 break;
             default:
@@ -24,10 +24,10 @@ export class ProtobufClientUtils {
 
         // sending to all clients in room except sender
         return {
-            x: position.getX(),
-            y: position.getY(),
+            x: position.x,
+            y: position.y,
             direction,
-            moving: position.getMoving(),
+            moving: position.moving,
         };
     }
 }
diff --git a/front/src/Phaser/Companion/Companion.ts b/front/src/Phaser/Companion/Companion.ts
index 80b0236e..6157ebaa 100644
--- a/front/src/Phaser/Companion/Companion.ts
+++ b/front/src/Phaser/Companion/Companion.ts
@@ -41,13 +41,15 @@ export class Companion extends Container {
         this.companionName = name;
         this._pictureStore = writable(undefined);
 
-        texturePromise.then((resource) => {
-            this.addResource(resource);
-            this.invisible = false;
-            return this.getSnapshot().then((htmlImageElementSrc) => {
-                this._pictureStore.set(htmlImageElementSrc);
-            });
-        });
+        texturePromise
+            .then((resource) => {
+                this.addResource(resource);
+                this.invisible = false;
+                return this.getSnapshot().then((htmlImageElementSrc) => {
+                    this._pictureStore.set(htmlImageElementSrc);
+                });
+            })
+            .catch((e) => console.error(e));
 
         this.scene.physics.world.enableBody(this);
 
diff --git a/front/src/Phaser/Companion/CompanionTexturesLoadingManager.ts b/front/src/Phaser/Companion/CompanionTexturesLoadingManager.ts
index bd87ba75..98cceafa 100644
--- a/front/src/Phaser/Companion/CompanionTexturesLoadingManager.ts
+++ b/front/src/Phaser/Companion/CompanionTexturesLoadingManager.ts
@@ -3,7 +3,7 @@ import { COMPANION_RESOURCES, CompanionResourceDescriptionInterface } from "./Co
 
 export const getAllCompanionResources = (loader: LoaderPlugin): CompanionResourceDescriptionInterface[] => {
     COMPANION_RESOURCES.forEach((resource: CompanionResourceDescriptionInterface) => {
-        lazyLoadCompanionResource(loader, resource.name);
+        lazyLoadCompanionResource(loader, resource.name).catch((e) => console.error(e));
     });
 
     return COMPANION_RESOURCES;
diff --git a/front/src/Phaser/Components/Loader.ts b/front/src/Phaser/Components/Loader.ts
index e013e758..7eb08e6d 100644
--- a/front/src/Phaser/Components/Loader.ts
+++ b/front/src/Phaser/Components/Loader.ts
@@ -72,9 +72,11 @@ export class Loader {
             if (this.loadingText) {
                 this.loadingText.destroy();
             }
-            promiseLoadLogoTexture.then((resLoadingImage: Phaser.GameObjects.Image) => {
-                resLoadingImage.destroy();
-            });
+            promiseLoadLogoTexture
+                .then((resLoadingImage: Phaser.GameObjects.Image) => {
+                    resLoadingImage.destroy();
+                })
+                .catch((e) => console.error(e));
             this.progress.destroy();
             this.progressContainer.destroy();
             if (this.scene instanceof DirtyScene) {
diff --git a/front/src/Phaser/Game/EmbeddedWebsiteManager.ts b/front/src/Phaser/Game/EmbeddedWebsiteManager.ts
index bf9f14e4..387940c7 100644
--- a/front/src/Phaser/Game/EmbeddedWebsiteManager.ts
+++ b/front/src/Phaser/Game/EmbeddedWebsiteManager.ts
@@ -16,7 +16,8 @@ export class EmbeddedWebsiteManager {
             if (website === undefined) {
                 throw new Error('Cannot find embedded website with name "' + name + '"');
             }
-            const rect = website.iframe.getBoundingClientRect();
+
+            const scale = website.scale ?? 1;
             return {
                 url: website.url,
                 name: website.name,
@@ -26,9 +27,11 @@ export class EmbeddedWebsiteManager {
                 position: {
                     x: website.phaserObject.x,
                     y: website.phaserObject.y,
-                    width: rect["width"],
-                    height: rect["height"],
+                    width: website.phaserObject.width * scale,
+                    height: website.phaserObject.height * scale,
                 },
+                origin: website.origin,
+                scale: website.scale,
             };
         });
 
@@ -59,7 +62,9 @@ export class EmbeddedWebsiteManager {
                     createEmbeddedWebsiteEvent.position.height,
                     createEmbeddedWebsiteEvent.visible ?? true,
                     createEmbeddedWebsiteEvent.allowApi ?? false,
-                    createEmbeddedWebsiteEvent.allow ?? ""
+                    createEmbeddedWebsiteEvent.allow ?? "",
+                    createEmbeddedWebsiteEvent.origin ?? "map",
+                    createEmbeddedWebsiteEvent.scale ?? 1
                 );
             }
         );
@@ -107,10 +112,18 @@ export class EmbeddedWebsiteManager {
                     website.phaserObject.y = embeddedWebsiteEvent.y;
                 }
                 if (embeddedWebsiteEvent?.width !== undefined) {
-                    website.iframe.style.width = embeddedWebsiteEvent.width + "px";
+                    website.position.width = embeddedWebsiteEvent.width;
+                    website.iframe.style.width = embeddedWebsiteEvent.width / website.phaserObject.scale + "px";
                 }
                 if (embeddedWebsiteEvent?.height !== undefined) {
-                    website.iframe.style.height = embeddedWebsiteEvent.height + "px";
+                    website.position.height = embeddedWebsiteEvent.height;
+                    website.iframe.style.height = embeddedWebsiteEvent.height / website.phaserObject.scale + "px";
+                }
+
+                if (embeddedWebsiteEvent?.scale !== undefined) {
+                    website.phaserObject.scale = embeddedWebsiteEvent.scale;
+                    website.iframe.style.width = website.position.width / embeddedWebsiteEvent.scale + "px";
+                    website.iframe.style.height = website.position.height / embeddedWebsiteEvent.scale + "px";
                 }
             }
         );
@@ -125,7 +138,9 @@ export class EmbeddedWebsiteManager {
         height: number,
         visible: boolean,
         allowApi: boolean,
-        allow: string
+        allow: string,
+        origin: "map" | "player" | undefined,
+        scale: number | undefined
     ): void {
         if (this.embeddedWebsites.has(name)) {
             throw new Error('An embedded website with the name "' + name + '" already exists in your map');
@@ -135,9 +150,9 @@ export class EmbeddedWebsiteManager {
             name,
             url,
             /*x,
-            y,
-            width,
-            height,*/
+y,
+width,
+height,*/
             allow,
             allowApi,
             visible,
@@ -147,6 +162,8 @@ export class EmbeddedWebsiteManager {
                 width,
                 height,
             },
+            origin,
+            scale,
         };
 
         const embeddedWebsite = this.doCreateEmbeddedWebsite(embeddedWebsiteEvent, visible);
@@ -161,22 +178,43 @@ export class EmbeddedWebsiteManager {
         const absoluteUrl = new URL(embeddedWebsiteEvent.url, this.gameScene.MapUrlFile).toString();
 
         const iframe = document.createElement("iframe");
+        const scale = embeddedWebsiteEvent.scale ?? 1;
+
         iframe.src = absoluteUrl;
         iframe.tabIndex = -1;
-        iframe.style.width = embeddedWebsiteEvent.position.width + "px";
-        iframe.style.height = embeddedWebsiteEvent.position.height + "px";
+        iframe.style.width = embeddedWebsiteEvent.position.width / scale + "px";
+        iframe.style.height = embeddedWebsiteEvent.position.height / scale + "px";
         iframe.style.margin = "0";
         iframe.style.padding = "0";
         iframe.style.border = "none";
 
+        const domElement = new DOMElement(
+            this.gameScene,
+            embeddedWebsiteEvent.position.x,
+            embeddedWebsiteEvent.position.y,
+            iframe
+        );
+        domElement.setOrigin(0, 0);
+        if (embeddedWebsiteEvent.scale) {
+            domElement.scale = embeddedWebsiteEvent.scale;
+        }
+        domElement.setVisible(visible);
+
+        switch (embeddedWebsiteEvent.origin) {
+            case "player":
+                this.gameScene.CurrentPlayer.add(domElement);
+                break;
+            case "map":
+            default:
+                this.gameScene.add.existing(domElement);
+        }
+
         const embeddedWebsite = {
             ...embeddedWebsiteEvent,
-            phaserObject: this.gameScene.add
-                .dom(embeddedWebsiteEvent.position.x, embeddedWebsiteEvent.position.y, iframe)
-                .setVisible(visible)
-                .setOrigin(0, 0),
+            phaserObject: domElement,
             iframe: iframe,
         };
+
         if (embeddedWebsiteEvent.allowApi) {
             iframeListener.registerIframe(iframe);
         }
diff --git a/front/src/Phaser/Game/EmoteManager.ts b/front/src/Phaser/Game/EmoteManager.ts
index 06e8b099..097ebf45 100644
--- a/front/src/Phaser/Game/EmoteManager.ts
+++ b/front/src/Phaser/Game/EmoteManager.ts
@@ -1,13 +1,13 @@
-import { emoteEventStream } from "../../Connexion/EmoteEventStream";
 import type { GameScene } from "./GameScene";
 import type { Subscription } from "rxjs";
+import type { RoomConnection } from "../../Connexion/RoomConnection";
 
 export class EmoteManager {
     private subscription: Subscription;
 
-    constructor(private scene: GameScene) {
-        this.subscription = emoteEventStream.stream.subscribe((event) => {
-            const actor = this.scene.MapPlayersByKey.get(event.userId);
+    constructor(private scene: GameScene, private connection: RoomConnection) {
+        this.subscription = connection.emoteEventMessageStream.subscribe((event) => {
+            const actor = this.scene.MapPlayersByKey.get(event.actorUserId);
             if (actor) {
                 actor.playEmote(event.emote);
             }
diff --git a/front/src/Phaser/Game/GameMapPropertiesListener.ts b/front/src/Phaser/Game/GameMapPropertiesListener.ts
index 2dc36df8..f6c28862 100644
--- a/front/src/Phaser/Game/GameMapPropertiesListener.ts
+++ b/front/src/Phaser/Game/GameMapPropertiesListener.ts
@@ -123,7 +123,7 @@ export class GameMapPropertiesListener {
                             .then((coWebsite) => {
                                 const coWebsiteOpen = this.coWebsitesOpenByLayer.get(layer);
                                 if (coWebsiteOpen && coWebsiteOpen.state === OpenCoWebsiteState.MUST_BE_CLOSE) {
-                                    coWebsiteManager.closeCoWebsite(coWebsite);
+                                    coWebsiteManager.closeCoWebsite(coWebsite).catch((e) => console.error(e));
                                     this.coWebsitesOpenByLayer.delete(layer);
                                     this.coWebsitesActionTriggerByLayer.delete(layer);
                                 } else {
@@ -132,7 +132,8 @@ export class GameMapPropertiesListener {
                                         state: OpenCoWebsiteState.OPENED,
                                     });
                                 }
-                            });
+                            })
+                            .catch((e) => console.error(e));
 
                         layoutManagerActionStore.removeAction(actionUuid);
                     };
@@ -198,7 +199,7 @@ export class GameMapPropertiesListener {
                     }
 
                     if (coWebsiteOpen.coWebsite !== undefined) {
-                        coWebsiteManager.closeCoWebsite(coWebsiteOpen.coWebsite);
+                        coWebsiteManager.closeCoWebsite(coWebsiteOpen.coWebsite).catch((e) => console.error(e));
                     }
 
                     this.coWebsitesOpenByLayer.delete(layer);
diff --git a/front/src/Phaser/Game/GameScene.ts b/front/src/Phaser/Game/GameScene.ts
index 4800e259..b6d7274c 100644
--- a/front/src/Phaser/Game/GameScene.ts
+++ b/front/src/Phaser/Game/GameScene.ts
@@ -40,7 +40,6 @@ import { ReconnectingSceneName } from "../Reconnecting/ReconnectingScene";
 import { GameMap } from "./GameMap";
 import { PlayerMovement } from "./PlayerMovement";
 import { PlayersPositionInterpolator } from "./PlayersPositionInterpolator";
-import { worldFullMessageStream } from "../../Connexion/WorldFullMessageStream";
 import { DirtyScene } from "./DirtyScene";
 import { TextUtils } from "../Components/TextUtils";
 import { joystickBaseImg, joystickBaseKey, joystickThumbImg, joystickThumbKey } from "../Components/MobileJoystick";
@@ -60,7 +59,6 @@ import type {
     PositionInterface,
     RoomJoinedMessageInterface,
 } from "../../Connexion/ConnexionModels";
-import type { UserMovedMessage } from "../../Messages/generated/messages_pb";
 import type { RoomConnection } from "../../Connexion/RoomConnection";
 import type { ActionableItem } from "../Items/ActionableItem";
 import type { ItemFactoryInterface } from "../Items/ItemFactoryInterface";
@@ -90,9 +88,10 @@ import SpriteSheetFile = Phaser.Loader.FileTypes.SpriteSheetFile;
 import { deepCopy } from "deep-copy-ts";
 import FILE_LOAD_ERROR = Phaser.Loader.Events.FILE_LOAD_ERROR;
 import { MapStore } from "../../Stores/Utils/MapStore";
-import { SetPlayerDetailsMessage } from "../../Messages/generated/messages_pb";
 import { followUsersColorStore, followUsersStore } from "../../Stores/FollowStore";
 import { getColorRgbFromHue } from "../../WebRtc/ColorGenerator";
+import Camera = Phaser.Cameras.Scene2D.Camera;
+import type { WasCameraUpdatedEvent } from "../../Api/Events/WasCameraUpdatedEvent";
 
 export interface GameSceneInitInterface {
     initPosition: PointInterface | null;
@@ -210,6 +209,8 @@ export class GameScene extends DirtyScene {
     private objectsByType = new Map<string, ITiledMapObject[]>();
     private embeddedWebsiteManager!: EmbeddedWebsiteManager;
     private loader: Loader;
+    private lastCameraEvent: WasCameraUpdatedEvent | undefined;
+    private firstCameraUpdateSent: boolean = false;
 
     constructor(private room: Room, MapUrlFile: string, customKey?: string | undefined) {
         super({
@@ -240,7 +241,7 @@ export class GameScene extends DirtyScene {
         const textures = localUser?.textures;
         if (textures) {
             for (const texture of textures) {
-                loadCustomTexture(this.load, texture);
+                loadCustomTexture(this.load, texture).catch((e) => console.error(e));
             }
         }
 
@@ -267,7 +268,7 @@ export class GameScene extends DirtyScene {
                 this.load.on(
                     "filecomplete-tilemapJSON-" + this.MapUrlFile,
                     (key: string, type: string, data: unknown) => {
-                        this.onMapLoad(data);
+                        this.onMapLoad(data).catch((e) => console.error(e));
                     }
                 );
                 return;
@@ -291,14 +292,14 @@ export class GameScene extends DirtyScene {
                 this.load.on(
                     "filecomplete-tilemapJSON-" + this.MapUrlFile,
                     (key: string, type: string, data: unknown) => {
-                        this.onMapLoad(data);
+                        this.onMapLoad(data).catch((e) => console.error(e));
                     }
                 );
                 // If the map has already been loaded as part of another GameScene, the "on load" event will not be triggered.
                 // In this case, we check in the cache to see if the map is here and trigger the event manually.
                 if (this.cache.tilemap.exists(this.MapUrlFile)) {
                     const data = this.cache.tilemap.get(this.MapUrlFile);
-                    this.onMapLoad(data);
+                    this.onMapLoad(data).catch((e) => console.error(e));
                 }
                 return;
             }
@@ -319,7 +320,7 @@ export class GameScene extends DirtyScene {
         });
         this.load.scenePlugin("AnimatedTiles", AnimatedTiles, "animatedTiles", "animatedTiles");
         this.load.on("filecomplete-tilemapJSON-" + this.MapUrlFile, (key: string, type: string, data: unknown) => {
-            this.onMapLoad(data);
+            this.onMapLoad(data).catch((e) => console.error(e));
         });
         //TODO strategy to add access token
         this.load.tilemapTiledJSON(this.MapUrlFile, this.MapUrlFile);
@@ -327,7 +328,7 @@ export class GameScene extends DirtyScene {
         // In this case, we check in the cache to see if the map is here and trigger the event manually.
         if (this.cache.tilemap.exists(this.MapUrlFile)) {
             const data = this.cache.tilemap.get(this.MapUrlFile);
-            this.onMapLoad(data);
+            this.onMapLoad(data).catch((e) => console.error(e));
         }
 
         //eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -405,21 +406,23 @@ export class GameScene extends DirtyScene {
             this.load.on("complete", () => {
                 // FIXME: the factory might fail because the resources might not be loaded yet...
                 // We would need to add a loader ended event in addition to the createPromise
-                this.createPromise.then(async () => {
-                    itemFactory.create(this);
+                this.createPromise
+                    .then(async () => {
+                        itemFactory.create(this);
 
-                    const roomJoinedAnswer = await this.connectionAnswerPromise;
+                        const roomJoinedAnswer = await this.connectionAnswerPromise;
 
-                    for (const object of objectsOfType) {
-                        // TODO: we should pass here a factory to create sprites (maybe?)
+                        for (const object of objectsOfType) {
+                            // TODO: we should pass here a factory to create sprites (maybe?)
 
-                        // Do we have a state for this object?
-                        const state = roomJoinedAnswer.items[object.id];
+                            // Do we have a state for this object?
+                            const state = roomJoinedAnswer.items[object.id];
 
-                        const actionableItem = itemFactory.factory(this, object, state);
-                        this.actionableItems.set(actionableItem.getId(), actionableItem);
-                    }
-                });
+                            const actionableItem = itemFactory.factory(this, object, state);
+                            this.actionableItems.set(actionableItem.getId(), actionableItem);
+                        }
+                    })
+                    .catch((e) => console.error(e));
             });
         }
     }
@@ -448,10 +451,6 @@ export class GameScene extends DirtyScene {
             this.pinchManager = new PinchManager(this);
         }
 
-        this.messageSubscription = worldFullMessageStream.stream.subscribe((message) =>
-            this.showWorldFullError(message)
-        );
-
         const playerName = gameManager.getPlayerName();
         if (!playerName) {
             throw "playerName is not set";
@@ -489,11 +488,11 @@ export class GameScene extends DirtyScene {
                 if (exitSceneUrl !== undefined) {
                     this.loadNextGame(
                         Room.getRoomPathFromExitSceneUrl(exitSceneUrl, window.location.toString(), this.MapUrlFile)
-                    );
+                    ).catch((e) => console.error(e));
                 }
                 const exitUrl = this.getExitUrl(layer);
                 if (exitUrl !== undefined) {
-                    this.loadNextGameFromExitUrl(exitUrl);
+                    this.loadNextGameFromExitUrl(exitUrl).catch((e) => console.error(e));
                 }
             }
             if (layer.type === "objectgroup") {
@@ -523,7 +522,9 @@ export class GameScene extends DirtyScene {
                             object.height,
                             object.visible,
                             allowApi ?? false,
-                            ""
+                            "",
+                            "map",
+                            1
                         );
                     }
                 }
@@ -531,7 +532,7 @@ export class GameScene extends DirtyScene {
         }
 
         this.gameMap.exitUrls.forEach((exitUrl) => {
-            this.loadNextGameFromExitUrl(exitUrl);
+            this.loadNextGameFromExitUrl(exitUrl).catch((e) => console.error(e));
         });
 
         this.startPositionCalculator = new StartPositionCalculator(
@@ -552,7 +553,10 @@ export class GameScene extends DirtyScene {
         mediaManager.setUserInputManager(this.userInputManager);
 
         if (localUserStore.getFullscreen()) {
-            document.querySelector("body")?.requestFullscreen();
+            document
+                .querySelector("body")
+                ?.requestFullscreen()
+                .catch((e) => console.error(e));
         }
 
         //notify game manager can to create currentUser in map
@@ -617,8 +621,6 @@ export class GameScene extends DirtyScene {
             this.connect();
         }
 
-        this.emoteManager = new EmoteManager(this);
-
         let oldPeerNumber = 0;
         this.peerStoreUnsubscribe = peerStore.subscribe((peers) => {
             const newPeerNumber = peers.size;
@@ -660,9 +662,16 @@ export class GameScene extends DirtyScene {
             }
         });
 
-        Promise.all([this.connectionAnswerPromise as Promise<unknown>, ...scriptPromises]).then(() => {
-            this.scene.wake();
-        });
+        Promise.all([this.connectionAnswerPromise as Promise<unknown>, ...scriptPromises])
+            .then(() => {
+                this.scene.wake();
+            })
+            .catch((e) =>
+                console.error(
+                    "Some scripts failed to load ot the connection failed to establish to WorkAdventure server",
+                    e
+                )
+            );
     }
 
     /**
@@ -693,7 +702,7 @@ export class GameScene extends DirtyScene {
                 playersStore.connectToRoomConnection(this.connection);
                 userIsAdminStore.set(this.connection.hasTag("admin"));
 
-                this.connection.onUserJoins((message: MessageUserJoined) => {
+                this.connection.userJoinedMessageStream.subscribe((message) => {
                     const userMessage: AddPlayerInterface = {
                         userId: message.userId,
                         characterLayers: message.characterLayers,
@@ -707,31 +716,33 @@ export class GameScene extends DirtyScene {
                     this.addPlayer(userMessage);
                 });
 
-                this.connection.onUserMoved((message: UserMovedMessage) => {
-                    const position = message.getPosition();
+                this.connection.userMovedMessageStream.subscribe((message) => {
+                    const position = message.position;
                     if (position === undefined) {
                         throw new Error("Position missing from UserMovedMessage");
                     }
 
                     const messageUserMoved: MessageUserMovedInterface = {
-                        userId: message.getUserid(),
+                        userId: message.userId,
                         position: ProtobufClientUtils.toPointInterface(position),
                     };
 
                     this.updatePlayerPosition(messageUserMoved);
                 });
 
-                this.connection.onUserLeft((userId: number) => {
-                    this.removePlayer(userId);
+                this.connection.userLeftMessageStream.subscribe((message) => {
+                    this.removePlayer(message.userId);
                 });
 
-                this.connection.onGroupUpdatedOrCreated((groupPositionMessage: GroupCreatedUpdatedMessageInterface) => {
-                    this.shareGroupPosition(groupPositionMessage);
-                });
+                this.connection.groupUpdateMessageStream.subscribe(
+                    (groupPositionMessage: GroupCreatedUpdatedMessageInterface) => {
+                        this.shareGroupPosition(groupPositionMessage);
+                    }
+                );
 
-                this.connection.onGroupDeleted((groupId: number) => {
+                this.connection.groupDeleteMessageStream.subscribe((message) => {
                     try {
-                        this.deleteGroup(groupId);
+                        this.deleteGroup(message.groupId);
                     } catch (e) {
                         console.error(e);
                     }
@@ -743,7 +754,7 @@ export class GameScene extends DirtyScene {
                     this.createSuccessorGameScene(true, true);
                 });
 
-                this.connection.onActionableEvent((message) => {
+                this.connection.itemEventMessageStream.subscribe((message) => {
                     const item = this.actionableItems.get(message.itemId);
                     if (item === undefined) {
                         console.warn(
@@ -756,18 +767,29 @@ export class GameScene extends DirtyScene {
                     item.fire(message.event, message.state, message.parameters);
                 });
 
-                this.connection.onPlayerDetailsUpdated((message) => {
+                this.connection.playerDetailsUpdatedMessageStream.subscribe((message) => {
+                    if (message.details === undefined) {
+                        throw new Error("Malformed message. Missing details in PlayerDetailsUpdatedMessage");
+                    }
                     this.pendingEvents.enqueue({
                         type: "PlayerDetailsUpdated",
-                        details: message,
+                        details: {
+                            userId: message.userId,
+                            outlineColor: message.details.outlineColor,
+                            removeOutlineColor: message.details.removeOutlineColor,
+                        },
                     });
                 });
 
                 /**
                  * Triggered when we receive the JWT token to connect to Jitsi
                  */
-                this.connection.onStartJitsiRoom((jwt, room) => {
-                    this.startJitsi(room, jwt);
+                this.connection.sendJitsiJwtMessageStream.subscribe((message) => {
+                    this.startJitsi(message.jitsiRoom, message.jwt);
+                });
+
+                this.messageSubscription = this.connection.worldFullMessageStream.subscribe((message) => {
+                    this.showWorldFullError(message);
                 });
 
                 // When connection is performed, let's connect SimplePeer
@@ -842,12 +864,15 @@ export class GameScene extends DirtyScene {
                     });
                 });
 
+                this.emoteManager = new EmoteManager(this, this.connection);
+
                 // this.gameMap.onLeaveLayer((layers) => {
                 //     layers.forEach((layer) => {
                 //         iframeListener.sendLeaveLayerEvent(layer.name);
                 //     });
                 // });
-            });
+            })
+            .catch((e) => console.error(e));
     }
 
     //todo: into dedicated classes
@@ -900,7 +925,7 @@ export class GameScene extends DirtyScene {
             if (newValue) {
                 this.onMapExit(
                     Room.getRoomPathFromExitSceneUrl(newValue as string, window.location.toString(), this.MapUrlFile)
-                );
+                ).catch((e) => console.error(e));
             } else {
                 setTimeout(() => {
                     layoutManagerActionStore.removeAction("roomAccessDenied");
@@ -909,7 +934,9 @@ export class GameScene extends DirtyScene {
         });
         this.gameMap.onPropertyChange(GameMapProperties.EXIT_URL, (newValue, oldValue) => {
             if (newValue) {
-                this.onMapExit(Room.getRoomPathFromExitUrl(newValue as string, window.location.toString()));
+                this.onMapExit(Room.getRoomPathFromExitUrl(newValue as string, window.location.toString())).catch((e) =>
+                    console.error(e)
+                );
             } else {
                 setTimeout(() => {
                     layoutManagerActionStore.removeAction("roomAccessDenied");
@@ -1095,21 +1122,47 @@ ${escapedMessage}
         this.iframeSubscriptionList.push(
             iframeListener.playSoundStream.subscribe((playSoundEvent) => {
                 const url = new URL(playSoundEvent.url, this.MapUrlFile);
-                soundManager.playSound(this.load, this.sound, url.toString(), playSoundEvent.config);
+                soundManager
+                    .playSound(this.load, this.sound, url.toString(), playSoundEvent.config)
+                    .catch((e) => console.error(e));
             })
         );
 
         this.iframeSubscriptionList.push(
-            iframeListener.stopSoundStream.subscribe((stopSoundEvent) => {
-                const url = new URL(stopSoundEvent.url, this.MapUrlFile);
-                soundManager.stopSound(this.sound, url.toString());
+            iframeListener.trackCameraUpdateStream.subscribe(() => {
+                if (!this.firstCameraUpdateSent) {
+                    this.cameras.main.on("followupdate", (camera: Camera) => {
+                        const cameraEvent: WasCameraUpdatedEvent = {
+                            x: camera.worldView.x,
+                            y: camera.worldView.y,
+                            width: camera.worldView.width,
+                            height: camera.worldView.height,
+                            zoom: camera.scaleManager.zoom,
+                        };
+                        if (
+                            this.lastCameraEvent?.x == cameraEvent.x &&
+                            this.lastCameraEvent?.y == cameraEvent.y &&
+                            this.lastCameraEvent?.width == cameraEvent.width &&
+                            this.lastCameraEvent?.height == cameraEvent.height &&
+                            this.lastCameraEvent?.zoom == cameraEvent.zoom
+                        ) {
+                            return;
+                        }
+
+                        this.lastCameraEvent = cameraEvent;
+                        iframeListener.sendCameraUpdated(cameraEvent);
+                        this.firstCameraUpdateSent = true;
+                    });
+
+                    iframeListener.sendCameraUpdated(this.cameras.main);
+                }
             })
         );
 
         this.iframeSubscriptionList.push(
             iframeListener.loadSoundStream.subscribe((loadSoundEvent) => {
                 const url = new URL(loadSoundEvent.url, this.MapUrlFile);
-                soundManager.loadSound(this.load, this.sound, url.toString());
+                soundManager.loadSound(this.load, this.sound, url.toString()).catch((e) => console.error(e));
             })
         );
 
@@ -1120,11 +1173,15 @@ ${escapedMessage}
         );
         this.iframeSubscriptionList.push(
             iframeListener.loadPageStream.subscribe((url: string) => {
-                this.loadNextGameFromExitUrl(url).then(() => {
-                    this.events.once(EVENT_TYPE.POST_UPDATE, () => {
-                        this.onMapExit(Room.getRoomPathFromExitUrl(url, window.location.toString()));
-                    });
-                });
+                this.loadNextGameFromExitUrl(url)
+                    .then(() => {
+                        this.events.once(EVENT_TYPE.POST_UPDATE, () => {
+                            this.onMapExit(Room.getRoomPathFromExitUrl(url, window.location.toString())).catch((e) =>
+                                console.error(e)
+                            );
+                        });
+                    })
+                    .catch((e) => console.error(e));
             })
         );
         let scriptedBubbleSprite: Sprite;
@@ -1165,6 +1222,12 @@ ${escapedMessage}
             })
         );
 
+        this.iframeSubscriptionList.push(
+            iframeListener.setPropertyStream.subscribe((setProperty) => {
+                this.setPropertyLayer(setProperty.layerName, setProperty.propertyName, setProperty.propertyValue);
+            })
+        );
+
         iframeListener.registerAnswerer("openCoWebsite", async (openCoWebsite, source) => {
             if (!source) {
                 throw new Error("Unknown query source");
@@ -1235,6 +1298,7 @@ ${escapedMessage}
                 roomId: this.roomUrl,
                 tags: this.connection ? this.connection.getAllTags() : [],
                 variables: this.sharedVariablesManager.variables,
+                playerVariables: localUserStore.getAllUserProperties(),
                 userRoomToken: this.connection ? this.connection.userRoomToken : "",
             };
         });
@@ -1325,6 +1389,22 @@ ${escapedMessage}
             })
         );
 
+        iframeListener.registerAnswerer("setVariable", (event, source) => {
+            switch (event.target) {
+                case "global": {
+                    this.sharedVariablesManager.setVariable(event, source);
+                    break;
+                }
+                case "player": {
+                    localUserStore.setUserProperty(event.key, event.value);
+                    break;
+                }
+                default: {
+                    const _exhaustiveCheck: never = event.target;
+                }
+            }
+        });
+
         iframeListener.registerAnswerer("removeActionMessage", (message) => {
             layoutManagerActionStore.removeAction(message.uuid);
         });
@@ -1343,6 +1423,13 @@ ${escapedMessage}
             this.CurrentPlayer.removeOutlineColor();
             this.connection?.emitPlayerOutlineColor(null);
         });
+
+        iframeListener.registerAnswerer("getPlayerPosition", () => {
+            return {
+                x: this.CurrentPlayer.x,
+                y: this.CurrentPlayer.y,
+            };
+        });
     }
 
     private setPropertyLayer(
@@ -1351,7 +1438,7 @@ ${escapedMessage}
         propertyValue: string | number | boolean | undefined
     ): void {
         if (propertyName === GameMapProperties.EXIT_URL && typeof propertyValue === "string") {
-            this.loadNextGameFromExitUrl(propertyValue);
+            this.loadNextGameFromExitUrl(propertyValue).catch((e) => console.error(e));
         }
         this.gameMap.setLayerProperty(layerName, propertyName, propertyValue);
     }
@@ -1436,7 +1523,7 @@ ${escapedMessage}
 
     public cleanupClosingScene(): void {
         // stop playing audio, close any open website, stop any open Jitsi
-        coWebsiteManager.closeCoWebsites();
+        coWebsiteManager.closeCoWebsites().catch((e) => console.error(e));
         // Stop the script, if any
         const scripts = this.getScriptUrls(this.mapFile);
         for (const script of scripts) {
@@ -1467,6 +1554,7 @@ ${escapedMessage}
         iframeListener.unregisterAnswerer("openCoWebsite");
         iframeListener.unregisterAnswerer("getCoWebsites");
         iframeListener.unregisterAnswerer("setPlayerOutline");
+        iframeListener.unregisterAnswerer("setVariable");
         this.sharedVariablesManager?.close();
         this.embeddedWebsiteManager?.close();
 
@@ -1945,6 +2033,7 @@ ${escapedMessage}
 
         this.loader.resize();
     }
+
     private getObjectLayerData(objectName: string): ITiledMapObject | undefined {
         for (const layer of this.mapFile.layers) {
             if (layer.type === "objectgroup" && layer.name === "floorLayer") {
@@ -1957,6 +2046,7 @@ ${escapedMessage}
         }
         return undefined;
     }
+
     private reposition(): void {
         // Recompute camera offset if needed
         biggestAvailableAreaStore.recompute();
@@ -1975,7 +2065,9 @@ ${escapedMessage}
         const jitsiUrl = allProps.get(GameMapProperties.JITSI_URL) as string | undefined;
         const jitsiWidth = allProps.get(GameMapProperties.JITSI_WIDTH) as number | undefined;
 
-        jitsiFactory.start(roomName, this.playerName, jwt, jitsiConfig, jitsiInterfaceConfig, jitsiUrl, jitsiWidth);
+        jitsiFactory
+            .start(roomName, this.playerName, jwt, jitsiConfig, jitsiInterfaceConfig, jitsiUrl, jitsiWidth)
+            .catch((e) => console.error(e));
         this.connection?.setSilent(true);
         mediaManager.hideGameOverlay();
         analyticsClient.enteredJitsi(roomName, this.room.id);
diff --git a/front/src/Phaser/Game/SharedVariablesManager.ts b/front/src/Phaser/Game/SharedVariablesManager.ts
index 5b5867dc..3c019e74 100644
--- a/front/src/Phaser/Game/SharedVariablesManager.ts
+++ b/front/src/Phaser/Game/SharedVariablesManager.ts
@@ -3,6 +3,7 @@ import { iframeListener } from "../../Api/IframeListener";
 import type { GameMap } from "./GameMap";
 import type { ITiledMapLayer, ITiledMapObject } from "../Map/ITiledMap";
 import { GameMapProperties } from "./GameMapProperties";
+import type { SetVariableEvent } from "../../Api/Events/SetVariableEvent";
 
 interface Variable {
     defaultValue: unknown;
@@ -41,58 +42,58 @@ export class SharedVariablesManager {
             this._variables.set(name, value);
         }
 
-        roomConnection.onSetVariable((name, value) => {
+        roomConnection.variableMessageStream.subscribe(({ name, value }) => {
             this._variables.set(name, value);
 
             // On server change, let's notify the iframes
             iframeListener.setVariable({
                 key: name,
                 value: value,
+                target: "global",
             });
         });
+    }
 
-        // When a variable is modified from an iFrame
-        iframeListener.registerAnswerer("setVariable", (event, source) => {
-            const key = event.key;
+    public setVariable(event: SetVariableEvent, source: MessageEventSource | null): void {
+        const key = event.key;
 
-            const object = this.variableObjects.get(key);
+        const object = this.variableObjects.get(key);
 
-            if (object === undefined) {
-                const errMsg =
-                    'A script is trying to modify variable "' +
-                    key +
-                    '" but this variable is not defined in the map.' +
-                    'There should be an object in the map whose name is "' +
-                    key +
-                    '" and whose type is "variable"';
-                console.error(errMsg);
-                throw new Error(errMsg);
-            }
+        if (object === undefined) {
+            const errMsg =
+                'A script is trying to modify variable "' +
+                key +
+                '" but this variable is not defined in the map.' +
+                'There should be an object in the map whose name is "' +
+                key +
+                '" and whose type is "variable"';
+            console.error(errMsg);
+            throw new Error(errMsg);
+        }
 
-            if (object.writableBy && !this.roomConnection.hasTag(object.writableBy)) {
-                const errMsg =
-                    'A script is trying to modify variable "' +
-                    key +
-                    '" but this variable is only writable for users with tag "' +
-                    object.writableBy +
-                    '".';
-                console.error(errMsg);
-                throw new Error(errMsg);
-            }
+        if (object.writableBy && !this.roomConnection.hasTag(object.writableBy)) {
+            const errMsg =
+                'A script is trying to modify variable "' +
+                key +
+                '" but this variable is only writable for users with tag "' +
+                object.writableBy +
+                '".';
+            console.error(errMsg);
+            throw new Error(errMsg);
+        }
 
-            // Let's stop any propagation of the value we set is the same as the existing value.
-            if (JSON.stringify(event.value) === JSON.stringify(this._variables.get(key))) {
-                return;
-            }
+        // Let's stop any propagation of the value we set is the same as the existing value.
+        if (JSON.stringify(event.value) === JSON.stringify(this._variables.get(key))) {
+            return;
+        }
 
-            this._variables.set(key, event.value);
+        this._variables.set(key, event.value);
 
-            // Dispatch to the room connection.
-            this.roomConnection.emitSetVariableEvent(key, event.value);
+        // Dispatch to the room connection.
+        this.roomConnection.emitSetVariableEvent(key, event.value);
 
-            // Dispatch to other iframes
-            iframeListener.dispatchVariableToOtherIframes(key, event.value, source);
-        });
+        // Dispatch to other iframes
+        iframeListener.dispatchVariableToOtherIframes(key, event.value, source);
     }
 
     private static findVariablesInMap(gameMap: GameMap): Map<string, Variable> {
diff --git a/front/src/Phaser/Login/CustomizeScene.ts b/front/src/Phaser/Login/CustomizeScene.ts
index d5629c88..5c208edd 100644
--- a/front/src/Phaser/Login/CustomizeScene.ts
+++ b/front/src/Phaser/Login/CustomizeScene.ts
@@ -40,19 +40,21 @@ export class CustomizeScene extends AbstractCharacterScene {
     }
 
     preload() {
-        this.loadCustomSceneSelectCharacters().then((bodyResourceDescriptions) => {
-            bodyResourceDescriptions.forEach((bodyResourceDescription) => {
-                if (
-                    bodyResourceDescription.level == undefined ||
-                    bodyResourceDescription.level < 0 ||
-                    bodyResourceDescription.level > 5
-                ) {
-                    throw "Texture level is null";
-                }
-                this.layers[bodyResourceDescription.level].unshift(bodyResourceDescription);
-            });
-            this.lazyloadingAttempt = true;
-        });
+        this.loadCustomSceneSelectCharacters()
+            .then((bodyResourceDescriptions) => {
+                bodyResourceDescriptions.forEach((bodyResourceDescription) => {
+                    if (
+                        bodyResourceDescription.level == undefined ||
+                        bodyResourceDescription.level < 0 ||
+                        bodyResourceDescription.level > 5
+                    ) {
+                        throw "Texture level is null";
+                    }
+                    this.layers[bodyResourceDescription.level].unshift(bodyResourceDescription);
+                });
+                this.lazyloadingAttempt = true;
+            })
+            .catch((e) => console.error(e));
 
         this.layers = loadAllLayers(this.load);
         this.lazyloadingAttempt = false;
diff --git a/front/src/Phaser/Login/SelectCharacterScene.ts b/front/src/Phaser/Login/SelectCharacterScene.ts
index 64fa9791..4e372e0e 100644
--- a/front/src/Phaser/Login/SelectCharacterScene.ts
+++ b/front/src/Phaser/Login/SelectCharacterScene.ts
@@ -41,12 +41,14 @@ export class SelectCharacterScene extends AbstractCharacterScene {
     }
 
     preload() {
-        this.loadSelectSceneCharacters().then((bodyResourceDescriptions) => {
-            bodyResourceDescriptions.forEach((bodyResourceDescription) => {
-                this.playerModels.push(bodyResourceDescription);
-            });
-            this.lazyloadingAttempt = true;
-        });
+        this.loadSelectSceneCharacters()
+            .then((bodyResourceDescriptions) => {
+                bodyResourceDescriptions.forEach((bodyResourceDescription) => {
+                    this.playerModels.push(bodyResourceDescription);
+                });
+                this.lazyloadingAttempt = true;
+            })
+            .catch((e) => console.error(e));
         this.playerModels = loadAllDefaultModels(this.load);
         this.lazyloadingAttempt = false;
 
diff --git a/front/src/Phaser/Map/ITiledMap.ts b/front/src/Phaser/Map/ITiledMap.ts
index 57bb13c9..74810df4 100644
--- a/front/src/Phaser/Map/ITiledMap.ts
+++ b/front/src/Phaser/Map/ITiledMap.ts
@@ -162,6 +162,7 @@ export interface ITiledTileSet {
 
     imageheight: number;
     imagewidth: number;
+    columns: number;
     margin: number;
     name: string;
     properties?: ITiledMapProperty[];
diff --git a/front/src/Phaser/Player/Player.ts b/front/src/Phaser/Player/Player.ts
index 946bb6c4..e41b3237 100644
--- a/front/src/Phaser/Player/Player.ts
+++ b/front/src/Phaser/Player/Player.ts
@@ -100,10 +100,6 @@ export class Player extends Character {
         return [xMovement, yMovement];
     }
 
-    public enableFollowing() {
-        followStateStore.set("active");
-    }
-
     public moveUser(delta: number): void {
         const activeEvents = this.userInputManager.getEventListForGameTick();
         const state = get(followStateStore);
@@ -111,8 +107,7 @@ export class Player extends Character {
 
         if (activeEvents.get(UserInputEvent.Follow)) {
             if (state === "off" && this.scene.groups.size > 0) {
-                followStateStore.set("requesting");
-                followRoleStore.set("leader");
+                this.sendFollowRequest();
             } else if (state === "active") {
                 followStateStore.set("ending");
             }
@@ -125,4 +120,15 @@ export class Player extends Character {
         }
         this.inputStep(activeEvents, x, y);
     }
+
+    public sendFollowRequest() {
+        this.scene.connection?.emitFollowRequest();
+        followRoleStore.set("leader");
+        followStateStore.set("active");
+    }
+
+    public startFollowing() {
+        followStateStore.set("active");
+        this.scene.connection?.emitFollowConfirmation();
+    }
 }
diff --git a/front/src/Phaser/Services/WaScaleManager.ts b/front/src/Phaser/Services/WaScaleManager.ts
index 447b6a1f..c0c01402 100644
--- a/front/src/Phaser/Services/WaScaleManager.ts
+++ b/front/src/Phaser/Services/WaScaleManager.ts
@@ -31,6 +31,10 @@ export class WaScaleManager {
             height: height * devicePixelRatio,
         });
 
+        if (gameSize.width == 0) {
+            return;
+        }
+
         this.actualZoom = realSize.width / gameSize.width / devicePixelRatio;
 
         this.scaleManager.setZoom(realSize.width / gameSize.width / devicePixelRatio);
diff --git a/front/src/Stores/FollowStore.ts b/front/src/Stores/FollowStore.ts
index ab1e61d1..eb9753d9 100644
--- a/front/src/Stores/FollowStore.ts
+++ b/front/src/Stores/FollowStore.ts
@@ -58,7 +58,6 @@ export const followUsersStore = createFollowUsersStore();
 export const followUsersColorStore = derived(
     [followStateStore, followRoleStore, followUsersStore],
     ([$followStateStore, $followRoleStore, $followUsersStore]) => {
-        console.log($followStateStore);
         if ($followStateStore !== "active") {
             return undefined;
         }
diff --git a/front/src/Stores/GameStore.ts b/front/src/Stores/GameStore.ts
index eada6d26..e5298b7c 100644
--- a/front/src/Stores/GameStore.ts
+++ b/front/src/Stores/GameStore.ts
@@ -5,3 +5,5 @@ export const userMovingStore = writable(false);
 export const requestVisitCardsStore = writable<string | null>(null);
 
 export const userIsAdminStore = writable(false);
+
+export const limitMapStore = writable(false);
diff --git a/front/src/Stores/MediaStore.ts b/front/src/Stores/MediaStore.ts
index a0f1a92b..7eeac35d 100644
--- a/front/src/Stores/MediaStore.ts
+++ b/front/src/Stores/MediaStore.ts
@@ -360,32 +360,27 @@ const implementCorrectTrackBehavior = getNavigatorType() === NavigatorType.firef
 /**
  * Stops the camera from filming
  */
-function applyCameraConstraints(currentStream: MediaStream | null, constraints: MediaTrackConstraints | boolean): void {
+async function applyCameraConstraints(
+    currentStream: MediaStream | null,
+    constraints: MediaTrackConstraints | boolean
+): Promise<void[]> {
     if (!currentStream) {
-        return;
-    }
-    for (const track of currentStream.getVideoTracks()) {
-        toggleConstraints(track, constraints).catch((e) =>
-            console.error("Error while setting new camera constraints:", e)
-        );
+        return [];
     }
+    return Promise.all(currentStream.getVideoTracks().map((track) => toggleConstraints(track, constraints)));
 }
 
 /**
  * Stops the microphone from listening
  */
-function applyMicrophoneConstraints(
+async function applyMicrophoneConstraints(
     currentStream: MediaStream | null,
     constraints: MediaTrackConstraints | boolean
-): void {
+): Promise<void[]> {
     if (!currentStream) {
-        return;
-    }
-    for (const track of currentStream.getAudioTracks()) {
-        toggleConstraints(track, constraints).catch((e) =>
-            console.error("Error while setting new audio constraints:", e)
-        );
+        return [];
     }
+    return Promise.all(currentStream.getAudioTracks().map((track) => toggleConstraints(track, constraints)));
 }
 
 async function toggleConstraints(track: MediaStreamTrack, constraints: MediaTrackConstraints | boolean): Promise<void> {
@@ -477,8 +472,8 @@ export const localStreamStore = derived<Readable<MediaStreamConstraints>, LocalS
             }
         }
 
-        applyMicrophoneConstraints(currentStream, constraints.audio || false);
-        applyCameraConstraints(currentStream, constraints.video || false);
+        applyMicrophoneConstraints(currentStream, constraints.audio || false).catch((e) => console.error(e));
+        applyCameraConstraints(currentStream, constraints.video || false).catch((e) => console.error(e));
 
         if (implementCorrectTrackBehavior) {
             //on good navigators like firefox, we can instantiate the stream once and simply disable or enable the tracks as needed
diff --git a/front/src/Stores/ModalStore.ts b/front/src/Stores/ModalStore.ts
new file mode 100644
index 00000000..3e99cde9
--- /dev/null
+++ b/front/src/Stores/ModalStore.ts
@@ -0,0 +1,4 @@
+import { writable } from "svelte/store";
+
+export const showLimitRoomModalStore = writable(false);
+export const showShareLinkMapModalStore = writable(false);
diff --git a/front/src/Stores/PlayersStore.ts b/front/src/Stores/PlayersStore.ts
index 07c18b96..0676235a 100644
--- a/front/src/Stores/PlayersStore.ts
+++ b/front/src/Stores/PlayersStore.ts
@@ -3,6 +3,7 @@ import type { PlayerInterface } from "../Phaser/Game/PlayerInterface";
 import type { RoomConnection } from "../Connexion/RoomConnection";
 import { getRandomColor } from "../WebRtc/ColorGenerator";
 import { localUserStore } from "../Connexion/LocalUserStore";
+import room from "../Api/iframe/room";
 
 let idCount = 0;
 
@@ -19,7 +20,8 @@ function createPlayersStore() {
         connectToRoomConnection: (roomConnection: RoomConnection) => {
             players = new Map<number, PlayerInterface>();
             set(players);
-            roomConnection.onUserJoins((message) => {
+            // TODO: it would be cool to unsubscribe properly here
+            roomConnection.userJoinedMessageStream.subscribe((message) => {
                 update((users) => {
                     users.set(message.userId, {
                         userId: message.userId,
@@ -33,9 +35,9 @@ function createPlayersStore() {
                     return users;
                 });
             });
-            roomConnection.onUserLeft((userId) => {
+            roomConnection.userLeftMessageStream.subscribe((message) => {
                 update((users) => {
-                    users.delete(userId);
+                    users.delete(message.userId);
                     return users;
                 });
             });
diff --git a/front/src/Stores/ScreenSharingStore.ts b/front/src/Stores/ScreenSharingStore.ts
index d68dbf8b..dc2c495c 100644
--- a/front/src/Stores/ScreenSharingStore.ts
+++ b/front/src/Stores/ScreenSharingStore.ts
@@ -156,7 +156,7 @@ export const screenSharingLocalStreamStore = derived<Readable<MediaStreamConstra
                     error: e instanceof Error ? e : new Error("An unknown error happened"),
                 });
             }
-        })();
+        })().catch((e) => console.error(e));
     }
 );
 
diff --git a/front/src/Url/UrlManager.ts b/front/src/Url/UrlManager.ts
index 50dbedc9..011efa5a 100644
--- a/front/src/Url/UrlManager.ts
+++ b/front/src/Url/UrlManager.ts
@@ -2,8 +2,7 @@ import type { Room } from "../Connexion/Room";
 import { localUserStore } from "../Connexion/LocalUserStore";
 
 export enum GameConnexionTypes {
-    anonymous = 1,
-    organization,
+    room = 1,
     register,
     empty,
     unknown,
@@ -19,10 +18,8 @@ class UrlManager {
             return GameConnexionTypes.login;
         } else if (url === "/jwt") {
             return GameConnexionTypes.jwt;
-        } else if (url.includes("_/")) {
-            return GameConnexionTypes.anonymous;
-        } else if (url.includes("@/")) {
-            return GameConnexionTypes.organization;
+        } else if (url.includes("_/") || url.includes("*/") || url.includes("@/")) {
+            return GameConnexionTypes.room;
         } else if (url.includes("register/")) {
             return GameConnexionTypes.register;
         } else if (url === "/") {
@@ -41,7 +38,7 @@ class UrlManager {
         if (window.location.pathname === room.id) return;
         //Set last room visited! (connected or nor, must to be saved in localstorage and cache API)
         //use href to keep # value
-        localUserStore.setLastRoomUrl(room.href);
+        localUserStore.setLastRoomUrl(room.href).catch((e) => console.error(e));
         const hash = window.location.hash;
         const search = room.search.toString();
         history.pushState({}, "WorkAdventure", room.id + (search ? "?" + search : "") + hash);
diff --git a/front/src/WebRtc/CoWebsiteManager.ts b/front/src/WebRtc/CoWebsiteManager.ts
index 7a003604..8bff2acb 100644
--- a/front/src/WebRtc/CoWebsiteManager.ts
+++ b/front/src/WebRtc/CoWebsiteManager.ts
@@ -149,7 +149,7 @@ class CoWebsiteManager {
             }
 
             buttonCloseCoWebsites.blur();
-            this.closeCoWebsites();
+            this.closeCoWebsites().catch((e) => console.error(e));
         });
 
         const buttonFullScreenFrame = HtmlUtils.getElementByIdOrFail(cowebsiteFullScreenButtonId);
@@ -515,70 +515,72 @@ class CoWebsiteManager {
                 throw new Error("Too many we");
             }
 
-            Promise.resolve(callback(this.cowebsiteBufferDom)).then((iframe) => {
-                iframe?.classList.add("pixel");
+            Promise.resolve(callback(this.cowebsiteBufferDom))
+                .then((iframe) => {
+                    iframe?.classList.add("pixel");
 
-                if (!iframe.id) {
-                    do {
-                        iframe.id = "cowebsite-iframe-" + (Math.random() + 1).toString(36).substring(7);
-                    } while (this.getCoWebsiteById(iframe.id));
-                }
-
-                const onloadPromise = new Promise<void>((resolve) => {
-                    iframe.onload = () => resolve();
-                });
-
-                const icon = this.generateCoWebsiteIcon(iframe);
-
-                const coWebsite = {
-                    iframe,
-                    icon,
-                    position: position ?? this.coWebsites.length,
-                };
-
-                // Iframe management on mobile
-                icon.addEventListener("click", () => {
-                    if (this.isSmallScreen()) {
-                        this.moveRightPreviousCoWebsite(coWebsite, 0);
+                    if (!iframe.id) {
+                        do {
+                            iframe.id = "cowebsite-iframe-" + (Math.random() + 1).toString(36).substring(7);
+                        } while (this.getCoWebsiteById(iframe.id));
                     }
-                });
 
-                this.coWebsites.push(coWebsite);
-                this.cowebsiteSubIconsDom.appendChild(icon);
+                    const onloadPromise = new Promise<void>((resolve) => {
+                        iframe.onload = () => resolve();
+                    });
 
-                const onTimeoutPromise = new Promise<void>((resolve) => {
-                    setTimeout(() => resolve(), 2000);
-                });
+                    const icon = this.generateCoWebsiteIcon(iframe);
 
-                this.currentOperationPromise = this.currentOperationPromise
-                    .then(() => Promise.race([onloadPromise, onTimeoutPromise]))
-                    .then(() => {
-                        if (coWebsite.position === 0) {
-                            this.openMain();
-                            if (widthPercent) {
-                                this.widthPercent = widthPercent;
-                            }
+                    const coWebsite = {
+                        iframe,
+                        icon,
+                        position: position ?? this.coWebsites.length,
+                    };
 
-                            setTimeout(() => {
-                                this.fire();
+                    // Iframe management on mobile
+                    icon.addEventListener("click", () => {
+                        if (this.isSmallScreen()) {
+                            this.moveRightPreviousCoWebsite(coWebsite, 0);
+                        }
+                    });
+
+                    this.coWebsites.push(coWebsite);
+                    this.cowebsiteSubIconsDom.appendChild(icon);
+
+                    const onTimeoutPromise = new Promise<void>((resolve) => {
+                        setTimeout(() => resolve(), 2000);
+                    });
+
+                    this.currentOperationPromise = this.currentOperationPromise
+                        .then(() => Promise.race([onloadPromise, onTimeoutPromise]))
+                        .then(() => {
+                            if (coWebsite.position === 0) {
+                                this.openMain();
+                                if (widthPercent) {
+                                    this.widthPercent = widthPercent;
+                                }
+
+                                setTimeout(() => {
+                                    this.fire();
+                                    position !== undefined
+                                        ? this.moveRightPreviousCoWebsite(coWebsite, coWebsite.position)
+                                        : this.moveCoWebsite(coWebsite, coWebsite.position);
+                                }, animationTime);
+                            } else {
                                 position !== undefined
                                     ? this.moveRightPreviousCoWebsite(coWebsite, coWebsite.position)
                                     : this.moveCoWebsite(coWebsite, coWebsite.position);
-                            }, animationTime);
-                        } else {
-                            position !== undefined
-                                ? this.moveRightPreviousCoWebsite(coWebsite, coWebsite.position)
-                                : this.moveCoWebsite(coWebsite, coWebsite.position);
-                        }
+                            }
 
-                        return resolve(coWebsite);
-                    })
-                    .catch((err) => {
-                        console.error("Error loadCoWebsite => ", err);
-                        this.removeCoWebsiteFromStack(coWebsite);
-                        return reject();
-                    });
-            });
+                            return resolve(coWebsite);
+                        })
+                        .catch((err) => {
+                            console.error("Error loadCoWebsite => ", err);
+                            this.removeCoWebsiteFromStack(coWebsite);
+                            return reject();
+                        });
+                })
+                .catch((e) => console.error("Error loadCoWebsite >=> ", e));
         });
     }
 
@@ -603,17 +605,21 @@ class CoWebsiteManager {
         return this.currentOperationPromise;
     }
 
-    public closeJitsi() {
+    public async closeJitsi() {
         const jitsi = this.searchJitsi();
         if (jitsi) {
-            this.closeCoWebsite(jitsi);
+            return this.closeCoWebsite(jitsi);
         }
     }
 
     public closeCoWebsites(): Promise<void> {
         this.currentOperationPromise = this.currentOperationPromise.then(() => {
+            const promises: Promise<void>[] = [];
             this.coWebsites.forEach((coWebsite: CoWebsite) => {
-                this.closeCoWebsite(coWebsite);
+                promises.push(this.closeCoWebsite(coWebsite));
+            });
+            return Promise.all(promises).then(() => {
+                return;
             });
         });
         return this.currentOperationPromise;
diff --git a/front/src/WebRtc/JitsiFactory.ts b/front/src/WebRtc/JitsiFactory.ts
index 0f205f47..c067a255 100644
--- a/front/src/WebRtc/JitsiFactory.ts
+++ b/front/src/WebRtc/JitsiFactory.ts
@@ -1,5 +1,5 @@
 import { JITSI_URL } from "../Enum/EnvironmentVariable";
-import { coWebsiteManager } from "./CoWebsiteManager";
+import { CoWebsite, coWebsiteManager } from "./CoWebsiteManager";
 import { requestedCameraState, requestedMicrophoneState } from "../Stores/MediaStore";
 import { get } from "svelte/store";
 
@@ -140,8 +140,8 @@ class JitsiFactory {
         interfaceConfig?: object,
         jitsiUrl?: string,
         jitsiWidth?: number
-    ): void {
-        coWebsiteManager.addCoWebsite(
+    ): Promise<CoWebsite> {
+        return coWebsiteManager.addCoWebsite(
             async (cowebsiteDiv) => {
                 // Jitsi meet external API maintains some data in local storage
                 // which is sent via the appData URL parameter when joining a
@@ -200,7 +200,7 @@ class JitsiFactory {
         const jitsiCoWebsite = coWebsiteManager.searchJitsi();
 
         if (jitsiCoWebsite) {
-            coWebsiteManager.closeJitsi();
+            coWebsiteManager.closeJitsi().catch((e) => console.error(e));
         }
 
         this.jitsiApi.removeListener("audioMuteStatusChanged", this.audioCallback);
diff --git a/front/src/WebRtc/SimplePeer.ts b/front/src/WebRtc/SimplePeer.ts
index ccbd0012..f4016015 100644
--- a/front/src/WebRtc/SimplePeer.ts
+++ b/front/src/WebRtc/SimplePeer.ts
@@ -75,23 +75,25 @@ export class SimplePeer {
      */
     private initialise() {
         //receive signal by gemer
-        this.Connection.receiveWebrtcSignal((message: WebRtcSignalReceivedMessageInterface) => {
+        this.Connection.webRtcSignalToClientMessageStream.subscribe((message: WebRtcSignalReceivedMessageInterface) => {
             this.receiveWebrtcSignal(message);
         });
 
         //receive signal by gemer
-        this.Connection.receiveWebrtcScreenSharingSignal((message: WebRtcSignalReceivedMessageInterface) => {
-            this.receiveWebrtcScreenSharingSignal(message);
-        });
+        this.Connection.webRtcScreenSharingSignalToClientMessageStream.subscribe(
+            (message: WebRtcSignalReceivedMessageInterface) => {
+                this.receiveWebrtcScreenSharingSignal(message);
+            }
+        );
 
         mediaManager.showGameOverlay();
 
         //receive message start
-        this.Connection.receiveWebrtcStart((message: UserSimplePeerInterface) => {
+        this.Connection.webRtcStartMessageStream.subscribe((message: UserSimplePeerInterface) => {
             this.receiveWebrtcStart(message);
         });
 
-        this.Connection.disconnectMessage((data: WebRtcDisconnectMessageInterface): void => {
+        this.Connection.webRtcDisconnectMessageStream.subscribe((data: WebRtcDisconnectMessageInterface): void => {
             this.closeConnection(data.userId);
         });
     }
diff --git a/front/src/iframe_api.ts b/front/src/iframe_api.ts
index 93415b0d..66ee77c0 100644
--- a/front/src/iframe_api.ts
+++ b/front/src/iframe_api.ts
@@ -9,30 +9,34 @@ import {
 } from "./Api/Events/IframeEvent";
 import chat from "./Api/iframe/chat";
 import type { IframeCallback } from "./Api/iframe/IframeApiContribution";
-import nav from "./Api/iframe/nav";
+import nav, { CoWebsite } from "./Api/iframe/nav";
 import controls from "./Api/iframe/controls";
 import ui from "./Api/iframe/ui";
 import sound from "./Api/iframe/sound";
 import room, { setMapURL, setRoomId } from "./Api/iframe/room";
-import state, { initVariables } from "./Api/iframe/state";
+import { createState } from "./Api/iframe/state";
 import player, { setPlayerName, setTags, setUserRoomToken, setUuid } from "./Api/iframe/player";
 import type { ButtonDescriptor } from "./Api/iframe/Ui/ButtonDescriptor";
 import type { Popup } from "./Api/iframe/Ui/Popup";
 import type { Sound } from "./Api/iframe/Sound/Sound";
 import { answerPromises, queryWorkadventure } from "./Api/iframe/IframeApiContribution";
+import camera from "./Api/iframe/camera";
+
+const globalState = createState("global");
 
 // Notify WorkAdventure that we are ready to receive data
 const initPromise = queryWorkadventure({
     type: "getState",
     data: undefined,
-}).then((state) => {
-    setPlayerName(state.nickname);
-    setRoomId(state.roomId);
-    setMapURL(state.mapUrl);
-    setTags(state.tags);
-    setUuid(state.uuid);
-    initVariables(state.variables as Map<string, unknown>);
-    setUserRoomToken(state.userRoomToken);
+}).then((gameState) => {
+    setPlayerName(gameState.nickname);
+    setRoomId(gameState.roomId);
+    setMapURL(gameState.mapUrl);
+    setTags(gameState.tags);
+    setUuid(gameState.uuid);
+    globalState.initVariables(gameState.variables as Map<string, unknown>);
+    player.state.initVariables(gameState.playerVariables as Map<string, unknown>);
+    setUserRoomToken(gameState.userRoomToken);
 });
 
 const wa = {
@@ -43,7 +47,8 @@ const wa = {
     sound,
     room,
     player,
-    state,
+    camera,
+    state: globalState,
 
     onInit(): Promise<void> {
         return initPromise;
@@ -131,17 +136,17 @@ const wa = {
     /**
      * @deprecated Use WA.nav.openCoWebSite instead
      */
-    openCoWebSite(url: string, allowApi: boolean = false, allowPolicy: string = ""): void {
+    openCoWebSite(url: string, allowApi: boolean = false, allowPolicy: string = ""): Promise<CoWebsite> {
         console.warn("Method WA.openCoWebSite is deprecated. Please use WA.nav.openCoWebSite instead");
-        nav.openCoWebSite(url, allowApi, allowPolicy);
+        return nav.openCoWebSite(url, allowApi, allowPolicy);
     },
 
     /**
      * @deprecated Use WA.nav.closeCoWebSite instead
      */
-    closeCoWebSite(): void {
+    closeCoWebSite(): Promise<void> {
         console.warn("Method WA.closeCoWebSite is deprecated. Please use WA.nav.closeCoWebSite instead");
-        nav.closeCoWebSite();
+        return nav.closeCoWebSite();
     },
 
     /**
@@ -225,7 +230,5 @@ window.addEventListener(
                 callback?.callback(payloadData);
             }
         }
-
-        // ...
     }
 );
diff --git a/front/style/style.scss b/front/style/style.scss
index 89437a99..32d95f57 100644
--- a/front/style/style.scss
+++ b/front/style/style.scss
@@ -1066,6 +1066,7 @@ div.action.danger p.action-body{
     width: 100%;
     height: 100%;
     pointer-events: none;
+    user-select: none;
 
     & > div {
         position: relative;
diff --git a/front/tests/Phaser/Game/PlayerMovementTest.ts b/front/tests/Phaser/Game/PlayerMovementTest.ts
index bd5f40b4..95b84f0b 100644
--- a/front/tests/Phaser/Game/PlayerMovementTest.ts
+++ b/front/tests/Phaser/Game/PlayerMovementTest.ts
@@ -1,22 +1,24 @@
 import "jasmine";
-import {PlayerMovement} from "../../../src/Phaser/Game/PlayerMovement";
+import { PlayerMovement } from "../../../src/Phaser/Game/PlayerMovement";
 
 describe("Interpolation / Extrapolation", () => {
     it("should interpolate", () => {
-        const playerMovement = new PlayerMovement({
-            x: 100, y: 200
-        }, 42000,
+        const playerMovement = new PlayerMovement(
+            {
+                x: 100,
+                y: 200,
+            },
+            42000,
             {
                 x: 200,
                 y: 100,
                 oldX: 100,
                 oldY: 200,
                 moving: true,
-                direction: "up"
+                direction: "up",
             },
             42200
-            );
-
+        );
 
         expect(playerMovement.isOutdated(42100)).toBe(false);
         expect(playerMovement.isOutdated(43000)).toBe(true);
@@ -26,8 +28,8 @@ describe("Interpolation / Extrapolation", () => {
             y: 150,
             oldX: 100,
             oldY: 200,
-            direction: 'up',
-            moving: true
+            direction: "up",
+            moving: true,
         });
 
         expect(playerMovement.getPosition(42200)).toEqual({
@@ -35,8 +37,8 @@ describe("Interpolation / Extrapolation", () => {
             y: 100,
             oldX: 100,
             oldY: 200,
-            direction: 'up',
-            moving: true
+            direction: "up",
+            moving: true,
         });
 
         expect(playerMovement.getPosition(42300)).toEqual({
@@ -44,22 +46,25 @@ describe("Interpolation / Extrapolation", () => {
             y: 50,
             oldX: 100,
             oldY: 200,
-            direction: 'up',
-            moving: true
+            direction: "up",
+            moving: true,
         });
     });
 
     it("should not extrapolate if we stop", () => {
-        const playerMovement = new PlayerMovement({
-                x: 100, y: 200
-            }, 42000,
+        const playerMovement = new PlayerMovement(
+            {
+                x: 100,
+                y: 200,
+            },
+            42000,
             {
                 x: 200,
                 y: 100,
                 oldX: 100,
                 oldY: 200,
                 moving: false,
-                direction: "up"
+                direction: "up",
             },
             42200
         );
@@ -69,22 +74,25 @@ describe("Interpolation / Extrapolation", () => {
             y: 100,
             oldX: 100,
             oldY: 200,
-            direction: 'up',
-            moving: false
+            direction: "up",
+            moving: false,
         });
     });
 
     it("should keep moving until it stops", () => {
-        const playerMovement = new PlayerMovement({
-                x: 100, y: 200
-            }, 42000,
+        const playerMovement = new PlayerMovement(
+            {
+                x: 100,
+                y: 200,
+            },
+            42000,
             {
                 x: 200,
                 y: 100,
                 oldX: 100,
                 oldY: 200,
                 moving: false,
-                direction: "up"
+                direction: "up",
             },
             42200
         );
@@ -94,8 +102,8 @@ describe("Interpolation / Extrapolation", () => {
             y: 150,
             oldX: 100,
             oldY: 200,
-            direction: 'up',
-            moving: false
+            direction: "up",
+            moving: false,
         });
     });
-})
+});
diff --git a/front/yarn.lock b/front/yarn.lock
index 1557a08e..faf5e681 100644
--- a/front/yarn.lock
+++ b/front/yarn.lock
@@ -150,6 +150,59 @@
   resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.9.2.tgz#adea7b6953cbb34651766b0548468e743c6a2353"
   integrity sha512-VZMYa7+fXHdwIq1TDhSXoVmSPEGM/aa+6Aiq3nVVJ9bXr24zScr+NlKFKC3iPljA7ho/GAZr+d2jOf5GIRC30Q==
 
+"@protobufjs/aspromise@^1.1.1", "@protobufjs/aspromise@^1.1.2":
+  version "1.1.2"
+  resolved "https://registry.yarnpkg.com/@protobufjs/aspromise/-/aspromise-1.1.2.tgz#9b8b0cc663d669a7d8f6f5d0893a14d348f30fbf"
+  integrity sha1-m4sMxmPWaafY9vXQiToU00jzD78=
+
+"@protobufjs/base64@^1.1.2":
+  version "1.1.2"
+  resolved "https://registry.yarnpkg.com/@protobufjs/base64/-/base64-1.1.2.tgz#4c85730e59b9a1f1f349047dbf24296034bb2735"
+  integrity sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==
+
+"@protobufjs/codegen@^2.0.4":
+  version "2.0.4"
+  resolved "https://registry.yarnpkg.com/@protobufjs/codegen/-/codegen-2.0.4.tgz#7ef37f0d010fb028ad1ad59722e506d9262815cb"
+  integrity sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==
+
+"@protobufjs/eventemitter@^1.1.0":
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz#355cbc98bafad5978f9ed095f397621f1d066b70"
+  integrity sha1-NVy8mLr61ZePntCV85diHx0Ga3A=
+
+"@protobufjs/fetch@^1.1.0":
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/@protobufjs/fetch/-/fetch-1.1.0.tgz#ba99fb598614af65700c1619ff06d454b0d84c45"
+  integrity sha1-upn7WYYUr2VwDBYZ/wbUVLDYTEU=
+  dependencies:
+    "@protobufjs/aspromise" "^1.1.1"
+    "@protobufjs/inquire" "^1.1.0"
+
+"@protobufjs/float@^1.0.2":
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/@protobufjs/float/-/float-1.0.2.tgz#5e9e1abdcb73fc0a7cb8b291df78c8cbd97b87d1"
+  integrity sha1-Xp4avctz/Ap8uLKR33jIy9l7h9E=
+
+"@protobufjs/inquire@^1.1.0":
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/@protobufjs/inquire/-/inquire-1.1.0.tgz#ff200e3e7cf2429e2dcafc1140828e8cc638f089"
+  integrity sha1-/yAOPnzyQp4tyvwRQIKOjMY48Ik=
+
+"@protobufjs/path@^1.1.2":
+  version "1.1.2"
+  resolved "https://registry.yarnpkg.com/@protobufjs/path/-/path-1.1.2.tgz#6cc2b20c5c9ad6ad0dccfd21ca7673d8d7fbf68d"
+  integrity sha1-bMKyDFya1q0NzP0hynZz2Nf79o0=
+
+"@protobufjs/pool@^1.1.0":
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/@protobufjs/pool/-/pool-1.1.0.tgz#09fd15f2d6d3abfa9b65bc366506d6ad7846ff54"
+  integrity sha1-Cf0V8tbTq/qbZbw2ZQbWrXhG/1Q=
+
+"@protobufjs/utf8@^1.1.0":
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/@protobufjs/utf8/-/utf8-1.1.0.tgz#a777360b5b39a1a2e5106f8e858f2fd2d060c570"
+  integrity sha1-p3c2C1s5oaLlEG+OhY8v0tBgxXA=
+
 "@sentry/types@^6.11.0":
   version "6.12.0"
   resolved "https://registry.yarnpkg.com/@sentry/types/-/types-6.12.0.tgz#b7395688a79403c6df8d8bb8d81deb8222519853"
@@ -293,6 +346,11 @@
   resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee"
   integrity sha1-7ihweulOEdK4J7y+UnC86n8+ce4=
 
+"@types/long@^4.0.1":
+  version "4.0.1"
+  resolved "https://registry.yarnpkg.com/@types/long/-/long-4.0.1.tgz#459c65fa1867dafe6a8f322c4c51695663cc55e9"
+  integrity sha512-5tXH6Bx/kNGd3MgffdmP4dy2Z+G4eaXw0SE81Tq3BNadtnMR5/ySMzX4SLEzHJzSmPNn4HIdpQsBvXMUykr58w==
+
 "@types/mime@^1":
   version "1.3.2"
   resolved "https://registry.yarnpkg.com/@types/mime/-/mime-1.3.2.tgz#93e25bf9ee75fe0fd80b594bc4feb0e862111b5a"
@@ -317,11 +375,26 @@
   resolved "https://registry.yarnpkg.com/@types/node/-/node-15.3.0.tgz#d6fed7d6bc6854306da3dea1af9f874b00783e26"
   integrity sha512-8/bnjSZD86ZfpBsDlCIkNXIvm+h6wi9g7IqL+kmFkQ+Wvu3JrasgLElfiPgoo8V8vVfnEi0QVS12gbl94h9YsQ==
 
+"@types/node@>=13.7.0":
+  version "17.0.5"
+  resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.5.tgz#57ca67ec4e57ad9e4ef5a6bab48a15387a1c83e0"
+  integrity sha512-w3mrvNXLeDYV1GKTZorGJQivK6XLCoGwpnyJFbJVK/aTBQUxOCaa/GlFAAN3OTDFcb7h5tiFG+YXCO2By+riZw==
+
+"@types/object-hash@^1.3.0":
+  version "1.3.4"
+  resolved "https://registry.yarnpkg.com/@types/object-hash/-/object-hash-1.3.4.tgz#079ba142be65833293673254831b5e3e847fe58b"
+  integrity sha512-xFdpkAkikBgqBdG9vIlsqffDV8GpvnPEzs0IUtr1v3BEB97ijsFQ4RXVbUZwjFThhB4MDSTUfvmxUD5PGx0wXA==
+
 "@types/parse-json@^4.0.0":
   version "4.0.0"
   resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0"
   integrity sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==
 
+"@types/prettier@^1.19.0":
+  version "1.19.1"
+  resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-1.19.1.tgz#33509849f8e679e4add158959fdb086440e9553f"
+  integrity sha512-5qOlnZscTn4xxM5MeGXAMOsIOIKIbh9e85zJWfBRVPlRMEVawzoPhINYbRGkBZCI8LxvBe7tJCdWiarA99OZfQ==
+
 "@types/pug@^2.0.4":
   version "2.0.4"
   resolved "https://registry.yarnpkg.com/@types/pug/-/pug-2.0.4.tgz#8772fcd0418e3cd2cc171555d73007415051f4b2"
@@ -1656,6 +1729,11 @@ cssesc@^3.0.0:
   resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee"
   integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==
 
+dataloader@^1.4.0:
+  version "1.4.0"
+  resolved "https://registry.yarnpkg.com/dataloader/-/dataloader-1.4.0.tgz#bca11d867f5d3f1b9ed9f737bd15970c65dff5c8"
+  integrity sha512-68s5jYdlvasItOJnCuI2Q9s4q98g0pCyL3HrcKJu8KNugUl8ahgmZYg38ysLTgQjjXX3H8CJLkAvWrclWfcalw==
+
 debug@2.6.9, debug@^2.2.0, debug@^2.3.3:
   version "2.6.9"
   resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
@@ -3662,7 +3740,7 @@ lodash.merge@^4.6.2:
   resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a"
   integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==
 
-lodash@^4.17.11, lodash@^4.17.14, lodash@^4.17.20:
+lodash@^4.17.11, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.20:
   version "4.17.21"
   resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
   integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
@@ -3695,6 +3773,11 @@ lokijs@^1.5.12:
   resolved "https://registry.yarnpkg.com/lokijs/-/lokijs-1.5.12.tgz#cb55b37009bdf09ee7952a6adddd555b893653a0"
   integrity sha512-Q5ALD6JiS6xAUWCwX3taQmgwxyveCtIIuL08+ml0nHwT3k0S/GIFJN+Hd38b1qYIMaE5X++iqsqWVksz7SYW+Q==
 
+long@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/long/-/long-4.0.0.tgz#9a7b71cfb7d361a194ea555241c92f7468d5bf28"
+  integrity sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==
+
 lower-case@^2.0.2:
   version "2.0.2"
   resolved "https://registry.yarnpkg.com/lower-case/-/lower-case-2.0.2.tgz#6fa237c63dbdc4a82ca0fd882e4722dc5e634e28"
@@ -4098,6 +4181,11 @@ object-copy@^0.1.0:
     define-property "^0.2.5"
     kind-of "^3.0.3"
 
+object-hash@^1.3.1:
+  version "1.3.1"
+  resolved "https://registry.yarnpkg.com/object-hash/-/object-hash-1.3.1.tgz#fde452098a951cb145f039bb7d455449ddc126df"
+  integrity sha512-OSuu/pU4ENM9kmREg0BdNrUDIl1heYa4mBZacJc+vVWz4GtAwu7jO8s4AIt2aGRUTqxykpWzI3Oqnsm13tTMDA==
+
 object-inspect@^1.9.0:
   version "1.10.3"
   resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.10.3.tgz#c2aa7d2d09f50c99375704f7a0adf24c5782d369"
@@ -4590,6 +4678,11 @@ prettier-plugin-svelte@^2.5.0:
   resolved "https://registry.yarnpkg.com/prettier-plugin-svelte/-/prettier-plugin-svelte-2.5.0.tgz#7922534729f7febe59b4c56c3f5360539f0d8ab1"
   integrity sha512-+iHY2uGChOngrgKielJUnqo74gIL/EO5oeWm8MftFWjEi213lq9QYTOwm1pv4lI1nA61tdgf80CF2i5zMcu1kw==
 
+prettier@^2.0.2:
+  version "2.5.1"
+  resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.5.1.tgz#fff75fa9d519c54cf0fce328c1017d94546bc56a"
+  integrity sha512-vBZcPRUR5MZJwoyi3ZoyQlc1rXeEck8KgeC9AwwOn+exuxLxq5toTRDTSaVrXHxelDMHy9zlicw8u66yxoSUFg==
+
 prettier@^2.3.1:
   version "2.3.1"
   resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.3.1.tgz#76903c3f8c4449bc9ac597acefa24dc5ad4cbea6"
@@ -4618,6 +4711,25 @@ progress@^2.0.0:
   resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8"
   integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==
 
+protobufjs@^6.8.8:
+  version "6.11.2"
+  resolved "https://registry.yarnpkg.com/protobufjs/-/protobufjs-6.11.2.tgz#de39fabd4ed32beaa08e9bb1e30d08544c1edf8b"
+  integrity sha512-4BQJoPooKJl2G9j3XftkIXjoC9C0Av2NOrWmbLWT1vH32GcSUHjM0Arra6UfTsVyfMAuFzaLucXn1sadxJydAw==
+  dependencies:
+    "@protobufjs/aspromise" "^1.1.2"
+    "@protobufjs/base64" "^1.1.2"
+    "@protobufjs/codegen" "^2.0.4"
+    "@protobufjs/eventemitter" "^1.1.0"
+    "@protobufjs/fetch" "^1.1.0"
+    "@protobufjs/float" "^1.0.2"
+    "@protobufjs/inquire" "^1.1.0"
+    "@protobufjs/path" "^1.1.2"
+    "@protobufjs/pool" "^1.1.0"
+    "@protobufjs/utf8" "^1.1.0"
+    "@types/long" "^4.0.1"
+    "@types/node" ">=13.7.0"
+    long "^4.0.0"
+
 proxy-addr@~2.0.5:
   version "2.0.6"
   resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.6.tgz#fdc2336505447d3f2f2c638ed272caf614bbb2bf"
@@ -5819,6 +5931,35 @@ ts-node@^10.4.0:
     make-error "^1.1.1"
     yn "3.1.1"
 
+ts-poet@^4.5.0:
+  version "4.6.1"
+  resolved "https://registry.yarnpkg.com/ts-poet/-/ts-poet-4.6.1.tgz#015dc823d726655af9f095c900f84ed7c60e2dd3"
+  integrity sha512-DXJ+mBJIDp+jiaUgB4N5I/sczHHDU2FWacdbDNVAVS4Mh4hb7ckpvUWVW7m7/nAOcjR0r4Wt+7AoO7FeJKExfA==
+  dependencies:
+    "@types/prettier" "^1.19.0"
+    lodash "^4.17.15"
+    prettier "^2.0.2"
+
+ts-proto-descriptors@^1.2.1:
+  version "1.3.1"
+  resolved "https://registry.yarnpkg.com/ts-proto-descriptors/-/ts-proto-descriptors-1.3.1.tgz#760ebaaa19475b03662f7b358ffea45b9c5348f5"
+  integrity sha512-Cybb3fqceMwA6JzHdC32dIo8eVGVmXrM6TWhdk1XQVVHT/6OQqk0ioyX1dIdu3rCIBhRmWUhUE4HsyK+olmgMw==
+  dependencies:
+    long "^4.0.0"
+    protobufjs "^6.8.8"
+
+ts-proto@^1.96.0:
+  version "1.96.0"
+  resolved "https://registry.yarnpkg.com/ts-proto/-/ts-proto-1.96.0.tgz#63768d7da533b337aee84db065dd66773bd4cac9"
+  integrity sha512-fKwaGzi8EOCU9xwmcXK917jj1WhFdLbFkPRawQ+5CAZM9eSXr/mpkz/yEctXCiuei364z6jAB2Odb64KCDFTPQ==
+  dependencies:
+    "@types/object-hash" "^1.3.0"
+    dataloader "^1.4.0"
+    object-hash "^1.3.1"
+    protobufjs "^6.8.8"
+    ts-poet "^4.5.0"
+    ts-proto-descriptors "^1.2.1"
+
 tsconfig-paths@^3.9.0:
   version "3.9.0"
   resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.9.0.tgz#098547a6c4448807e8fcb8eae081064ee9a3c90b"
diff --git a/maps/tests/EmbeddedWebsite/website_in_map_script.php b/maps/tests/EmbeddedWebsite/website_in_map_script.php
index c822b8ca..78eced1a 100644
--- a/maps/tests/EmbeddedWebsite/website_in_map_script.php
+++ b/maps/tests/EmbeddedWebsite/website_in_map_script.php
@@ -15,6 +15,8 @@
                 const heightField = document.getElementById('height');
                 const urlField = document.getElementById('url');
                 const visibleField = document.getElementById('visible');
+                const originField = document.getElementById('origin');
+                const scaleField = document.getElementById('scale');
 
                 createButton.addEventListener('click', () => {
                     console.log('CREATING NEW EMBEDDED IFRAME');
@@ -28,6 +30,8 @@
                             height: parseInt(heightField.value),
                         },
                         visible: !!visibleField.value,
+                        origin: originField.value,
+                        scale: parseFloat(scaleField.value),
                     });
                 });
 
@@ -61,6 +65,16 @@
                     const website = await WA.room.website.get('test');
                     website.visible = this.checked;
                 });
+
+                originField.addEventListener('change', async function() {
+                    const website = await WA.room.website.get('test');
+                    website.origin = this.value;
+                });
+
+                scaleField.addEventListener('change', async function() {
+                    const website = await WA.room.website.get('test');
+                    website.scale = parseFloat(this.value);
+                });
             });
         })
     </script>
@@ -72,6 +86,8 @@ width: <input type="text" id="width" value="600" /><br/>
 height: <input type="text" id="height" value="400" /><br/>
 URL: <input type="text" id="url" value="https://mensuel.framapad.org/p/rt6c904745-9oxm?lang=en" /><br/>
 Visible: <input type="checkbox" id="visible" value=1 /><br/>
+Origin: <input type="text" id="origin" value="map" /><br/>
+Scale: <input type="text" id="scale" value=1 /><br/>
 
 <button id="createEmbeddedWebsite">Create embedded website</button>
 
diff --git a/messages/JsonMessages/MapDetailsData.ts b/messages/JsonMessages/MapDetailsData.ts
index 794c9bac..39866eee 100644
--- a/messages/JsonMessages/MapDetailsData.ts
+++ b/messages/JsonMessages/MapDetailsData.ts
@@ -20,6 +20,10 @@ export const isMapDetailsData = new tg.IsInterface()
     })
     .withOptionalProperties({
         iframeAuthentication: tg.isNullable(tg.isString),
+        // The date (in ISO 8601 format) at which the room will expire
+        expireOn: tg.isString,
+        // Whether the "report" feature is enabled or not on this room
+        canReport: tg.isBoolean,
     })
     .get();
 
diff --git a/messages/package.json b/messages/package.json
index 6095a626..e6f04ba5 100644
--- a/messages/package.json
+++ b/messages/package.json
@@ -4,13 +4,14 @@
   "description": "",
   "scripts": {
     "proto": "grpc_tools_node_protoc --plugin=protoc-gen-ts=./node_modules/.bin/protoc-gen-ts --grpc_out=generated --js_out=\"import_style=commonjs,binary:generated\" --ts_out=generated -I ./protos protos/*.proto",
+    "ts-proto": "grpc_tools_node_protoc --plugin=./node_modules/.bin/protoc-gen-ts_proto --ts_proto_out=ts-proto-generated --ts_proto_opt=oneof=unions --ts_proto_opt=esModuleInterop=true protos/*.proto",
     "copy-to-back": "rm -rf ../back/src/Messages/generated && cp -rf generated/ ../back/src/Messages/generated",
-    "copy-to-front": "rm -rf ../front/src/Messages/generated && cp -rf generated/ ../front/src/Messages/generated",
+    "copy-to-front-ts-proto": "sed 's/import { Observable } from \"rxjs\";/import type { Observable } from \"rxjs\";/g' ts-proto-generated/protos/messages.ts > ../front/src/Messages/ts-proto-generated/messages.ts",
     "copy-to-pusher": "rm -rf ../pusher/src/Messages/generated && cp -rf generated/ ../pusher/src/Messages/generated",
     "json-copy-to-pusher": "rm -rf ../pusher/src/Messages/JsonMessages/* && cp -rf JsonMessages/* ../pusher/src/Messages/JsonMessages/",
     "json-copy-to-front": "rm -rf ../front/src/Messages/JsonMessages/* && cp -rf JsonMessages/* ../front/src/Messages/JsonMessages/",
     "precommit": "lint-staged",
-    "proto-all": "yarn run proto && yarn run copy-to-back && yarn run copy-to-front && yarn run copy-to-pusher && yarn run json-copy-to-pusher && yarn run json-copy-to-front",
+    "proto-all": "yarn run proto && yarn run ts-proto && yarn run copy-to-back && yarn run copy-to-front-ts-proto && yarn run copy-to-pusher && yarn run json-copy-to-pusher && yarn run json-copy-to-front",
     "proto:watch": "yarn run proto-all; inotifywait -q -m -e close_write protos/messages.proto JsonMessages/ | while read -r filename event; do yarn run proto-all; done",
     "pretty": "yarn prettier --write 'JsonMessages/**/*.ts'",
     "pretty-check": "yarn prettier --check 'JsonMessages/**/*.ts'"
@@ -18,7 +19,8 @@
   "dependencies": {
     "generic-type-guard": "^3.5.0",
     "google-protobuf": "^3.13.0",
-    "grpc": "^1.24.4"
+    "grpc": "^1.24.4",
+    "ts-proto": "^1.96.0"
   },
   "devDependencies": {
     "@types/google-protobuf": "^3.7.4",
diff --git a/messages/protos/messages.proto b/messages/protos/messages.proto
index 3c05037a..8ac7bbf0 100644
--- a/messages/protos/messages.proto
+++ b/messages/protos/messages.proto
@@ -296,8 +296,6 @@ message ServerToClientMessage {
     WebRtcSignalToClientMessage webRtcSignalToClientMessage = 5;
     WebRtcSignalToClientMessage webRtcScreenSharingSignalToClientMessage = 6;
     WebRtcDisconnectMessage webRtcDisconnectMessage = 7;
-    PlayGlobalMessage playGlobalMessage = 8;
-    StopGlobalMessage stopGlobalMessage = 9;
     TeleportMessageMessage teleportMessageMessage = 10;
     SendJitsiJwtMessage sendJitsiJwtMessage = 11;
     SendUserMessage sendUserMessage = 12;
@@ -390,8 +388,6 @@ message PusherToBackMessage {
     SetPlayerDetailsMessage setPlayerDetailsMessage = 5;
     WebRtcSignalToServerMessage webRtcSignalToServerMessage = 6;
     WebRtcSignalToServerMessage webRtcScreenSharingSignalToServerMessage = 7;
-    PlayGlobalMessage playGlobalMessage = 8;
-    StopGlobalMessage stopGlobalMessage = 9;
     ReportPlayerMessage reportPlayerMessage = 10;
     QueryJitsiJwtMessage queryJitsiJwtMessage = 11;
     SendUserMessage sendUserMessage = 12;
diff --git a/messages/ts-proto-generated/.gitignore b/messages/ts-proto-generated/.gitignore
new file mode 100644
index 00000000..d6b7ef32
--- /dev/null
+++ b/messages/ts-proto-generated/.gitignore
@@ -0,0 +1,2 @@
+*
+!.gitignore
diff --git a/messages/yarn.lock b/messages/yarn.lock
index 5d040c6f..8ba2fe90 100644
--- a/messages/yarn.lock
+++ b/messages/yarn.lock
@@ -174,6 +174,11 @@
   resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.7.tgz#8ea1e8f8eae2430cf440564b98c6dfce1ec5945d"
   integrity sha512-Zw1vhUSQZYw+7u5dAwNbIA9TuTotpzY/OF7sJM9FqPOF3SPjKnxrjoTktXDZgUjybf4cWVBP7O8wvKdSaGHweg==
 
+"@types/node@>=13.7.0":
+  version "17.0.5"
+  resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.5.tgz#57ca67ec4e57ad9e4ef5a6bab48a15387a1c83e0"
+  integrity sha512-w3mrvNXLeDYV1GKTZorGJQivK6XLCoGwpnyJFbJVK/aTBQUxOCaa/GlFAAN3OTDFcb7h5tiFG+YXCO2By+riZw==
+
 "@types/node@^12.12.29":
   version "12.19.4"
   resolved "https://registry.yarnpkg.com/@types/node/-/node-12.19.4.tgz#cdfbb62e26c7435ed9aab9c941393cc3598e9b46"
@@ -184,6 +189,11 @@
   resolved "https://registry.yarnpkg.com/@types/node/-/node-13.13.30.tgz#1ed6e01e4ca576d5aec9cc802cc3bcf94c274192"
   integrity sha512-HmqFpNzp3TSELxU/bUuRK+xzarVOAsR00hzcvM0TXrMlt/+wcSLa5q6YhTb6/cA6wqDCZLDcfd8fSL95x5h7AA==
 
+"@types/object-hash@^1.3.0":
+  version "1.3.4"
+  resolved "https://registry.yarnpkg.com/@types/object-hash/-/object-hash-1.3.4.tgz#079ba142be65833293673254831b5e3e847fe58b"
+  integrity sha512-xFdpkAkikBgqBdG9vIlsqffDV8GpvnPEzs0IUtr1v3BEB97ijsFQ4RXVbUZwjFThhB4MDSTUfvmxUD5PGx0wXA==
+
 "@types/parse-json@^4.0.0":
   version "4.0.0"
   resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0"
@@ -194,6 +204,11 @@
   resolved "https://registry.yarnpkg.com/@types/parsimmon/-/parsimmon-1.10.4.tgz#7639e16015440d9baf622f83c12dae47787226b7"
   integrity sha512-M56NfQHfaWuaj6daSgCVs7jh8fXLI3LmxjRoQxmOvYesgIkI+9HPsDLO0vd7wX7cwA0D0ZWFEJdp0VPwLdS+bQ==
 
+"@types/prettier@^1.19.0":
+  version "1.19.1"
+  resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-1.19.1.tgz#33509849f8e679e4add158959fdb086440e9553f"
+  integrity sha512-5qOlnZscTn4xxM5MeGXAMOsIOIKIbh9e85zJWfBRVPlRMEVawzoPhINYbRGkBZCI8LxvBe7tJCdWiarA99OZfQ==
+
 "@typescript-eslint/eslint-plugin@^4.7.0":
   version "4.7.0"
   resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.7.0.tgz#85c9bbda00c0cb604d3c241f7bc7fb171a2d3479"
@@ -1156,6 +1171,11 @@ dashdash@^1.12.0:
   dependencies:
     assert-plus "^1.0.0"
 
+dataloader@^1.4.0:
+  version "1.4.0"
+  resolved "https://registry.yarnpkg.com/dataloader/-/dataloader-1.4.0.tgz#bca11d867f5d3f1b9ed9f737bd15970c65dff5c8"
+  integrity sha512-68s5jYdlvasItOJnCuI2Q9s4q98g0pCyL3HrcKJu8KNugUl8ahgmZYg38ysLTgQjjXX3H8CJLkAvWrclWfcalw==
+
 date.js@^0.3.1:
   version "0.3.3"
   resolved "https://registry.yarnpkg.com/date.js/-/date.js-0.3.3.tgz#ef1e92332f507a638795dbb985e951882e50bbda"
@@ -3154,6 +3174,11 @@ object-copy@^0.1.0:
     define-property "^0.2.5"
     kind-of "^3.0.3"
 
+object-hash@^1.3.1:
+  version "1.3.1"
+  resolved "https://registry.yarnpkg.com/object-hash/-/object-hash-1.3.1.tgz#fde452098a951cb145f039bb7d455449ddc126df"
+  integrity sha512-OSuu/pU4ENM9kmREg0BdNrUDIl1heYa4mBZacJc+vVWz4GtAwu7jO8s4AIt2aGRUTqxykpWzI3Oqnsm13tTMDA==
+
 object-inspect@^1.8.0:
   version "1.8.0"
   resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.8.0.tgz#df807e5ecf53a609cc6bfe93eac3cc7be5b3a9d0"
@@ -3423,7 +3448,7 @@ prelude-ls@^1.2.1:
   resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396"
   integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==
 
-prettier@^2.3.1:
+prettier@^2.0.2, prettier@^2.3.1:
   version "2.5.1"
   resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.5.1.tgz#fff75fa9d519c54cf0fce328c1017d94546bc56a"
   integrity sha512-vBZcPRUR5MZJwoyi3ZoyQlc1rXeEck8KgeC9AwwOn+exuxLxq5toTRDTSaVrXHxelDMHy9zlicw8u66yxoSUFg==
@@ -3467,6 +3492,25 @@ protobufjs@^6.10.1:
     "@types/node" "^13.7.0"
     long "^4.0.0"
 
+protobufjs@^6.8.8:
+  version "6.11.2"
+  resolved "https://registry.yarnpkg.com/protobufjs/-/protobufjs-6.11.2.tgz#de39fabd4ed32beaa08e9bb1e30d08544c1edf8b"
+  integrity sha512-4BQJoPooKJl2G9j3XftkIXjoC9C0Av2NOrWmbLWT1vH32GcSUHjM0Arra6UfTsVyfMAuFzaLucXn1sadxJydAw==
+  dependencies:
+    "@protobufjs/aspromise" "^1.1.2"
+    "@protobufjs/base64" "^1.1.2"
+    "@protobufjs/codegen" "^2.0.4"
+    "@protobufjs/eventemitter" "^1.1.0"
+    "@protobufjs/fetch" "^1.1.0"
+    "@protobufjs/float" "^1.0.2"
+    "@protobufjs/inquire" "^1.1.0"
+    "@protobufjs/path" "^1.1.2"
+    "@protobufjs/pool" "^1.1.0"
+    "@protobufjs/utf8" "^1.1.0"
+    "@types/long" "^4.0.1"
+    "@types/node" ">=13.7.0"
+    long "^4.0.0"
+
 psl@^1.1.28:
   version "1.8.0"
   resolved "https://registry.yarnpkg.com/psl/-/psl-1.8.0.tgz#9326f8bcfb013adcc005fdff056acce020e51c24"
@@ -4245,6 +4289,35 @@ tough-cookie@~2.5.0:
     psl "^1.1.28"
     punycode "^2.1.1"
 
+ts-poet@^4.5.0:
+  version "4.6.1"
+  resolved "https://registry.yarnpkg.com/ts-poet/-/ts-poet-4.6.1.tgz#015dc823d726655af9f095c900f84ed7c60e2dd3"
+  integrity sha512-DXJ+mBJIDp+jiaUgB4N5I/sczHHDU2FWacdbDNVAVS4Mh4hb7ckpvUWVW7m7/nAOcjR0r4Wt+7AoO7FeJKExfA==
+  dependencies:
+    "@types/prettier" "^1.19.0"
+    lodash "^4.17.15"
+    prettier "^2.0.2"
+
+ts-proto-descriptors@^1.2.1:
+  version "1.3.1"
+  resolved "https://registry.yarnpkg.com/ts-proto-descriptors/-/ts-proto-descriptors-1.3.1.tgz#760ebaaa19475b03662f7b358ffea45b9c5348f5"
+  integrity sha512-Cybb3fqceMwA6JzHdC32dIo8eVGVmXrM6TWhdk1XQVVHT/6OQqk0ioyX1dIdu3rCIBhRmWUhUE4HsyK+olmgMw==
+  dependencies:
+    long "^4.0.0"
+    protobufjs "^6.8.8"
+
+ts-proto@^1.96.0:
+  version "1.96.0"
+  resolved "https://registry.yarnpkg.com/ts-proto/-/ts-proto-1.96.0.tgz#63768d7da533b337aee84db065dd66773bd4cac9"
+  integrity sha512-fKwaGzi8EOCU9xwmcXK917jj1WhFdLbFkPRawQ+5CAZM9eSXr/mpkz/yEctXCiuei364z6jAB2Odb64KCDFTPQ==
+  dependencies:
+    "@types/object-hash" "^1.3.0"
+    dataloader "^1.4.0"
+    object-hash "^1.3.1"
+    protobufjs "^6.8.8"
+    ts-poet "^4.5.0"
+    ts-proto-descriptors "^1.2.1"
+
 tsconfig-paths@^3.9.0:
   version "3.9.0"
   resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.9.0.tgz#098547a6c4448807e8fcb8eae081064ee9a3c90b"