{"id":22955147,"url":"https://github.com/tuukkaviitanen/messenger","last_synced_at":"2026-04-11T03:15:10.254Z","repository":{"id":206658124,"uuid":"717369833","full_name":"tuukkaviitanen/messenger","owner":"tuukkaviitanen","description":"Web chat application using React frontend and Node backend ","archived":false,"fork":false,"pushed_at":"2024-12-28T15:41:34.000Z","size":847,"stargazers_count":0,"open_issues_count":2,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-02-07T16:16:39.406Z","etag":null,"topics":["docker","github-actions","material-ui","nodejs","postgresql","react","socket-io","typeorm","typescript"],"latest_commit_sha":null,"homepage":"https://messenger.tuukka.net","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/tuukkaviitanen.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2023-11-11T09:15:04.000Z","updated_at":"2024-12-28T15:38:12.000Z","dependencies_parsed_at":"2024-01-06T12:54:31.007Z","dependency_job_id":"c4448869-61f5-4355-a71b-0fc67cf2af90","html_url":"https://github.com/tuukkaviitanen/messenger","commit_stats":null,"previous_names":["tuukkaviitanen/messenger"],"tags_count":38,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tuukkaviitanen%2Fmessenger","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tuukkaviitanen%2Fmessenger/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tuukkaviitanen%2Fmessenger/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tuukkaviitanen%2Fmessenger/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/tuukkaviitanen","download_url":"https://codeload.github.com/tuukkaviitanen/messenger/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":246735343,"owners_count":20825223,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["docker","github-actions","material-ui","nodejs","postgresql","react","socket-io","typeorm","typescript"],"created_at":"2024-12-14T16:28:03.278Z","updated_at":"2025-12-30T20:27:33.672Z","avatar_url":"https://github.com/tuukkaviitanen.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Messenger application\n\n\u003e Full Stack chat application using Node backend and React frontend. Currently includes a global chatroom and private chats.\n\nCheck out live at https://messenger.tuukka.net\n\n## Summary\n\nNode server has a [REST API](https://www.ibm.com/topics/rest-apis) using [Express](https://expressjs.com/) and WebSockets server using [Socket.io](https://socket.io/).\n\nUsers are validated and stored in a [PostgreSQL](https://www.postgresql.org/) database through [TypeORM](https://typeorm.io/). Encrypted private messages are also stored in the postgres database, while global chat messages are stored in a [Redis](https://redis.io/) database for one hour.\n\nREST API is used for user management and WebSockets are used to transport messages between users real-time.\n\nProject is built with [TypeScript](https://www.typescriptlang.org/).\n[Zod](https://zod.dev/)-library is used to validate all inbound data at runtime as TypeScript types exist only until it is compiled into JavaScript. [ESLint](https://eslint.org/) is used to enforce coding-style and format. Both frontend and backend ESLint setups are based on [XO](https://github.com/xojs/xo) rule set.\n\n### User instructions\n\nNew user can be created on the login page. Logging in with your user opens the global chatroom. There you can chat with everyone currently using the app. These messages are saved for one hour and then deleted.\n\nOnline users are shown in the bottom right corner of the page. Another online user can be clicked to open a private chatroom with them. Existing chatrooms are shown on the right. Private messages are stored indefinitely.\n\nMultiline message field can be toggled on the bottom left corner for longer messages or ASCII art.\n\n\n## Docker\n\nThis application can be fully containerized. A [Docker](https://www.docker.com/) image is created on every release. They are stored in the [GitHub Container Registry (ghcr.io)](https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-container-registry). These images can be found in this repository under `Packages`. Images are tagged with the same release version number as the commit. Latest version is also tagged with the `latest` tag.\n\n### Dockerfile\n\nThe Dockerfile used in this application is using a multi-stage build. This means that the application is built in the first stage, and just the final builds are copied to the final stage. Build-stage is not included in the image.\n\nFrom the frontend, only the dist directory is carried to the final image. From the backend, only the compiled JavaScript files and the production dependencies are carried over. No tests, no TypeScript, and no development dependencies. This reduces the image size to a fraction of what it would be.\n\nThe dockerfile is also using a optimized [base Node image](https://hub.docker.com/_/node/). I chose 20-bookworm-slim. The alpine-version is the smallest, but is using an unofficial Node version and has some other disadvantages. ([Read more about the differences](https://snyk.io/blog/choosing-the-best-node-js-docker-image/))\n\n\n\n### Image usage\n\nRunning this image requires the same environment variables as the app would normally need. This is the template setup that is used in the `docker-compose.standalone.yml`. \u003cb\u003eDon't use the same secret\u003c/b\u003e.\n```\nPORT: 3000\nPOSTGRES_URL: postgres://postgres:postgres@postgres:5432/postgres\nSECRET: sdfnwef80wejw8fjw489fjw48fjw4893f89w4g\nREDIS_URL: redis://redis:6379\n```\n\n### ~~Render Docker image deployment~~\n\n~~The app is deployed in Render also as a [Docker image based web service](https://docs.render.com/deploy-an-image). The CI workflow also deploys the newly created image on Render. For the moment, the \u003ci\u003eWait for deployment\u003c/i\u003e -action doesn't seem to support tracking these prebuilt image web services. Successful deployment therefore is not guaranteed. This is not critical for now, as this secondary deployment is just a demonstration that it can be deployed both ways.~~\n\n~~I would otherwise prefer this containerized deployment as there is less configuration on the hosting service. Most of the configuration is done in the dockerfile, and the image content and size can be more optimized.~~\n\n~~These separate deployments also have no real-time communication with each other, despite sharing the same databases and this way recovering all messages on a page refresh. I'm interested in looking further into how these real-time chat applications could be scaled horizontally with multiple containers and servers.~~\n\n~~The deployed containerized version found here: https://messenger-containerized.onrender.com~~\n\n~~More on GitHub Actions and setup below.~~\n\n## CI/CD\n\nThere is a [Github Actions workflow](https://docs.github.com/en/actions/using-workflows/about-workflows) in place to lint, build, test, tag \u0026 deploy the application. Everything is tested in pull requests so broken code doesn't get into main branch.\n\nThe the CI/CD pipeline consist of these jobs:\n\n### build\n\nCheckouts the code, installs dependencies, runs eslint, builds projects, runs tests (integration tests and end-to-end tests).\n\nBackend integration testing requires a test database, so the build pipeline runs a PostgreSQL Docker container as a service that is used in the pipeline tests. This guarantees that there aren't any external dependencies that would fail the tests (in this case, losing connection to remote test database)\n\nBuild job is actually done 3 times concurrently. Each with a different node version: 16.x, 18.x and 20.x. Running different versions is redundant and it could be reduced to only run 20.x as that is currently run on production. But in a project this size, it doesn't affect the build length that much so I think it's nice to be alerted if there are version specific problems.\n\n### tag_release\n\nIf build is successful, runs [github-tag-action](https://github.com/anothrNick/github-tag-action) that bumps the tag version of the repository. By default it bumps up the patch version, but minor version and major version can also be bumped by adding #major or #minor to a commit message.\n\n### ~~deployment~~\n\n~~If build and tagging are successful, deploys the main branch to [Render](https://render.com/) using [render-deploy-action](https://github.com/johnbeynon/render-deploy-action).~~\n~~After initializing the deployment, waits for deployment to finish successfully using [render-action](https://github.com/Bounceapp/render-action). It's good to note that these are separate steps to make locating possible problems easier. Free tier render services might take a long time to build and start up if there's a lot of traffic.~~\n\n### publish_docker_image\n\nIf build and tagging are successful, creates a docker image from the application. Also deploys the image to GitHub Container Repository. It gets the version from the tag_release job output, and sets it as a tag for the image. It also sets the `latest` tag on the image.\n\n### container_deployment\n\nDeploys the latest published image to Render using the same deploy action used in the repository-based deployment. The wait action is not used as it doesn't currently support image-based deployments.\n\n\n## Tests\n\nThe project contains integration tests and end-to-end tests. All tests are currently located in the backend, but E2E tests test the frontend as well.\n\n### Integration tests\n\nThere are integration tests for REST API Endpoints and WebSocket events.\n\nThese tests use the [Jest](https://jestjs.io/)-library for tests.\nREST API endpoints are tested with [Supertest](https://github.com/ladjs/supertest#readme).\n[Socket.io](https://socket.io/) events are tested with multiple client sockets in the backend. These tests make sure that messages are received in correct form and are sent to the correct users.\n\n### End-to-End tests\n\nE2E tests are done using [Cypress](https://docs.cypress.io/)\nE2E tests are run when the whole application is running.\nCypress opens a browser and tests the application more like a user would. All essential actions are tested.\n\nE2E tests are important for the CI/CD pipeline. Finishing these tests successfully means that the application works as a whole, even when built in a different environment.\n\n## Backend\n\n### User management\n\nBackend runs an express server with `/api/users` and `/api/login` endpoints. Users can be created and single users can be fetched with id. Passwords are hashed before storage. Login returns a [JsonWebToken](https://jwt.io/) that can be used for authentication in following requests.\n\n## Databases\n\n### PostgreSQL\n\nUsers and private messages are stored in a [PostgreSQL](https://www.postgresql.org/) database through [TypeORM](https://typeorm.io/).\n\nDatabase schema is updated with migrations. Migrations are generated with [TypeORM CLI](https://orkhan.gitbook.io/typeorm/docs/using-cli). Migrations run at server startup or manually. Tests also run migrations, so broken migrations won't get through the CI/CD pipeline.\n\n### Redis\n\nGlobal messages and server events (such as user joined) are stored in a [Redis](https://redis.io/) database. Redis is a key-value database that stores data in-memory (by default) so everything can be accessed fast and easily. Data stored in-memory also disappears when the server is closed.\n\nGlobal messages and events are not using complex relations, and there is no need to store them persistently. On the contrary, I would prefer to store them for only about an hour to not bloat the front page or the database. I thought this would be a great chance to learn more about Redis.\n\nMessages and events are stored in a list structure. Unfortunately, each list element can't be assigned it's own [TTL](https://redis.io/commands/ttl/) (Time-To-Live), so the whole list is cleared after an hour of no new updates. This is fine when there is not an around-the-clock active user base, but should find a better option for larger scale. Each message could be stored as it's own key (and TTL), but the speed and efficiency of this should be tested more.\n\n### Encryption\n\nPrivate messages are stored to database so they can be restored after a page refresh or logging back in.\nTo keep private messages actually private, message contents are encrypted with an AES-256 encryption before storage. Column encryption is done using [typeorm-encrypted](https://www.npmjs.com/package/typeorm-encrypted).\n\nThis prevents reading plain text messages from the database. Server-side SECRET environment variable (hashed with SHA-256) is needed to decrypt the message.\n\n### Migration from Sequelize to TypeORM\n\nBackend was using [Sequelize](https://sequelize.org/) as an ORM up to version 0.0.8.\nI made the decision to migrate to TypeORM because of Sequelize's poor TypeScript support.\n\nSequelize supports TypeScript, but the types have to be added manually and are separate from the actual Sequelize model. [See Sequelize documentation for an example](https://sequelize.org/docs/v6/other-topics/typescript/#usage). This negates the point of using types, as developer can set the types invalidly as they become really complex with relations.\n\nA great benefit of TypeORM is that migrations can be generated automatically. Generated migrations also consist of pure [SQL](https://en.wikipedia.org/wiki/SQL) scripts instead of being fully JavaScript/TypeScript. This let's the developer know exactly what the migrations are doing.\n\nTypeORM is a great upgrade from Sequelize as it offers more control on migrations while automating type creation and actually writing the migrations.\n\nTo me personally, it feels like TypeORM abstracts more of the process, while giving the developer control on just the few things that matter, giving the developer a feeling of being in even more control.\n\n## Frontend\n\nFrontend is built with [React](https://react.dev/) and styled with [Material UI](https://mui.com/material-ui/).\n\n[Redux](https://redux.js.org/) is used for state management. Client's state isn't too complicated so [React's Context API](https://react.dev/learn/passing-data-deeply-with-context) could have been used instead, but I was interested in learning more about Redux and at least it leaves room for scaling.\n\n[Full-Duplex](https://techterms.com/definition/full-duplex) connection to server is created using [Socket.io](https://socket.io/). It primarily uses WebSockets to send messages both ways. All socket connections are authenticated with the JsonWebToken that is returned from the server on successful login. JWT is currently stored in [LocalStorage](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage).\n\n[CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) is not needed even when running the client in development mode as Vite is configured to proxy these connections to server port and to change the origin. This works for both HTTP and WebSocket connections.\n\n## Setup\n\n### Environment\n\n#### Node version\n\nThe build process requires a least Node version 15.x or higher because of some dependencies. Some dependencies warn that to get the optimal performance, at least version 18.x is needed.\nIt's good to note that these are dev dependencies and running the built application in production might be possible with a lower version of node.\n\nThe production version is running version 20.9.0.\n\n### Backend\n\n1. Change to `/server` directory\n2. Run `npm install`\n3. Create `.env` file with required env variables\n   a. Check out `.env.template` for instructions or copy the contents of `.env.dev` that are configured to be used with `docker-compose.dev.yml`.\n   b. This requires setting up PostgreSQL and Redis databases. This can be done easily with `docker-compose.dev.yml` in the root directory. This requires Docker.\n   c. NOTE: Use separate databases and your own secret for production.\n4. For the server to serve the client, client project needs to be built. Look for setup instructions below.\n5. Now you can run the application with following commands:\n   a. Run `npm run dev` for development with automatic reloading on save.\n   b. Run `npm run build` and `npm start` to build and run for production.\n   c. Run `npm test` for running tests\n   d. Run `npm start:test` to start server with `test` mode (required for E2E tests)\n   e. Run `npm test:e2e` to run End-to-End test while server is running in `test` mode. Run this in a separate terminal.\n\n#### Database migrations\n\nDatabase migrations are run automatically when running the server or tests.\nMigrations can also be run manually to testing database with `npm run migration:run`.\nMigrations can be generated with `npm run migration:generate --name=\u003cmigration name\u003e`.\nThese migration commands both use the test database so it needs to be set up. (See instructions above and in .env.template).\nExisting migrations also have to be run before generating new migrations.\n\n### Frontend\n\n1. Change to `/client` directory\n2. Run `npm install`\n3. Now you can run the application with following commands:\n   a. Run `npm run dev` for development with hot reload. Server still needs to be running.\n   b. Run `npm run build` to build static files. This needs to be done for the backend to start serving the client.\n\n## Usage\n\n### Backend\n\nExpress API endpoints:\n\nThese URLs work when running locally and port is left to default (3000)\n\n```\nCreate user:\nPOST http://localhost:3000/api/users\nREQUEST BODY { \"username\": \"username\", \"password\": \"password\"}\n\nGet single user:\nGET http://localhost:3000/api/users/:id\n\nLogin (get token):\nPOST http://localhost:3000/api/login\nREQUEST BODY { \"username\": \"username\", \"password\": \"password\"}\n```\n\n### Frontend\n\nClient can be accessed in the localhost port that is printed to console after running the application. e.g. http://localhost:3000.\n\nWhen running frontend separately in dev mode, client should be accessed in a separate URL that is printed to frontend dev server console.\n\nWhen running only the server, client can be accessed at the root path of the server URL.\n\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftuukkaviitanen%2Fmessenger","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ftuukkaviitanen%2Fmessenger","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftuukkaviitanen%2Fmessenger/lists"}