diff --git a/conf/daemon/consolecallback b/conf/daemon/consolecallback
new file mode 100755
index 0000000..d7810b9
--- /dev/null
+++ b/conf/daemon/consolecallback
@@ -0,0 +1,100 @@
+#!/usr/bin/env python3
+# consolecallback - provide a persistent console that survives guest reboots
+
+import os
+import logging
+import libvirt
+import tty
+import termios
+import atexit
+from argparse import ArgumentParser
+from typing import Optional  # noqa F401
+
+
+def reset_term() -> None:
+    termios.tcsetattr(0, termios.TCSADRAIN, attrs)
+
+
+def error_handler(unused, error) -> None:
+    # The console stream errors on VM shutdown; we don't care
+    if error[0] == libvirt.VIR_ERR_RPC and error[1] == libvirt.VIR_FROM_STREAMS:
+        return
+    logging.warn(error)
+
+
+class Console(object):
+    def __init__(self, uri: str, uuid: str) -> None:
+        self.uri = uri
+        self.uuid = uuid
+        self.connection = libvirt.open(uri)
+        self.domain = self.connection.lookupByUUIDString(uuid)
+        self.state = self.domain.state(0)
+        self.connection.domainEventRegister(lifecycle_callback, self)
+        self.stream = None  # type: Optional[libvirt.virStream]
+        self.run_console = True
+        self.stdin_watch = -1
+        logging.info("%s initial state %d, reason %d",
+                     self.uuid, self.state[0], self.state[1])
+
+
+def check_console(console: Console) -> bool:
+    if (console.state[0] == libvirt.VIR_DOMAIN_RUNNING or console.state[0] == libvirt.VIR_DOMAIN_PAUSED):
+        if console.stream is None:
+            console.stream = console.connection.newStream(libvirt.VIR_STREAM_NONBLOCK)
+            console.domain.openConsole(None, console.stream, 0)
+            console.stream.eventAddCallback(libvirt.VIR_STREAM_EVENT_READABLE, stream_callback, console)
+    else:
+        if console.stream:
+            console.stream.eventRemoveCallback()
+            console.stream = None
+
+    return console.run_console
+
+
+def stdin_callback(watch: int, fd: int, events: int, console: Console) -> None:
+    readbuf = os.read(fd, 1024)
+    if readbuf.startswith(b""):
+        console.run_console = False
+        return
+    if console.stream:
+        console.stream.send(readbuf)
+
+
+def stream_callback(stream: libvirt.virStream, events: int, console: Console) -> None:
+    try:
+        assert console.stream
+        received_data = console.stream.recv(1024)
+    except Exception:
+        return
+    os.write(0, received_data)
+
+
+def lifecycle_callback(connection: libvirt.virConnect, domain: libvirt.virDomain, event: int, detail: int, console: Console) -> None:
+    console.state = console.domain.state(0)
+    logging.info("%s transitioned to state %d, reason %d",
+                 console.uuid, console.state[0], console.state[1])
+
+
+# main
+parser = ArgumentParser(epilog="Example: %(prog)s 'qemu:///system' '32ad945f-7e78-c33a-e96d-39f25e025d81'")
+parser.add_argument("uri")
+parser.add_argument("uuid")
+args = parser.parse_args()
+
+print("Escape character is ^]")
+logging.basicConfig(filename='msg.log', level=logging.DEBUG)
+logging.info("URI: %s", args.uri)
+logging.info("UUID: %s", args.uuid)
+
+libvirt.virEventRegisterDefaultImpl()
+libvirt.registerErrorHandler(error_handler, None)
+
+atexit.register(reset_term)
+attrs = termios.tcgetattr(0)
+tty.setraw(0)
+
+console = Console(args.uri, args.uuid)
+console.stdin_watch = libvirt.virEventAddHandle(0, libvirt.VIR_EVENT_HANDLE_READABLE, stdin_callback, console)
+
+while check_console(console):
+    libvirt.virEventRunDefaultImpl()
diff --git a/conf/nginx/webvirtcloud.conf b/conf/nginx/webvirtcloud.conf
index d16a814..3ba36ec 100644
--- a/conf/nginx/webvirtcloud.conf
+++ b/conf/nginx/webvirtcloud.conf
@@ -28,8 +28,17 @@ server {
         proxy_set_header Upgrade $http_upgrade;
         proxy_set_header Connection "upgrade";
     }
+    location /socket.io/ {
+        proxy_pass http://wssocketiod;
+        proxy_http_version 1.1;
+        proxy_set_header Upgrade $http_upgrade;
+        proxy_set_header Connection "upgrade";
+    }
 }
 
 upstream wsnovncd {
       server 127.0.0.1:6080;
-}
\ No newline at end of file
+}
+upstream wssocketiod {
+      server 127.0.0.1:6081;
+}
diff --git a/conf/requirements.txt b/conf/requirements.txt
index 6d17fa3..99629fb 100644
--- a/conf/requirements.txt
+++ b/conf/requirements.txt
@@ -13,3 +13,4 @@ rwlock==0.0.7
 websockify==0.10.0
 zipp==3.6.0
 ldap3==2.9.1
+python-socketio==5.7.0
diff --git a/conf/supervisor/webvirtcloud.conf b/conf/supervisor/webvirtcloud.conf
index 692fe8a..a554f0d 100644
--- a/conf/supervisor/webvirtcloud.conf
+++ b/conf/supervisor/webvirtcloud.conf
@@ -12,4 +12,12 @@ directory=/srv/webvirtcloud
 user=www-data
 autostart=true
 autorestart=true
-redirect_stderr=true
\ No newline at end of file
+redirect_stderr=true
+
+[program:socketiod]
+command=/srv/webvirtcloud/venv/bin/python3 /srv/webvirtcloud/console/socketiod -d
+directory=/srv/webvirtcloud
+user=www-data
+autostart=true
+autorestart=true
+redirect_stderr=true
diff --git a/console/socketiod b/console/socketiod
new file mode 100755
index 0000000..a3e2bdc
--- /dev/null
+++ b/console/socketiod
@@ -0,0 +1,195 @@
+#!/usr/bin/env python3
+import os
+import sys
+import logging
+import django
+
+DIR_PATH = os.path.dirname(os.path.abspath(__file__))
+ROOT_PATH = os.path.abspath(os.path.join(DIR_PATH, '..', ''))
+os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'webvirtcloud.settings')
+CERT = DIR_PATH + '/cert.pem'
+
+if ROOT_PATH not in sys.path:
+    sys.path.append(ROOT_PATH)
+
+django.setup()
+
+import re
+import tempfile
+import io
+import socket
+import socketio
+import pty
+import select
+import subprocess
+import struct
+import fcntl
+import termios
+import signal
+import eventlet
+import atexit
+import tty
+import termios
+import libvirt
+
+from six.moves import http_cookies as Cookie
+from webvirtcloud.settings import SOCKETIO_PORT, SOCKETIO_HOST
+from vrtManager.connection import CONN_SSH, CONN_SOCKET
+from optparse import OptionParser
+
+parser = OptionParser()
+
+parser.add_option("-v",
+                  "--verbose",
+                  dest="verbose",
+                  action="store_true",
+                  help="Verbose mode",
+                  default=False)
+
+parser.add_option("-d",
+                  "--debug",
+                  dest="debug",
+                  action="store_true",
+                  help="Debug mode",
+                  default=False)
+
+parser.add_option("-H",
+                  "--host",
+                  dest="host",
+                  action="store",
+                  help="Listen host",
+                  default=SOCKETIO_HOST)
+
+parser.add_option("-p",
+                  "--port",
+                  dest="port",
+                  action="store",
+                  help="Listen port",
+                  default=SOCKETIO_PORT or 6081)
+
+(options, args) = parser.parse_args()
+
+FORMAT = "%(asctime)s - %(name)s - %(levelname)s : %(message)s"
+if options.debug:
+    logging.basicConfig(level=logging.DEBUG, format=FORMAT)
+    options.verbose = True
+elif options.verbose:
+    logging.basicConfig(level=logging.INFO, format=FORMAT)
+else:
+    logging.basicConfig(level=logging.WARNING, format=FORMAT)
+
+async_mode = "eventlet"
+sio = socketio.Server(async_mode=async_mode, cors_allowed_origins="https://vmm.cyborgside.net")
+
+fd = None
+child_pid = None
+
+def get_connection_infos(token):
+    from instances.models import Instance
+    from vrtManager.instance import wvmInstance
+
+    try:
+        temptoken = token.split('-', 1)
+        host = int(temptoken[0])
+        uuid = temptoken[1]
+        instance = Instance.objects.get(compute_id=host, uuid=uuid)
+        conn = wvmInstance(instance.compute.hostname,
+                           instance.compute.login,
+                           instance.compute.password,
+                           instance.compute.type,
+                           instance.name)
+    except Exception as e:
+        logging.error(
+            'Fail to retrieve console connection infos for token %s : %s' % (token, e))
+        raise
+    return (instance, conn)
+
+def set_winsize(fd, row, col, xpix=0, ypix=0):
+    winsize = struct.pack("HHHH", row, col, xpix, ypix)
+    fcntl.ioctl(fd, termios.TIOCSWINSZ, winsize)
+
+
+def read_and_forward_pty_output():
+    global fd
+    max_read_bytes = 1024 * 20
+    while True:
+        sio.sleep(0.01)
+        if fd:
+            timeout_sec = 0
+            (data_ready, _, _) = select.select([fd], [], [], timeout_sec)
+            if data_ready:
+                output = os.read(fd, max_read_bytes).decode()
+                sio.emit("pty_output", {"output": output})
+        else:
+            return
+       
+
+@sio.event
+def resize(sid, message):   
+    global fd
+    if fd:
+        set_winsize(fd, message["rows"], message["cols"])
+
+@sio.event
+def pty_input(sid, message):
+    global fd
+    if fd:
+        os.write(fd, message["input"].encode())
+
+@sio.event
+def disconnect_request(sid):
+    sio.disconnect(sid)
+
+@sio.event
+def connect(sid, environ):
+    global fd
+    global child_pid
+
+    hcookie = environ.get('HTTP_COOKIE')
+    if hcookie:
+        cookie = Cookie.SimpleCookie()
+        for hcookie_part in hcookie.split(';'):
+            hcookie_part = hcookie_part.lstrip()
+            try:
+                cookie.load(hcookie_part)
+            except Cookie.CookieError:
+                logging.warn('Found malformed cookie')
+            else:
+                if 'token' in cookie:
+                    token = cookie['token'].value
+
+    if child_pid:
+        # already started child process, don't start another
+        # write a new line so that when a client refresh the shell prompt is printed
+        fd.write("\n")
+        return
+
+    # create child process attached to a pty we can read from and write to
+    (child_pid, fd) = pty.fork()
+
+    if child_pid == 0:
+        (instance, conn) = get_connection_infos(token)
+        uuid = conn.get_uuid() 
+        uri = conn.wvm.getURI()
+        subprocess.run(["consolecallback", uri, uuid])
+    else:
+        # this is the parent process fork.
+        sio.start_background_task(target=read_and_forward_pty_output)
+
+@sio.event
+def disconnect(sid):
+
+    global fd
+    global child_pid
+
+    # kill pty process
+    os.kill(child_pid,signal.SIGKILL)
+    os.wait()
+
+    # reset the variables
+    fd = None
+    child_pid = None
+
+app = socketio.WSGIApp(sio)
+import eventlet
+eventlet.wsgi.server(eventlet.listen((options.host,int(options.port))), app)
diff --git a/console/templates/console-xterm.html b/console/templates/console-xterm.html
new file mode 100644
index 0000000..21fe8c2
--- /dev/null
+++ b/console/templates/console-xterm.html
@@ -0,0 +1,75 @@
+{% extends "console-base.html" %}
+{% load i18n %}
+{% load static %}
+
+{% block head %}
+<title>WebVirtCloud - XTerm</title>
+<link rel="stylesheet" href="https://unpkg.com/xterm@3.6.0/dist/xterm.css" />
+<script src="https://unpkg.com/xterm@3.6.0/dist/xterm.js"></script>
+<script src="https://unpkg.com/xterm@3.6.0/dist/addons/fit/fit.js"></script>
+<script src="https://unpkg.com/xterm@3.6.0/dist/addons/fullscreen/fullscreen.js"></script>
+
+<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.5.0/socket.io.js"></script>
+
+{% endblock %}
+{% block content %}
+<div style="background: white; padding-bottom: 5px;">
+    <span style="font-size: small;">Status: <span style="font-size: small;" id="status">connecting...</span></span>
+    <button id="button"; type="button"; onclick="myFunction()";>Connect</button>
+</div>
+
+<div style="width: 100%; height:100%;" id="terminal"></div>
+<script crossorigin="anonymous">
+	Terminal.applyAddon(fit)
+
+        var socket = io.connect({transports: ["websocket", "polling"]});
+
+        const status = document.getElementById("status")
+        const button = document.getElementById("button")
+
+        var term = new Terminal({
+            cursorBlink: true,
+        });
+
+        term.open(document.getElementById('terminal'));
+
+        term.on('key', (key, ev) => {
+            console.log("pressed key", key)
+            socket.emit("pty_input", {"input": key})
+        });
+
+        socket.on("pty_output", function(output){
+            console.log(output["output"])
+            term.write(output["output"])
+        })
+
+        socket.on("connect", () => {
+        status.innerHTML = '<span style="background-color: lightgreen;">connected</span>'
+        button.innerHTML = 'Disconnect'
+        })
+
+        socket.on("disconnect", () => {
+        status.innerHTML =  '<span style="background-color: #ff8383;">disconnected</span>'
+        button.innerHTML = 'Connect'
+
+        })
+
+        function myFunction(){
+            if (button.innerHTML =='Connect'){
+               location.reload();
+            }
+
+            else if (button.innerHTML == "Disconnect"){
+                socket.emit("disconnect_request")
+            }
+        }
+
+        function resize(){
+            term.fit()
+            socket.emit("resize", {"cols": term.cols, "rows": term.rows})
+        }
+
+        window.onresize = resize
+        window.onload = resize
+</script>
+{% endblock %}
diff --git a/console/views.py b/console/views.py
index 81f5465..a83a0bd 100644
--- a/console/views.py
+++ b/console/views.py
@@ -79,6 +79,8 @@ def console(request):
     if console_type == "vnc" or console_type == "spice":
         console_page = "console-" + console_type + "-" + view_type + ".html"
         response = render(request, console_page, locals())
+    elif console_type == "pty":
+        response = render(request, "console-xterm.html", locals())
     else:
         if console_type is None:
             console_error = _("Fail to get console. Please check the console configuration of your VM.")
diff --git a/vrtManager/instance.py b/vrtManager/instance.py
index 63fd835..8212aab 100644
--- a/vrtManager/instance.py
+++ b/vrtManager/instance.py
@@ -1019,6 +1019,8 @@ class wvmInstance(wvmConnect):
 
     def get_console_type(self):
         console_type = util.get_xml_path(self._XMLDesc(0), "/domain/devices/graphics/@type")
+        if console_type is None:
+            console_type = util.get_xml_path(self._XMLDesc(0), "/domain/devices/console/@type")
         return console_type
 
     def set_console_type(self, console_type):
diff --git a/webvirtcloud.sh b/webvirtcloud.sh
index d9aa0c7..36286b3 100755
--- a/webvirtcloud.sh
+++ b/webvirtcloud.sh
@@ -254,6 +254,7 @@ install_webvirtcloud () {
   pip3 install -r conf/requirements.txt -q
 
   
+  cp "$APP_PATH/conf/daemon/consolecallback" "$APP_PATH/venv/bin/consolecallback"
   chown -R "$nginx_group":"$nginx_group" "$APP_PATH"
 
   
diff --git a/webvirtcloud/settings.py.template b/webvirtcloud/settings.py.template
index ffaacf3..6818262 100644
--- a/webvirtcloud/settings.py.template
+++ b/webvirtcloud/settings.py.template
@@ -198,6 +198,9 @@ WS_PUBLIC_PATH = "/novncd/"
 # Websock Certificate for SSL
 WS_CERT = None
 
+SOCKETIO_PORT = 6081
+SOCKETIO_HOST = '0.0.0.0'
+
 # List of console listen addresses
 QEMU_CONSOLE_LISTEN_ADDRESSES = (
     ("127.0.0.1", "Localhost"),