Add `common` subproject containing shared files

Move classes in `model` to their own `common` package.
Now, the client and server can import `common` as a library.
This is the first step to eventually migrate all the source code at the root of the
project to an `old-server` package.

GitLab: #55
Change-Id: I4b7a52e80171d9c3399416ab524bcdd6915ac540
diff --git a/common/package-lock.json b/common/package-lock.json
new file mode 100644
index 0000000..e6d0fce
--- /dev/null
+++ b/common/package-lock.json
@@ -0,0 +1,607 @@
+{
+  "name": "jami-web-common",
+  "version": "1.0.0",
+  "lockfileVersion": 2,
+  "requires": true,
+  "packages": {
+    "": {
+      "name": "jami-web-common",
+      "version": "1.0.0",
+      "license": "ISC",
+      "dependencies": {
+        "typescript": "^4.8.4"
+      },
+      "devDependencies": {
+        "@types/express-session": "^1.17.5",
+        "@types/socket.io": "^3.0.2"
+      }
+    },
+    "node_modules/@socket.io/component-emitter": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz",
+      "integrity": "sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg==",
+      "dev": true
+    },
+    "node_modules/@types/body-parser": {
+      "version": "1.19.2",
+      "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.2.tgz",
+      "integrity": "sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g==",
+      "dev": true,
+      "dependencies": {
+        "@types/connect": "*",
+        "@types/node": "*"
+      }
+    },
+    "node_modules/@types/connect": {
+      "version": "3.4.35",
+      "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz",
+      "integrity": "sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==",
+      "dev": true,
+      "dependencies": {
+        "@types/node": "*"
+      }
+    },
+    "node_modules/@types/cookie": {
+      "version": "0.4.1",
+      "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.1.tgz",
+      "integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==",
+      "dev": true
+    },
+    "node_modules/@types/cors": {
+      "version": "2.8.12",
+      "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.12.tgz",
+      "integrity": "sha512-vt+kDhq/M2ayberEtJcIN/hxXy1Pk+59g2FV/ZQceeaTyCtCucjL2Q7FXlFjtWn4n15KCr1NE2lNNFhp0lEThw==",
+      "dev": true
+    },
+    "node_modules/@types/express": {
+      "version": "4.17.14",
+      "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.14.tgz",
+      "integrity": "sha512-TEbt+vaPFQ+xpxFLFssxUDXj5cWCxZJjIcB7Yg0k0GMHGtgtQgpvx/MUQUeAkNbA9AAGrwkAsoeItdTgS7FMyg==",
+      "dev": true,
+      "dependencies": {
+        "@types/body-parser": "*",
+        "@types/express-serve-static-core": "^4.17.18",
+        "@types/qs": "*",
+        "@types/serve-static": "*"
+      }
+    },
+    "node_modules/@types/express-serve-static-core": {
+      "version": "4.17.31",
+      "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.31.tgz",
+      "integrity": "sha512-DxMhY+NAsTwMMFHBTtJFNp5qiHKJ7TeqOo23zVEM9alT1Ml27Q3xcTH0xwxn7Q0BbMcVEJOs/7aQtUWupUQN3Q==",
+      "dev": true,
+      "dependencies": {
+        "@types/node": "*",
+        "@types/qs": "*",
+        "@types/range-parser": "*"
+      }
+    },
+    "node_modules/@types/express-session": {
+      "version": "1.17.5",
+      "resolved": "https://registry.npmjs.org/@types/express-session/-/express-session-1.17.5.tgz",
+      "integrity": "sha512-l0DhkvNVfyUPEEis8fcwbd46VptfA/jmMwHfob2TfDMf3HyPLiB9mKD71LXhz5TMUobODXPD27zXSwtFQLHm+w==",
+      "dev": true,
+      "dependencies": {
+        "@types/express": "*"
+      }
+    },
+    "node_modules/@types/mime": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/@types/mime/-/mime-3.0.1.tgz",
+      "integrity": "sha512-Y4XFY5VJAuw0FgAqPNd6NNoV44jbq9Bz2L7Rh/J6jLTiHBSBJa9fxqQIvkIld4GsoDOcCbvzOUAbLPsSKKg+uA==",
+      "dev": true
+    },
+    "node_modules/@types/node": {
+      "version": "18.8.3",
+      "resolved": "https://registry.npmjs.org/@types/node/-/node-18.8.3.tgz",
+      "integrity": "sha512-0os9vz6BpGwxGe9LOhgP/ncvYN5Tx1fNcd2TM3rD/aCGBkysb+ZWpXEocG24h6ZzOi13+VB8HndAQFezsSOw1w==",
+      "dev": true
+    },
+    "node_modules/@types/qs": {
+      "version": "6.9.7",
+      "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz",
+      "integrity": "sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==",
+      "dev": true
+    },
+    "node_modules/@types/range-parser": {
+      "version": "1.2.4",
+      "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.4.tgz",
+      "integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==",
+      "dev": true
+    },
+    "node_modules/@types/serve-static": {
+      "version": "1.15.0",
+      "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.0.tgz",
+      "integrity": "sha512-z5xyF6uh8CbjAu9760KDKsH2FcDxZ2tFCsA4HIMWE6IkiYMXfVoa+4f9KX+FN0ZLsaMw1WNG2ETLA6N+/YA+cg==",
+      "dev": true,
+      "dependencies": {
+        "@types/mime": "*",
+        "@types/node": "*"
+      }
+    },
+    "node_modules/@types/socket.io": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmjs.org/@types/socket.io/-/socket.io-3.0.2.tgz",
+      "integrity": "sha512-pu0sN9m5VjCxBZVK8hW37ZcMe8rjn4HHggBN5CbaRTvFwv5jOmuIRZEuddsBPa9Th0ts0SIo3Niukq+95cMBbQ==",
+      "deprecated": "This is a stub types definition. socket.io provides its own type definitions, so you do not need this installed.",
+      "dev": true,
+      "dependencies": {
+        "socket.io": "*"
+      }
+    },
+    "node_modules/accepts": {
+      "version": "1.3.8",
+      "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
+      "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
+      "dev": true,
+      "dependencies": {
+        "mime-types": "~2.1.34",
+        "negotiator": "0.6.3"
+      },
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/base64id": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz",
+      "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==",
+      "dev": true,
+      "engines": {
+        "node": "^4.5.0 || >= 5.9"
+      }
+    },
+    "node_modules/cookie": {
+      "version": "0.4.2",
+      "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz",
+      "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==",
+      "dev": true,
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/cors": {
+      "version": "2.8.5",
+      "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz",
+      "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==",
+      "dev": true,
+      "dependencies": {
+        "object-assign": "^4",
+        "vary": "^1"
+      },
+      "engines": {
+        "node": ">= 0.10"
+      }
+    },
+    "node_modules/debug": {
+      "version": "4.3.4",
+      "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
+      "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
+      "dev": true,
+      "dependencies": {
+        "ms": "2.1.2"
+      },
+      "engines": {
+        "node": ">=6.0"
+      },
+      "peerDependenciesMeta": {
+        "supports-color": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/engine.io": {
+      "version": "6.2.0",
+      "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.2.0.tgz",
+      "integrity": "sha512-4KzwW3F3bk+KlzSOY57fj/Jx6LyRQ1nbcyIadehl+AnXjKT7gDO0ORdRi/84ixvMKTym6ZKuxvbzN62HDDU1Lg==",
+      "dev": true,
+      "dependencies": {
+        "@types/cookie": "^0.4.1",
+        "@types/cors": "^2.8.12",
+        "@types/node": ">=10.0.0",
+        "accepts": "~1.3.4",
+        "base64id": "2.0.0",
+        "cookie": "~0.4.1",
+        "cors": "~2.8.5",
+        "debug": "~4.3.1",
+        "engine.io-parser": "~5.0.3",
+        "ws": "~8.2.3"
+      },
+      "engines": {
+        "node": ">=10.0.0"
+      }
+    },
+    "node_modules/engine.io-parser": {
+      "version": "5.0.4",
+      "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.0.4.tgz",
+      "integrity": "sha512-+nVFp+5z1E3HcToEnO7ZIj3g+3k9389DvWtvJZz0T6/eOCPIyyxehFcedoYrZQrp0LgQbD9pPXhpMBKMd5QURg==",
+      "dev": true,
+      "engines": {
+        "node": ">=10.0.0"
+      }
+    },
+    "node_modules/mime-db": {
+      "version": "1.52.0",
+      "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+      "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+      "dev": true,
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/mime-types": {
+      "version": "2.1.35",
+      "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+      "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+      "dev": true,
+      "dependencies": {
+        "mime-db": "1.52.0"
+      },
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/ms": {
+      "version": "2.1.2",
+      "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
+      "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
+      "dev": true
+    },
+    "node_modules/negotiator": {
+      "version": "0.6.3",
+      "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
+      "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
+      "dev": true,
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/object-assign": {
+      "version": "4.1.1",
+      "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
+      "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
+      "dev": true,
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/socket.io": {
+      "version": "4.5.2",
+      "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.5.2.tgz",
+      "integrity": "sha512-6fCnk4ARMPZN448+SQcnn1u8OHUC72puJcNtSgg2xS34Cu7br1gQ09YKkO1PFfDn/wyUE9ZgMAwosJed003+NQ==",
+      "dev": true,
+      "dependencies": {
+        "accepts": "~1.3.4",
+        "base64id": "~2.0.0",
+        "debug": "~4.3.2",
+        "engine.io": "~6.2.0",
+        "socket.io-adapter": "~2.4.0",
+        "socket.io-parser": "~4.2.0"
+      },
+      "engines": {
+        "node": ">=10.0.0"
+      }
+    },
+    "node_modules/socket.io-adapter": {
+      "version": "2.4.0",
+      "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.4.0.tgz",
+      "integrity": "sha512-W4N+o69rkMEGVuk2D/cvca3uYsvGlMwsySWV447y99gUPghxq42BxqLNMndb+a1mm/5/7NeXVQS7RLa2XyXvYg==",
+      "dev": true
+    },
+    "node_modules/socket.io-parser": {
+      "version": "4.2.1",
+      "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.1.tgz",
+      "integrity": "sha512-V4GrkLy+HeF1F/en3SpUaM+7XxYXpuMUWLGde1kSSh5nQMN4hLrbPIkD+otwh6q9R6NOQBN4AMaOZ2zVjui82g==",
+      "dev": true,
+      "dependencies": {
+        "@socket.io/component-emitter": "~3.1.0",
+        "debug": "~4.3.1"
+      },
+      "engines": {
+        "node": ">=10.0.0"
+      }
+    },
+    "node_modules/typescript": {
+      "version": "4.8.4",
+      "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.8.4.tgz",
+      "integrity": "sha512-QCh+85mCy+h0IGff8r5XWzOVSbBO+KfeYrMQh7NJ58QujwcE22u+NUSmUxqF+un70P9GXKxa2HCNiTTMJknyjQ==",
+      "bin": {
+        "tsc": "bin/tsc",
+        "tsserver": "bin/tsserver"
+      },
+      "engines": {
+        "node": ">=4.2.0"
+      }
+    },
+    "node_modules/vary": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
+      "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
+      "dev": true,
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/ws": {
+      "version": "8.2.3",
+      "resolved": "https://registry.npmjs.org/ws/-/ws-8.2.3.tgz",
+      "integrity": "sha512-wBuoj1BDpC6ZQ1B7DWQBYVLphPWkm8i9Y0/3YdHjHKHiohOJ1ws+3OccDWtH+PoC9DZD5WOTrJvNbWvjS6JWaA==",
+      "dev": true,
+      "engines": {
+        "node": ">=10.0.0"
+      },
+      "peerDependencies": {
+        "bufferutil": "^4.0.1",
+        "utf-8-validate": "^5.0.2"
+      },
+      "peerDependenciesMeta": {
+        "bufferutil": {
+          "optional": true
+        },
+        "utf-8-validate": {
+          "optional": true
+        }
+      }
+    }
+  },
+  "dependencies": {
+    "@socket.io/component-emitter": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz",
+      "integrity": "sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg==",
+      "dev": true
+    },
+    "@types/body-parser": {
+      "version": "1.19.2",
+      "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.2.tgz",
+      "integrity": "sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g==",
+      "dev": true,
+      "requires": {
+        "@types/connect": "*",
+        "@types/node": "*"
+      }
+    },
+    "@types/connect": {
+      "version": "3.4.35",
+      "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz",
+      "integrity": "sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==",
+      "dev": true,
+      "requires": {
+        "@types/node": "*"
+      }
+    },
+    "@types/cookie": {
+      "version": "0.4.1",
+      "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.1.tgz",
+      "integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==",
+      "dev": true
+    },
+    "@types/cors": {
+      "version": "2.8.12",
+      "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.12.tgz",
+      "integrity": "sha512-vt+kDhq/M2ayberEtJcIN/hxXy1Pk+59g2FV/ZQceeaTyCtCucjL2Q7FXlFjtWn4n15KCr1NE2lNNFhp0lEThw==",
+      "dev": true
+    },
+    "@types/express": {
+      "version": "4.17.14",
+      "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.14.tgz",
+      "integrity": "sha512-TEbt+vaPFQ+xpxFLFssxUDXj5cWCxZJjIcB7Yg0k0GMHGtgtQgpvx/MUQUeAkNbA9AAGrwkAsoeItdTgS7FMyg==",
+      "dev": true,
+      "requires": {
+        "@types/body-parser": "*",
+        "@types/express-serve-static-core": "^4.17.18",
+        "@types/qs": "*",
+        "@types/serve-static": "*"
+      }
+    },
+    "@types/express-serve-static-core": {
+      "version": "4.17.31",
+      "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.31.tgz",
+      "integrity": "sha512-DxMhY+NAsTwMMFHBTtJFNp5qiHKJ7TeqOo23zVEM9alT1Ml27Q3xcTH0xwxn7Q0BbMcVEJOs/7aQtUWupUQN3Q==",
+      "dev": true,
+      "requires": {
+        "@types/node": "*",
+        "@types/qs": "*",
+        "@types/range-parser": "*"
+      }
+    },
+    "@types/express-session": {
+      "version": "1.17.5",
+      "resolved": "https://registry.npmjs.org/@types/express-session/-/express-session-1.17.5.tgz",
+      "integrity": "sha512-l0DhkvNVfyUPEEis8fcwbd46VptfA/jmMwHfob2TfDMf3HyPLiB9mKD71LXhz5TMUobODXPD27zXSwtFQLHm+w==",
+      "dev": true,
+      "requires": {
+        "@types/express": "*"
+      }
+    },
+    "@types/mime": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/@types/mime/-/mime-3.0.1.tgz",
+      "integrity": "sha512-Y4XFY5VJAuw0FgAqPNd6NNoV44jbq9Bz2L7Rh/J6jLTiHBSBJa9fxqQIvkIld4GsoDOcCbvzOUAbLPsSKKg+uA==",
+      "dev": true
+    },
+    "@types/node": {
+      "version": "18.8.3",
+      "resolved": "https://registry.npmjs.org/@types/node/-/node-18.8.3.tgz",
+      "integrity": "sha512-0os9vz6BpGwxGe9LOhgP/ncvYN5Tx1fNcd2TM3rD/aCGBkysb+ZWpXEocG24h6ZzOi13+VB8HndAQFezsSOw1w==",
+      "dev": true
+    },
+    "@types/qs": {
+      "version": "6.9.7",
+      "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz",
+      "integrity": "sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==",
+      "dev": true
+    },
+    "@types/range-parser": {
+      "version": "1.2.4",
+      "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.4.tgz",
+      "integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==",
+      "dev": true
+    },
+    "@types/serve-static": {
+      "version": "1.15.0",
+      "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.0.tgz",
+      "integrity": "sha512-z5xyF6uh8CbjAu9760KDKsH2FcDxZ2tFCsA4HIMWE6IkiYMXfVoa+4f9KX+FN0ZLsaMw1WNG2ETLA6N+/YA+cg==",
+      "dev": true,
+      "requires": {
+        "@types/mime": "*",
+        "@types/node": "*"
+      }
+    },
+    "@types/socket.io": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmjs.org/@types/socket.io/-/socket.io-3.0.2.tgz",
+      "integrity": "sha512-pu0sN9m5VjCxBZVK8hW37ZcMe8rjn4HHggBN5CbaRTvFwv5jOmuIRZEuddsBPa9Th0ts0SIo3Niukq+95cMBbQ==",
+      "dev": true,
+      "requires": {
+        "socket.io": "*"
+      }
+    },
+    "accepts": {
+      "version": "1.3.8",
+      "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
+      "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
+      "dev": true,
+      "requires": {
+        "mime-types": "~2.1.34",
+        "negotiator": "0.6.3"
+      }
+    },
+    "base64id": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz",
+      "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==",
+      "dev": true
+    },
+    "cookie": {
+      "version": "0.4.2",
+      "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz",
+      "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==",
+      "dev": true
+    },
+    "cors": {
+      "version": "2.8.5",
+      "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz",
+      "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==",
+      "dev": true,
+      "requires": {
+        "object-assign": "^4",
+        "vary": "^1"
+      }
+    },
+    "debug": {
+      "version": "4.3.4",
+      "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
+      "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
+      "dev": true,
+      "requires": {
+        "ms": "2.1.2"
+      }
+    },
+    "engine.io": {
+      "version": "6.2.0",
+      "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.2.0.tgz",
+      "integrity": "sha512-4KzwW3F3bk+KlzSOY57fj/Jx6LyRQ1nbcyIadehl+AnXjKT7gDO0ORdRi/84ixvMKTym6ZKuxvbzN62HDDU1Lg==",
+      "dev": true,
+      "requires": {
+        "@types/cookie": "^0.4.1",
+        "@types/cors": "^2.8.12",
+        "@types/node": ">=10.0.0",
+        "accepts": "~1.3.4",
+        "base64id": "2.0.0",
+        "cookie": "~0.4.1",
+        "cors": "~2.8.5",
+        "debug": "~4.3.1",
+        "engine.io-parser": "~5.0.3",
+        "ws": "~8.2.3"
+      }
+    },
+    "engine.io-parser": {
+      "version": "5.0.4",
+      "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.0.4.tgz",
+      "integrity": "sha512-+nVFp+5z1E3HcToEnO7ZIj3g+3k9389DvWtvJZz0T6/eOCPIyyxehFcedoYrZQrp0LgQbD9pPXhpMBKMd5QURg==",
+      "dev": true
+    },
+    "mime-db": {
+      "version": "1.52.0",
+      "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+      "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+      "dev": true
+    },
+    "mime-types": {
+      "version": "2.1.35",
+      "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+      "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+      "dev": true,
+      "requires": {
+        "mime-db": "1.52.0"
+      }
+    },
+    "ms": {
+      "version": "2.1.2",
+      "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
+      "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
+      "dev": true
+    },
+    "negotiator": {
+      "version": "0.6.3",
+      "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
+      "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
+      "dev": true
+    },
+    "object-assign": {
+      "version": "4.1.1",
+      "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
+      "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
+      "dev": true
+    },
+    "socket.io": {
+      "version": "4.5.2",
+      "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.5.2.tgz",
+      "integrity": "sha512-6fCnk4ARMPZN448+SQcnn1u8OHUC72puJcNtSgg2xS34Cu7br1gQ09YKkO1PFfDn/wyUE9ZgMAwosJed003+NQ==",
+      "dev": true,
+      "requires": {
+        "accepts": "~1.3.4",
+        "base64id": "~2.0.0",
+        "debug": "~4.3.2",
+        "engine.io": "~6.2.0",
+        "socket.io-adapter": "~2.4.0",
+        "socket.io-parser": "~4.2.0"
+      }
+    },
+    "socket.io-adapter": {
+      "version": "2.4.0",
+      "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.4.0.tgz",
+      "integrity": "sha512-W4N+o69rkMEGVuk2D/cvca3uYsvGlMwsySWV447y99gUPghxq42BxqLNMndb+a1mm/5/7NeXVQS7RLa2XyXvYg==",
+      "dev": true
+    },
+    "socket.io-parser": {
+      "version": "4.2.1",
+      "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.1.tgz",
+      "integrity": "sha512-V4GrkLy+HeF1F/en3SpUaM+7XxYXpuMUWLGde1kSSh5nQMN4hLrbPIkD+otwh6q9R6NOQBN4AMaOZ2zVjui82g==",
+      "dev": true,
+      "requires": {
+        "@socket.io/component-emitter": "~3.1.0",
+        "debug": "~4.3.1"
+      }
+    },
+    "typescript": {
+      "version": "4.8.4",
+      "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.8.4.tgz",
+      "integrity": "sha512-QCh+85mCy+h0IGff8r5XWzOVSbBO+KfeYrMQh7NJ58QujwcE22u+NUSmUxqF+un70P9GXKxa2HCNiTTMJknyjQ=="
+    },
+    "vary": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
+      "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
+      "dev": true
+    },
+    "ws": {
+      "version": "8.2.3",
+      "resolved": "https://registry.npmjs.org/ws/-/ws-8.2.3.tgz",
+      "integrity": "sha512-wBuoj1BDpC6ZQ1B7DWQBYVLphPWkm8i9Y0/3YdHjHKHiohOJ1ws+3OccDWtH+PoC9DZD5WOTrJvNbWvjS6JWaA==",
+      "dev": true,
+      "requires": {}
+    }
+  }
+}
diff --git a/common/package.json b/common/package.json
new file mode 100644
index 0000000..78e2bb3
--- /dev/null
+++ b/common/package.json
@@ -0,0 +1,25 @@
+{
+  "name": "jami-web-common",
+  "version": "1.0.0",
+  "license": "ISC",
+  "type": "module",
+  "main": "dist/index.js",
+  "module": "dist/index.js",
+  "types": "dist/index.d.ts",
+  "files": [
+    "dist/index.js"
+  ],
+  "scripts": {
+    "build": "tsc --build",
+    "clean": "rm -rf dist tsconfig.tsbuildinfo",
+    "lint": "eslint src",
+    "lint:fix": "npm run lint -- --fix",
+    "format": "prettier --write src",
+    "format:check": "prettier --check src"
+  },
+  "dependencies": {
+    "@types/express-session": "^1.17.5",
+    "@types/socket.io": "^3.0.2",
+    "typescript": "^4.8.4"
+  }
+}
diff --git a/common/src/Account.ts b/common/src/Account.ts
new file mode 100644
index 0000000..70acb52
--- /dev/null
+++ b/common/src/Account.ts
@@ -0,0 +1,239 @@
+/*
+ * Copyright (C) 2022 Savoir-faire Linux Inc.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation; either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public
+ * License along with this program.  If not, see
+ * <https://www.gnu.org/licenses/>.
+ */
+import { AccountDetails, VolatileDetails } from './AccountDetails.js';
+import { Contact } from './Contact.js';
+import { Conversation } from './Conversation.js';
+import { Lookup, PromiseExecutor } from './util.js';
+
+type Devices = Record<string, string>;
+
+export type RegistrationState =
+  | 'UNREGISTERED'
+  | 'TRYING'
+  | 'REGISTERED'
+  | 'ERROR_GENERIC'
+  | 'ERROR_AUTH'
+  | 'ERROR_NETWORK'
+  | 'ERROR_HOST'
+  | 'ERROR_SERVICE_UNAVAILABLE'
+  | 'ERROR_NEED_MIGRATION'
+  | 'INITIALIZING';
+
+interface AccountRegisteringName extends PromiseExecutor<number> {
+  name: string;
+}
+
+export class Account {
+  private readonly id: string;
+  private _details: AccountDetails;
+  private _volatileDetails: VolatileDetails;
+  private readonly contactCache: Record<string, Contact>;
+  private _contacts: Contact[];
+  private readonly conversations: Record<string, Conversation>;
+  private defaultModerators: Contact[];
+  private _lookups: Lookup[];
+  private devices: Devices;
+  private _registrationState: RegistrationState | undefined;
+  private _registeringName: AccountRegisteringName | undefined;
+
+  static TYPE_JAMI: string;
+  static TYPE_SIP: string;
+  static BOOL_TRUE: string;
+  static BOOL_FALSE: string;
+
+  constructor(id: string, details: AccountDetails, volatileDetails: VolatileDetails) {
+    this.id = id;
+    this._details = details || {};
+    this._volatileDetails = volatileDetails || {};
+    this.contactCache = {};
+    this._contacts = [];
+    this.conversations = {};
+    this.defaultModerators = [];
+    this._lookups = [];
+    this.devices = {};
+    this.registrationState = undefined;
+    this._registeringName = undefined;
+  }
+
+  static from(object: any) {
+    const account = new Account(object.id, object.details, object.volatileDetails);
+    if (object.defaultModerators) account.defaultModerators = object.defaultModerators.map((m: any) => Contact.from(m));
+    return account;
+  }
+
+  update(data: Account) {
+    this._details = data._details;
+    this._volatileDetails = data._volatileDetails;
+  }
+
+  async getObject() {
+    const hasModerators = this.defaultModerators && this.defaultModerators.length;
+    return {
+      id: this.id,
+      details: this._details,
+      defaultModerators: hasModerators
+        ? await Promise.all(this.defaultModerators.map(async (c) => await c.getObject()))
+        : undefined,
+      volatileDetails: this._volatileDetails,
+    };
+  }
+
+  getId() {
+    return this.id;
+  }
+
+  getType() {
+    return this._details['Account.type'];
+  }
+
+  getUri() {
+    return this._details['Account.username'];
+  }
+
+  getRegisteredName() {
+    return this._volatileDetails['Account.registeredName'];
+  }
+
+  isRendezVous() {
+    return this._details['Account.rendezVous'] === Account.BOOL_TRUE;
+  }
+
+  isPublicIn() {
+    return this._details['DHT.PublicInCalls'] === Account.BOOL_TRUE;
+  }
+
+  setDetail(detail: keyof AccountDetails, value: string) {
+    this._details[detail] = value;
+  }
+
+  updateDetails(details: Partial<AccountDetails>) {
+    return Object.assign(this._details, details);
+  }
+
+  getDetails() {
+    return this._details;
+  }
+
+  getSummary() {
+    return this.getObject();
+  }
+
+  getDisplayName() {
+    return this._details['Account.displayName'] || this.getDisplayUri();
+  }
+
+  getDisplayUri() {
+    return this.getRegisteredName() || this.getUri();
+  }
+
+  getDisplayNameNoFallback() {
+    return this._details['Account.displayName'] || this.getRegisteredName();
+  }
+
+  getConversationIds() {
+    return Object.keys(this.conversations);
+  }
+
+  getConversations() {
+    return this.conversations;
+  }
+
+  getConversation(conversationId: string) {
+    return this.conversations[conversationId];
+  }
+
+  addConversation(conversation: Conversation) {
+    const conversationId = conversation.getId();
+    if (conversationId != null) {
+      this.conversations[conversationId] = conversation;
+    } else {
+      throw new Error('Conversation ID cannot be undefined');
+    }
+  }
+
+  removeConversation(conversationId: string) {
+    delete this.conversations[conversationId];
+  }
+
+  getContactFromCache(uri: string) {
+    let contact = this.contactCache[uri];
+    if (!contact) {
+      contact = new Contact(uri);
+      this.contactCache[uri] = contact;
+    }
+    return contact;
+  }
+
+  getContacts() {
+    return this._contacts;
+  }
+
+  set contacts(contacts: Contact[]) {
+    this._contacts = contacts;
+  }
+
+  getDefaultModerators() {
+    return this.defaultModerators;
+  }
+
+  set details(value: AccountDetails) {
+    this._details = value;
+  }
+
+  set volatileDetails(value: VolatileDetails) {
+    this._volatileDetails = value;
+  }
+
+  get lookups(): Lookup[] {
+    return this._lookups;
+  }
+
+  set lookups(lookups: Lookup[]) {
+    this._lookups = lookups;
+  }
+
+  setDevices(devices: Devices) {
+    this.devices = { ...devices };
+  }
+
+  getDevices() {
+    return this.devices;
+  }
+
+  get registrationState(): RegistrationState | undefined {
+    return this._registrationState;
+  }
+
+  set registrationState(registrationState: RegistrationState | undefined) {
+    this._registrationState = registrationState;
+  }
+
+  get registeringName(): AccountRegisteringName | undefined {
+    return this._registeringName;
+  }
+
+  set registeringName(registeringName: AccountRegisteringName | undefined) {
+    this._registeringName = registeringName;
+  }
+}
+
+Account.TYPE_JAMI = 'RING';
+Account.TYPE_SIP = 'SIP';
+
+Account.BOOL_TRUE = 'true';
+Account.BOOL_FALSE = 'false';
diff --git a/common/src/AccountDetails.ts b/common/src/AccountDetails.ts
new file mode 100644
index 0000000..69968c0
--- /dev/null
+++ b/common/src/AccountDetails.ts
@@ -0,0 +1,187 @@
+/*
+ * Copyright (C) 2022 Savoir-faire Linux Inc.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation; either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public
+ * License along with this program.  If not, see
+ * <https://www.gnu.org/licenses/>.
+ */
+
+/**
+ * Account parameters
+ *
+ * See `jami-daemon/src/account_schema.h`
+ */
+export interface AccountDetails {
+  // Common account parameters
+  'Account.type': string;
+  'Account.alias': string;
+  'Account.displayName': string;
+  'Account.mailbox': string;
+  'Account.enable': string;
+  'Account.autoAnswer': string;
+  'Account.sendReadReceipt': string;
+  'Account.rendezVous': string;
+  'Account.registrationExpire': string;
+  'Account.dtmfType': string;
+  'Account.ringtonePath': string;
+  'Account.ringtoneEnabled': string;
+  'Account.videoEnabled': string;
+  'Account.keepAliveEnabled': string;
+  'Account.presenceEnabled': string;
+  'Account.presencePublishSupported': string;
+  'Account.presenceSubscribeSupported': string;
+  'Account.presenceStatus': string;
+  'Account.presenceNote': string;
+
+  'Account.hostname': string;
+  'Account.username': string;
+  'Account.routeset': string;
+  'Account.allowIPAutoRewrite': string;
+  'Account.password': string;
+  'Account.realm': string;
+  'Account.useragent': string;
+  'Account.hasCustomUserAgent': string;
+  'Account.audioPortMin': string;
+  'Account.audioPortMax': string;
+  'Account.videoPortMin': string;
+  'Account.videoPortMax': string;
+
+  'Account.bindAddress': string;
+  'Account.localInterface': string;
+  'Account.publishedSameAsLocal': string;
+  'Account.localPort': string;
+  'Account.publishedPort': string;
+  'Account.publishedAddress': string;
+  'Account.upnpEnabled': string;
+  'Account.defaultModerators': string;
+  'Account.localModeratorsEnabled': string;
+  'Account.allModeratorEnabled': string;
+
+  // SIP specific parameters
+  'STUN.server': string;
+  'STUN.enable': string;
+  'TURN.server': string;
+  'TURN.enable': string;
+  'TURN.username': string;
+  'TURN.password': string;
+  'TURN.realm': string;
+
+  // SRTP specific parameters
+  'SRTP.enable': string;
+  'SRTP.keyExchange': string;
+  'SRTP.rtpFallback': string;
+
+  'TLS.listenerPort': string;
+  'TLS.enable': string;
+  'TLS.certificateListFile': string;
+  'TLS.certificateFile': string;
+  'TLS.privateKeyFile': string;
+  'TLS.password': string;
+  'TLS.method': string;
+  'TLS.ciphers': string;
+  'TLS.serverName': string;
+  'TLS.verifyServer': string;
+  'TLS.verifyClient': string;
+  'TLS.requireClientCertificate': string;
+  'TLS.negotiationTimeoutSec': string;
+
+  // DHT specific parameters
+  'DHT.port': string;
+  'DHT.PublicInCalls': string;
+
+  // Volatile parameters
+  'Account.registrationStatus': string;
+  'Account.registrationCode': string;
+  'Account.registrationDescription': string;
+  'Transport.statusCode': string;
+  'Transport.statusDescription': string;
+}
+
+/**
+ * Volatile properties
+ *
+ * See `jami-daemon/src/jami/account_const.h`
+ */
+export interface VolatileDetails {
+  'Account.active': string;
+  'Account.deviceAnnounced': string;
+  'Account.registeredName': string;
+}
+
+/**
+ * See `ConfProperties` in `jami-daemon/src/jami/account_const.h
+ */
+export interface AccountConfig {
+  id?: string;
+  type?: string;
+  alias?: string;
+  displayName?: string;
+  enable?: boolean;
+  mailbox?: string;
+  dtmfType?: string;
+  autoAnswer?: boolean;
+  sendReadReceipt?: string;
+  rendezVous?: boolean;
+  activeCallLimit?: string;
+  hostname?: string;
+  username?: string;
+  bindAddress?: string;
+  routeset?: string;
+  password?: string;
+  realm?: string;
+  localInterface?: string;
+  publishedSameAsLocal?: boolean;
+  localPort?: string;
+  publishedPort?: string;
+  publishedAddress?: string;
+  useragent?: string;
+  upnpEnabled?: boolean;
+  hasCustomUserAgent?: string;
+  allowCertFromHistory?: string;
+  allowCertFromContact?: string;
+  allowCertFromTrusted?: string;
+  archivePassword?: string;
+  archiveHasPassword?: string;
+  archivePath?: string;
+  archivePIN?: string;
+  deviceID?: string;
+  deviceName?: string;
+  proxyEnabled?: boolean;
+  proxyServer?: string;
+  proxyPushToken?: string;
+  keepAliveEnabled?: boolean;
+  peerDiscovery?: string;
+  accountDiscovery?: string;
+  accountPublish?: string;
+  managerUri?: string;
+  managerUsername?: string;
+  bootstrapListUrl?: string;
+  dhtProxyListUrl?: string;
+  defaultModerators?: string;
+  localModeratorsEnabled?: boolean;
+  allModeratorsEnabled?: boolean;
+  allowIPAutoRewrite?: string;
+
+  // Audio
+  audioPortMax?: string;
+  audioPortMin?: string;
+
+  // Video
+  videoEnabled?: boolean;
+  videoPortMax?: boolean;
+  videoPortMin?: string;
+
+  // Ringtone
+  ringtonePath?: string;
+  ringtoneEnabled?: boolean;
+}
diff --git a/common/src/Contact.ts b/common/src/Contact.ts
new file mode 100644
index 0000000..88e1916
--- /dev/null
+++ b/common/src/Contact.ts
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2022 Savoir-faire Linux Inc.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation; either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public
+ * License along with this program.  If not, see
+ * <https://www.gnu.org/licenses/>.
+ */
+export class Contact {
+  private readonly uri: string;
+  private readonly displayName: string | undefined;
+  private registeredName: string | undefined;
+
+  constructor(uri: string) {
+    this.uri = uri;
+    this.displayName = undefined;
+    this.registeredName = undefined;
+  }
+
+  static from(object: any) {
+    const contact = new Contact(object.uri);
+    if (object.registeredName) contact.setRegisteredName(object.registeredName);
+    return contact;
+  }
+
+  getUri() {
+    return this.uri;
+  }
+
+  getRegisteredName() {
+    return this.registeredName;
+  }
+
+  setRegisteredName(name: string | undefined) {
+    this.registeredName = name;
+  }
+
+  isRegisteredNameResolved() {
+    return this.registeredName !== undefined;
+  }
+
+  getDisplayName() {
+    return this.getDisplayNameNoFallback() || this.getUri();
+  }
+
+  getDisplayNameNoFallback() {
+    return this.displayName || this.getRegisteredName();
+  }
+
+  async getObject() {
+    return {
+      uri: this.uri,
+      registeredName: await this.registeredName,
+    };
+  }
+}
diff --git a/common/src/Conversation.ts b/common/src/Conversation.ts
new file mode 100644
index 0000000..01b70cd
--- /dev/null
+++ b/common/src/Conversation.ts
@@ -0,0 +1,171 @@
+/*
+ * Copyright (C) 2022 Savoir-faire Linux Inc.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation; either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public
+ * License along with this program.  If not, see
+ * <https://www.gnu.org/licenses/>.
+ */
+import { Socket } from 'socket.io';
+
+import { Contact } from './Contact.js';
+import { PromiseExecutor, Session } from './util.js';
+
+export interface ConversationMember {
+  contact: Contact;
+  role?: 'admin' | 'member' | 'invited' | 'banned' | 'left';
+}
+
+type ConversationInfos = Record<string, unknown>;
+
+export type Message = Record<string, string>;
+type ConversationRequest = PromiseExecutor<Message[]>;
+
+type ConversationListeners = Record<
+  string,
+  {
+    socket: Socket;
+    session: Session;
+  }
+>;
+
+export class Conversation {
+  private readonly id: string | undefined;
+  private readonly accountId: string;
+  private readonly members: ConversationMember[];
+  private messages: Message[];
+  private _infos: ConversationInfos;
+  private _requests: Record<string, ConversationRequest>;
+  private _listeners: ConversationListeners;
+
+  constructor(id: string | undefined, accountId: string, members?: ConversationMember[]) {
+    this.id = id;
+    this.accountId = accountId;
+    this.members = members || [];
+
+    this.messages = [];
+    this._infos = {};
+    this._requests = {};
+    this._listeners = {};
+  }
+
+  static from(accountId: string, object: any) {
+    const conversation = new Conversation(
+      object.id,
+      accountId,
+      object.members.map((member: any) => {
+        member.contact = Contact.from(member.contact);
+        return member;
+      })
+    );
+    conversation.messages = object.messages;
+    return conversation;
+  }
+  static fromSingleContact(accountId: string, contact: Contact) {
+    return new Conversation(undefined, accountId, [{ contact }]);
+  }
+
+  getId() {
+    return this.id;
+  }
+
+  getAccountId() {
+    return this.accountId;
+  }
+
+  getDisplayName() {
+    if (this.members.length !== 0) {
+      return this.members[0].contact.getDisplayName();
+    }
+    return this.getDisplayUri();
+  }
+
+  getDisplayNameNoFallback() {
+    if (this.members.length !== 0) {
+      return this.members[0].contact.getDisplayNameNoFallback();
+    }
+  }
+
+  async getObject(params?: {
+    memberFilter: (value: ConversationMember, index: number, array: ConversationMember[]) => boolean;
+  }) {
+    const members = params?.memberFilter ? this.members.filter(params.memberFilter) : this.members;
+    return {
+      id: this.id,
+      messages: this.messages,
+      members: await Promise.all(
+        members.map(async (member) => {
+          //Object.assign({}, member);
+          return {
+            role: member.role,
+            contact: await member.contact.getObject(),
+          };
+        })
+      ),
+    };
+  }
+
+  getSummary() {
+    return this.getObject();
+  }
+
+  getDisplayUri() {
+    return this.getId() || this.getFirstMember().contact.getUri();
+  }
+
+  getFirstMember() {
+    return this.members[0];
+  }
+
+  getMembers() {
+    return this.members;
+  }
+
+  addMessage(message: Message) {
+    if (this.messages.length === 0) this.messages.push(message);
+    else if (message.id === this.messages[this.messages.length - 1].linearizedParent) {
+      this.messages.push(message);
+    } else if (message.linearizedParent === this.messages[0].id) {
+      this.messages.unshift(message);
+    } else {
+      console.log("Can't insert message " + message.id);
+    }
+  }
+
+  addLoadedMessages(messages: Message[]) {
+    messages.forEach((message) => this.addMessage(message));
+  }
+
+  getMessages() {
+    return this.messages;
+  }
+
+  set infos(infos: ConversationInfos) {
+    this._infos = infos;
+  }
+
+  get requests(): Record<string, ConversationRequest> {
+    return this._requests;
+  }
+
+  set requests(value: Record<string, ConversationRequest>) {
+    this._requests = value;
+  }
+
+  get listeners(): ConversationListeners {
+    return this._listeners;
+  }
+
+  set listeners(listeners: ConversationListeners) {
+    this._listeners = listeners;
+  }
+}
diff --git a/common/src/index.ts b/common/src/index.ts
new file mode 100644
index 0000000..e58fd49
--- /dev/null
+++ b/common/src/index.ts
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2022 Savoir-faire Linux Inc.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation; either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public
+ * License along with this program.  If not, see
+ * <https://www.gnu.org/licenses/>.
+ */
+export * from './Account.js';
+export * from './AccountDetails.js';
+export * from './Contact.js';
+export * from './Conversation.js';
+export * from './util.js';
diff --git a/common/src/util.ts b/common/src/util.ts
new file mode 100644
index 0000000..c814ba1
--- /dev/null
+++ b/common/src/util.ts
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2022 Savoir-faire Linux Inc.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation; either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public
+ * License along with this program.  If not, see
+ * <https://www.gnu.org/licenses/>.
+ */
+import { Session as ISession } from 'express-session';
+
+export interface PromiseExecutor<T> {
+  resolve: (value: T) => void;
+  reject: (reason?: any) => void;
+}
+
+export interface LookupResolveValue {
+  address: string;
+  name: string;
+  state: number;
+}
+
+export interface Lookup extends PromiseExecutor<LookupResolveValue> {
+  name?: string;
+  address?: string;
+}
+
+export interface Session extends ISession {
+  socketId: string;
+  conversation: any;
+}
diff --git a/common/tsconfig.json b/common/tsconfig.json
new file mode 100644
index 0000000..b03c254
--- /dev/null
+++ b/common/tsconfig.json
@@ -0,0 +1,9 @@
+{
+  "extends": "../tsconfig",
+  "compilerOptions": {
+    "declaration": true,
+    "outDir": "dist",
+    "rootDir": "src"
+  },
+  "include": ["src"]
+}