diff --git a/client/public/index.html b/client/public/index.html
index 5195978ccc73c6ac5a1bcfe9728534d65dd5ba64..4d4313cf33978f9188b411cd663f485670e507d2 100644
--- a/client/public/index.html
+++ b/client/public/index.html
@@ -2,7 +2,7 @@
 <html>
   <head>
     <meta charset="UTF-8" />
-    <title>Todo web application example</title>
+    <title>Chat app</title>
     <link rel="stylesheet" href="bootstrap.min.css" />
   </head>
   <body>
diff --git a/client/src/chat-component.tsx b/client/src/chat-component.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..8c402fe5199bedae074ac878f033aa1af6449fcf
--- /dev/null
+++ b/client/src/chat-component.tsx
@@ -0,0 +1,109 @@
+import * as React from 'react';
+import { Component } from 'react-simplified';
+import chatService, { Subscription } from './chat-service';
+import { Alert, Card, Column, Form, Row } from './widgets';
+import { KeyboardEvent } from 'react';
+
+export class Chat extends Component {
+  subscription: Subscription | null = null;
+  connected = false;
+  users: string[] = [];
+  messages: string[] = [];
+  message = '';
+  user = '';
+
+  render() {
+    return (
+      <>
+        <Card title={this.connected ? 'Chat(Connected)' : 'Chat(Not connected)'}>
+          <Card title="Connected users">
+            {this.users.map((user, index) => {
+              return <div key={index}>{user}</div>;
+            })}
+          </Card>
+          <Card title="Messages">
+            {this.messages.map((message, index) => {
+              return <div key={index}>{message}</div>;
+            })}
+          </Card>
+          <Card title="New message">
+            <Row>
+              <Column width={2}>
+                <Form.Input
+                  type="text"
+                  placeholder="Enter username"
+                  value={this.user}
+                  disabled={this.subscription}
+                  onChange={(event) => {
+                    this.user = event.currentTarget.value;
+                  }}
+                  onKeyUp={(event: KeyboardEvent<HTMLInputElement>) => {
+                    if (event.key == 'Enter') { 
+                      if (!this.subscription) {
+                        this.subscription = chatService.subscribe();
+                        this.subscription.onopen = () => {
+                          this.connected = true;
+                          chatService.send({ addUser: this.user });
+
+                          // Remove user when web page is closed
+                          window.addEventListener('beforeunload', () =>
+                            chatService.send({ removeUser: this.user })
+                          );
+                        };
+                        // Called on incoming message
+                        this.subscription.onmessage = (message) => {
+                          if ('text' in message) {
+                            this.messages.push(message.text);
+                          }
+                          if ('users' in message) {
+                            this.users = message.users;
+                          }
+                        };
+
+                        // Called if connection is closed
+                        this.subscription.onclose = (code, reason) => {
+                          this.connected = false;
+                          Alert.danger(
+                            'Connection closed with code ' + code + ' and reason: ' + reason
+                          );
+                        };
+                        // Called on connection error
+                        this.subscription.onerror = (error) => {
+                          this.connected = false;
+                          Alert.danger('Connection error: ' + error.message);
+                        };
+                      }
+                    }
+                  }}
+                />
+              </Column>
+              <Column>
+                <Form.Input
+                  placeholder="Message"
+                  type="text"
+                  value={this.message}
+                  onChange={(event) => {
+                    this.message = event.currentTarget.value;
+                  }}
+                  onKeyUp={(event: KeyboardEvent<HTMLInputElement>) => {
+                    if (event.key == 'Enter') {
+                      if (this.connected) {
+                        chatService.send({ text: this.user + ': ' + this.message });
+                        this.message = '';
+                      } else Alert.danger('Not connected to server');
+                    }
+                  }}
+                />
+              </Column>
+            </Row>
+          </Card>
+        </Card>
+      </>
+    );
+  }
+
+  // Unsubscribe from chatService when component is no longer in use
+  beforeUnmount() {
+    if (this.subscription) chatService.unsubscribe(this.subscription);
+  }
+}
diff --git a/client/src/whiteboard-service.tsx b/client/src/chat-service.tsx
similarity index 79%
rename from client/src/whiteboard-service.tsx
rename to client/src/chat-service.tsx
index 0b17723ea4d96aa7eb9514ba62eb17e9e21542da..f884fbd2a6e8a364c26ca935278745ea14291e31 100644
--- a/client/src/whiteboard-service.tsx
+++ b/client/src/chat-service.tsx
@@ -1,30 +1,31 @@
 /**
  * In and out message type (to and from server).
  */
-export type Message = { line: { from: { x: number; y: number }; to: { x: number; y: number } } };
+export type ClientMessage = { text: string } | { addUser: string } | { removeUser: string };
+export type ServerMessage = { text: string } | { users: string[] };
 
 /**
- * Subscription class that enables multiple components to receive events from Whiteboard server.
+ * Subscription class that enables multiple components to receive events from Chat server.
  */
 export class Subscription {
   onopen: () => void = () => {};
-  onmessage: (message: Message) => void = () => {};
+  onmessage: (message: ServerMessage) => void = () => {};
   onclose: (code: number, reason: string) => void = () => {};
   onerror: (error: Error) => void = () => {};
 }
 
 /**
- * Service class to communicate with Whiteboard server.
+ * Service class to communicate with Chat server.
  *
  * Variables and functions marked with @private should not be used outside of this class.
  */
-class WhiteboardService {
+class ChatService {
   /**
-   * Connection to Whiteboard server.
+   * Connection to Chat server.
    *
    * @private
    */
-  connection = new WebSocket('ws://localhost:3000/api/v1/whiteboard');
+  connection = new WebSocket('ws://localhost:3000/api/v1/chat');
   /**
    * Component subscriptions.
    *
@@ -39,10 +40,9 @@ class WhiteboardService {
     };
 
     this.connection.onmessage = (event) => {
-      // Call subscription onmessage functions on messages from Whiteboard server
+      // Call subscription onmessage functions on messages from chat server
       const data = event.data;
-      console.log(data);
-      if (typeof data == 'string')
+      if (typeof data === 'string')
         this.subscriptions.forEach((subscription) => subscription.onmessage(JSON.parse(data)));
     };
 
@@ -73,7 +73,7 @@ class WhiteboardService {
   }
 
   /**
-   * Returns a subscription that enables multiple components to receive events from Whiteboard server.
+   * Returns a subscription that enables multiple components to receive events from chat server.
    */
   subscribe() {
     const subscription = new Subscription();
@@ -95,19 +95,19 @@ class WhiteboardService {
   }
 
   /**
-   * Given subscription will no longer receive events from Whiteboard server.
+   * Given subscription will no longer receive events from chat server.
    */
   unsubscribe(subscription: Subscription) {
-    this.subscriptions.delete(subscription);
+    if (subscription) this.subscriptions.delete(subscription);
   }
 
   /**
-   * Send message to Whiteboard server.
+   * Send message to chat server.
    */
-  send(message: Message) {
+  send(message: ClientMessage) {
     this.connection.send(JSON.stringify(message));
   }
 }
 
-const whiteboardService = new WhiteboardService();
-export default whiteboardService;
+const chatService = new ChatService();
+export default chatService;
diff --git a/client/src/index.tsx b/client/src/index.tsx
index 0fb489d3f0d615268fc7ce28b9c84038342c47aa..f8e79fa545032d06666a3673bd8bd5d3d1037397 100644
--- a/client/src/index.tsx
+++ b/client/src/index.tsx
@@ -1,12 +1,14 @@
 import ReactDOM from 'react-dom';
 import * as React from 'react';
-import { Whiteboard } from './whiteboard-component';
+import { Chat } from './chat-component';
 import { Alert } from './widgets';
 
-ReactDOM.render(
-  <>
-    <Alert />
-    <Whiteboard />
-  </>,
-  document.getElementById('root')
-);
+const root = document.getElementById('root');
+if (root)
+  ReactDOM.render(
+    <>
+      <Alert />
+      <Chat />
+    </>,
+    root
+  );
diff --git a/client/src/whiteboard-component.tsx b/client/src/whiteboard-component.tsx
deleted file mode 100644
index f9001be25fad34739ca6f0d02078c9e0cd2e4178..0000000000000000000000000000000000000000
--- a/client/src/whiteboard-component.tsx
+++ /dev/null
@@ -1,70 +0,0 @@
-import * as React from 'react';
-import { Component } from 'react-simplified';
-import whiteboardService, { Subscription } from './whiteboard-service';
-import { Alert } from './widgets';
-
-export class Whiteboard extends Component {
-  canvas: HTMLCanvasElement | null = null;
-  lastPos: { x: number; y: number } | null = null;
-  subscription: Subscription | null = null;
-  connected = false;
-
-  render() {
-    return (
-      <>
-        <canvas
-          ref={(e) => (this.canvas = e) /* Store canvas element */}
-          onMouseMove={(event) => {
-            // Send lines to Whiteboard server
-            const pos = { x: event.clientX, y: event.clientY };
-            if (this.lastPos && this.connected) {
-              whiteboardService.send({ line: { from: this.lastPos, to: pos } });
-            }
-            this.lastPos = pos;
-          }}
-          width={400}
-          height={400}
-          style={{ border: '2px solid black' }}
-        />
-        <div>{this.connected ? 'Connected' : 'Not connected'}</div>
-      </>
-    );
-  }
-
-  mounted() {
-    // Subscribe to whiteboardService to receive events from Whiteboard server in this component
-    this.subscription = whiteboardService.subscribe();
-
-    // Called when the subscription is ready
-    this.subscription.onopen = () => {
-      this.connected = true;
-    };
-
-    // Called on incoming message
-    this.subscription.onmessage = (message) => {
-      const context = this.canvas?.getContext('2d');
-      context?.beginPath();
-      context?.moveTo(message.line.from.x, message.line.from.y);
-      context?.lineTo(message.line.to.x, message.line.to.y);
-      context?.closePath();
-      context?.stroke();
-    };
-
-    // Called if connection is closed
-    this.subscription.onclose = (code, reason) => {
-      this.connected = false;
-      Alert.danger('Connection closed with code ' + code + ' and reason: ' + reason);
-    };
-
-    // Called on connection error
-    this.subscription.onerror = (error) => {
-      this.connected = false;
-      Alert.danger('Connection error: ' + error.message);
-    };
-  }
-
-  // Unsubscribe from whiteboardService when component is no longer in use
-  beforeUnmount() {
-    if (this.subscription) whiteboardService.unsubscribe(this.subscription);
-  }
-}
diff --git a/client/test/whiteboard-component.test.tsx b/client/test/chat-component.test.tsx
similarity index 70%
rename from client/test/whiteboard-component.test.tsx
rename to client/test/chat-component.test.tsx
index 613411018fc7064baa3e753174ec8f225899bb29..237b0859ed71fe90d7d56de6ff44296c46131c4b 100644
--- a/client/test/whiteboard-component.test.tsx
+++ b/client/test/chat-component.test.tsx
@@ -1,13 +1,13 @@
 import * as React from 'react';
-import { Whiteboard } from '../src/whiteboard-component';
+import { Chat } from '../src/chat-component';
 import { shallow } from 'enzyme';
 
-jest.mock('../src/whiteboard-service', () => {
+jest.mock('../src/chat-service', () => {
   class Subscription {
     onopen = () => {};
   }
 
-  class WhiteboardService {
+  class ChatService {
     constructor() {}
 
     subscribe() {
@@ -19,12 +19,13 @@ jest.mock('../src/whiteboard-service', () => {
       return subscription;
     }
   }
-  return new WhiteboardService();
+  return new ChatService();
 });
 
-describe('Whiteboard component tests', () => {
+describe('Chat component tests', () => {
   test('draws correctly when connected', (done) => {
-    const wrapper = shallow(<Whiteboard />);
+    // @ts-ignore: do not type check next line.
+    const wrapper = shallow(<Chat />);
 
     expect(wrapper.containsMatchingElement(<div>Not connected</div>)).toEqual(true);
 
diff --git a/server/package.json b/server/package.json
index 21de6d8c380a0eb92f2100d13df37c17affaa8a6..3827ca77420e6c18366c437c57da02943b6c05ff 100644
--- a/server/package.json
+++ b/server/package.json
@@ -18,6 +18,7 @@
     "axios": "^0.21.1",
     "express": "^4.17.1",
     "mysql": "^2.18.1",
+    "reload": "^3.2.0",
     "ws": "^8.1.0"
   },
   "devDependencies": {
diff --git a/server/src/chat-server.ts b/server/src/chat-server.ts
new file mode 100644
index 0000000000000000000000000000000000000000..6184151be48c10df016ba7d80da8dc9063d38e2b
--- /dev/null
+++ b/server/src/chat-server.ts
@@ -0,0 +1,48 @@
+import type http from 'http';
+import type https from 'https';
+import WebSocket from 'ws';
+
+/**
+ * In message type (from client).
+ */
+export type ClientMessage = { text: string } | { addUser: string } | { removeUser: string };
+/**
+ * Out message type (to client).
+ */
+export type ServerMessage = { text: string } | { users: string[] };
+
+/**
+ * Chat server
+ */
+export default class ChatServer {
+  users: string[] = [];
+
+  /**
+   * Constructs a WebSocket server that will respond to the given path on webServer.
+   */
+  constructor(webServer: http.Server | https.Server, path: string) {
+    const server = new WebSocket.Server({ server: webServer, path: path + '/chat' });
+
+    server.on('connection', (connection, _request) => {
+      connection.on('message', (message) => {
+        const data: ClientMessage = JSON.parse(message.toString());
+        if ('addUser' in data) {
+          this.users.push(data.addUser);
+          const message = JSON.stringify({ users: this.users } as ServerMessage);
+          server.clients.forEach((connection) => connection.send(message));
+        }
+        if ('removeUser' in data) {
+          this.users = this.users.filter((e) => e != data.removeUser);
+          const message = JSON.stringify({ users: this.users } as ServerMessage);
+          server.clients.forEach((connection) => connection.send(message));
+        }
+        if ('text' in data) {
+          // Send the message to all current client connections
+          server.clients.forEach((connection) =>
+            connection.send(JSON.stringify({ text: data.text } as ServerMessage))
+          );
+        }
+      });
+    });
+  }
+}
diff --git a/server/src/server.ts b/server/src/server.ts
index 2d5f8245689dbd61e2d78da8f814259e28bbd741..0069b3cfb075e55dd913953154ca317665c6d3e2 100644
--- a/server/src/server.ts
+++ b/server/src/server.ts
@@ -6,13 +6,13 @@ import app from './app';
 import express from 'express';
 import path from 'path';
 import http from 'http';
-import WhiteboardServer from './whiteboard-server';
+import ChatServer from './chat-server';
 
 // Serve client files
 app.use(express.static(path.join(__dirname, '/../../client/public')));
 
 const webServer = http.createServer(app);
-new WhiteboardServer(webServer, '/api/v1');
+const webSocketServer = new ChatServer(webServer, '/api/v1');
 
 const port = 3000;
 webServer.listen(port, () => {
diff --git a/server/src/whiteboard-server.ts b/server/src/whiteboard-server.ts
deleted file mode 100644
index 0e48757d1b40454445f386714e1553041e002a03..0000000000000000000000000000000000000000
--- a/server/src/whiteboard-server.ts
+++ /dev/null
@@ -1,22 +0,0 @@
-import type http from 'http';
-import type https from 'https';
-import WebSocket from 'ws';
-
-/**
- * Whiteboard server
- */
-export default class WhiteboardServer {
-  /**
-   * Constructs a WebSocket server that will respond to the given path on webServer.
-   */
-  constructor(webServer: http.Server | https.Server, path: string) {
-    const server = new WebSocket.Server({ server: webServer, path: path + '/whiteboard' });
-
-    server.on('connection', (connection, _request) => {
-      connection.on('message', (message) => {
-        // Send the message to all current client connections
-        server.clients.forEach((connection) => connection.send(message.toString()));
-      });
-    });
-  }
-}
diff --git a/server/test/whiteboard-server.test.ts b/server/test/chat-server.test.ts
similarity index 80%
rename from server/test/whiteboard-server.test.ts
rename to server/test/chat-server.test.ts
index 609a6a3e49797df30fe1de69f46629b64ec3483d..4dce340905aa67feac85f0db9546fb40fe195f40 100644
--- a/server/test/whiteboard-server.test.ts
+++ b/server/test/chat-server.test.ts
@@ -1,11 +1,11 @@
 import http from 'http';
 import WebSocket from 'ws';
-import WhiteboardServer from '../src/whiteboard-server';
+import ChatServer from '../src/chat-server';
 
 let webServer: any;
 beforeAll((done) => {
   webServer = http.createServer();
-  new WhiteboardServer(webServer, '/api/v1');
+  new ChatServer(webServer, '/api/v1');
   // Use separate port for testing
   webServer.listen(3001, () => done());
 });
@@ -15,9 +15,9 @@ afterAll((done) => {
   webServer.close(() => done());
 });
 
-describe('WhiteboardServer tests', () => {
+describe('ChatServer tests', () => {
   test('Connection opens successfully', (done) => {
-    const connection = new WebSocket('ws://localhost:3001/api/v1/whiteboard');
+    const connection = new WebSocket('ws://localhost:3001/api/v1/chat');
 
     connection.on('open', () => {
       connection.close();
@@ -29,8 +29,8 @@ describe('WhiteboardServer tests', () => {
     });
   });
 
-  test('WhiteboardServer replies correctly', (done) => {
-    const connection = new WebSocket('ws://localhost:3001/api/v1/whiteboard');
+  test('ChatServer replies correctly', (done) => {
+    const connection = new WebSocket('ws://localhost:3001/api/v1/chat');
 
     connection.on('open', () => connection.send('test'));