Repository: JarbasHiveMind/HiveMind-core (language: python)

Issue:
handshake
integrate with https://github.com/JarbasHiveMind/poorman_handshake

closes #13 
Rwrite
big refactor and cleanup in progress

needs https://github.com/JarbasHiveMind/hivemind_websocket_client/pull/15


Discussion / hints:

Fix patch:
diff --git a/examples/add_keys.py b/examples/add_keys.py
index 04a66d1..4b8be7c 100644
--- a/examples/add_keys.py
+++ b/examples/add_keys.py
@@ -1,4 +1,4 @@
-from jarbas_hive_mind.database import ClientDatabase
+from hivemind_core.database import ClientDatabase
 
 name = "JarbasCliTerminal"
 key = "RESISTENCEisFUTILE"
diff --git a/examples/fakecroft.py b/examples/fakecroft.py
new file mode 100644
index 0000000..57d9a53
--- /dev/null
+++ b/examples/fakecroft.py
@@ -0,0 +1,77 @@
+from hivemind_bus_client.message import HiveMessage, HiveMessageType
+from ovos_bus_client.message import Message
+from ovos_utils.log import LOG
+from ovos_utils.messagebus import FakeBus
+from hivemind_listener.service import MessageBusEventHandler
+from hivemind_listener.protocol import HiveMindListenerProtocol, \
+    HiveMindListenerInternalProtocol, HiveMindClientConnection
+
+
+class HiveMindFakeCroftProtocol(HiveMindListenerProtocol):
+    """ Fake ovos-core instance, not actually connected to a messagebus"""
+    peer: str = "fakecroft:0.0.0.0"
+
+    def bind(self, websocket, bus=None):
+        websocket.protocol = self
+        if bus is None:
+            bus = FakeBus()
+        self.internal_protocol = HiveMindListenerInternalProtocol(bus)
+
+    def handle_incoming_mycroft(self, message: Message, client: HiveMindClientConnection):
+        """
+        message (Message): mycroft bus message object
+        """
+        super().handle_inject_mycroft_msg(message, client)
+        answer = "mycroft is dead! long live mycroft!"
+
+        payload = HiveMessage(HiveMessageType.BUS,
+                              message.reply("speak", {"utterance": answer}))
+        client.send(payload)
+
+
+def on_ready():
+    LOG.info('FakeCroft started!')
+
+
+def on_error(e='Unknown'):
+    LOG.info('FakeCroft failed to start ({})'.format(repr(e)))
+
+
+def on_stopping():
+    LOG.info('FakeCroft is shutting down...')
+
+
+def main(ready_hook=on_ready, error_hook=on_error, stopping_hook=on_stopping):
+    from ovos_utils import create_daemon, wait_for_exit_signal
+    from tornado import web, ioloop
+    from ovos_config import Configuration
+
+    LOG.info('Starting FakeCroft...')
+
+    try:
+        websocket_configs = Configuration()['websocket']
+    except KeyError as ke:
+        LOG.error('No websocket configs found ({})'.format(repr(ke)))
+        raise
+
+    host = websocket_configs.get('host')
+    port = websocket_configs.get('port')
+    route = websocket_configs.get('route')
+    port = 5678
+    route = "/"
+
+    # TODO - protocol from config / argparse
+    protocol = HiveMindFakeCroftProtocol()
+    protocol.bind(MessageBusEventHandler)
+
+    routes = [(route, MessageBusEventHandler)]
+    application = web.Application(routes)
+    application.listen(port, host)
+    create_daemon(ioloop.IOLoop.instance().start)
+    ready_hook()
+    wait_for_exit_signal()
+    stopping_hook()
+
+
+if __name__ == "__main__":
+    main()
diff --git a/examples/mycroft_discover.py b/examples/mycroft_discover.py
deleted file mode 100644
index 7b86d70..0000000
--- a/examples/mycroft_discover.py
+++ /dev/null
@@ -1,15 +0,0 @@
-from jarbas_hive_mind.discovery import LocalDiscovery
-from jarbas_hive_mind import HiveMindConnection
-from ovos_utils.log import LOG
-
-
-def discover_hivemind(key="dummy_key",
-                      name="HiveDummyNode"):
-    headers = HiveMindConnection.get_headers(name, key)
-    discovery = LocalDiscovery()
-    discovery.search_and_connect(headers=headers)
-
-
-if __name__ == '__main__':
-    # TODO arg parse
-    discover_hivemind()
diff --git a/examples/mycroft_master.py b/examples/mycroft_master.py
deleted file mode 100644
index c9f8d5e..0000000
--- a/examples/mycroft_master.py
+++ /dev/null
@@ -1,44 +0,0 @@
-from jarbas_hive_mind import get_listener
-from jarbas_hive_mind.configuration import CONFIGURATION
-
-
-def start_mind(config=None, bus=None):
-
-    config = config or CONFIGURATION
-
-    # listen
-    listener = get_listener(bus=bus)
-
-    # use http
-    # config["ssl"]["use_ssl"] = False
-
-    # read port and ssl settings
-    listener.load_config(config)
-
-    listener.listen()
-
-
-if __name__ == '__main__':
-    # TODO argparse
-    start_mind()
-
-    # that's it, now external applications can connect to the HiveMind
-
-    # use configuration to set things like
-    #  - blacklisted/whitelisted ips
-    #  - blacklisted/whitelisted message_types
-    #  - blacklisted/whitelisted intents - Coming soon
-    #  - blacklisted/whitelisted skills  - Coming soon
-
-    # you can send messages to the mycroft bus to send/broadcast to clients
-    # 'Message(hive.client.broadcast',
-    #           {"payload":
-    #               {"msg_type": "speak",
-    #               "data": {"utterance": "Connected to the HiveMind"}
-    #           })
-
-    # or you can listen to hive mind events
-    # "hive.client.connection.error"
-    # "hive.client.connect"
-    # "hive.client.disconnect"
-    # "hive.client.send.error"
diff --git a/examples/mycroft_slave.py b/examples/mycroft_slave.py
deleted file mode 100644
index 5102c76..0000000
--- a/examples/mycroft_slave.py
+++ /dev/null
@@ -1,40 +0,0 @@
-from jarbas_hive_mind import get_connection
-from jarbas_hive_mind.slave import HiveMindSlave
-
-
-def connect_to_hivemind(host="wss://0.0.0.0", port=5678,
-                        name="unsafe",
-                        key="unsafe", crypto_key=None,
-                        useragent="JarbasDroneV0.2",
-                        bus=None):
-    con = get_connection(host, port)
-
-    component = HiveMindSlave(bus=bus, headers=con.get_headers(name, key),
-                              crypto_key=crypto_key, useragent=useragent)
-                              #,auto_reconnect=True)
-
-    # will check url for ssl
-    con.connect(component)
-
-
-if __name__ == '__main__':
-    # TODO arg parse
-    connect_to_hivemind()
-
-    # That's it, now the hive mind can send messages to the mycroft bus
-
-    # you can react to hive mind events
-    # NOTE the payload from hive mind is also sent directly to the messagebus
-    # "hive.mind.connected"
-    # "hive.mind.websocket.open"
-    # "hive.mind.connection.closed"
-    # "hive.mind.message.received"
-    # "hive.mind.message.sent"
-
-    # you can send messages to the mycroft bus to send to the HiveMind
-    # for example to interact with a skill
-    # 'Message("hive.mind.message.send",
-    #           {"payload":
-    #               {"msg_type": "sensor.data",
-    #               "data": {"status": "off"}
-    #           })
diff --git a/hivemind_core/__init__.py b/hivemind_core/__init__.py
new file mode 100644
index 0000000..303109e
--- /dev/null
+++ b/hivemind_core/__init__.py
@@ -0,0 +1,16 @@
+# Copyright 2017 Mycroft AI Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+from ovos_utils.log import init_service_logger
+
+init_service_logger("HiveMind")
diff --git a/hivemind_core/__main__.py b/hivemind_core/__main__.py
new file mode 100644
index 0000000..622e5bd
--- /dev/null
+++ b/hivemind_core/__main__.py
@@ -0,0 +1,10 @@
+from hivemind_core.service import HiveMindService
+
+
+def main():
+    service = HiveMindService()
+    service.start()
+
+
+if __name__ == "__main__":
+    main()
diff --git a/jarbas_hive_mind/database/__init__.py b/hivemind_core/database.py
similarity index 94%
rename from jarbas_hive_mind/database/__init__.py
rename to hivemind_core/database.py
index 69653f5..04279d6 100644
--- a/jarbas_hive_mind/database/__init__.py
+++ b/hivemind_core/database.py
@@ -1,9 +1,9 @@
 import json
-from jarbas_hive_mind.configuration import CONFIGURATION
-from json_database import JsonDatabase
-from ovos_utils.log import LOG
 from functools import wraps
 
+from json_database import JsonDatabaseXDG
+from ovos_utils.log import LOG
+
 
 def cast_to_client_obj():
     valid_kwargs = ("client_id", "api_key", "name",
@@ -74,9 +74,9 @@ def __repr__(self):
         return str(self.__dict__)
 
 
-class ClientDatabase(JsonDatabase):
-    def __init__(self, path=CONFIGURATION["database"]):
-        super().__init__("clients", path)
+class ClientDatabase(JsonDatabaseXDG):
+    def __init__(self):
+        super().__init__("clients", subfolder="HiveMind")
 
     def update_timestamp(self, key, timestamp):
         user = self.get_client_by_api_key(key)
@@ -119,6 +119,12 @@ def get_crypto_key(self, api_key):
             return None
         return user["crypto_key"]
 
+    def get_password(self, api_key):
+        user = self.get_client_by_api_key(api_key)
+        if not user:
+            return None
+        return user["password"]
+
     def change_name(self, new_name, key):
         user = self.get_client_by_api_key(key)
         if not user:
diff --git a/hivemind_core/protocol.py b/hivemind_core/protocol.py
new file mode 100644
index 0000000..612d295
--- /dev/null
+++ b/hivemind_core/protocol.py
@@ -0,0 +1,479 @@
+import asyncio
+import json
+from dataclasses import dataclass, field
+from enum import Enum, IntEnum
+from typing import Optional, List
+
+from ovos_utils.log import LOG
+from poorman_handshake import HandShake, PasswordHandShake
+from tornado.platform.asyncio import AsyncIOMainLoop
+from tornado.websocket import WebSocketHandler
+
+from hivemind_bus_client.message import HiveMessage, HiveMessageType
+from hivemind_bus_client.util import decrypt_from_json, encrypt_as_json
+from ovos_bus_client import MessageBusClient
+from ovos_bus_client.message import Message
+from ovos_bus_client.session import Session
+
+
+class ProtocolVersion(IntEnum):
+    ZERO = 0  # json only, no handshake, no binary
+    ONE = 1  # handshake https://github.com/JarbasHiveMind/HiveMind-core/pull/29
+    TWO = 2  # binary https://github.com/JarbasHiveMind/hivemind_websocket_client/pull/4
+
+
+class HiveMindNodeType(str, Enum):
+    CANDIDATE_NODE = "candidate"  # potential node, if it manages to connect...
+    NODE = "node"  # anything connected to the hivemind is a "node"
+    MIND = "mind"  # listening for connections and providing mycroft-core
+    # (mycroft itself may be running in a different "mind")
+    FAKECROFT = "fakecroft"  # a mind, that pretends to be running mycroft
+    # but is actually using a different stack
+    # (mycroft itself may be running in a different "mind")
+    SLAVE = "slave"  # node that can be partially controlled by a "mind"
+    TERMINAL = "terminal"  # user facing endpoint that connects to some Mind
+    # and does not itself accept connections
+    BRIDGE = "bridge"  # connects some external service to the hive
+
+    # RESERVED
+    HIVE = "hive"  # a collection of nodes
+    MASTER_MIND = "master"  # the top level node, not connected to anything
+    # but receiving connections
+
+
+@dataclass
+class HiveMindClientConnection:
+    """ represents a connection to the hivemind listener """
+    key: str
+    ip: str
+    loop: AsyncIOMainLoop
+    sess: Session  # unique session per client
+    name: str = "AnonClient"
+    node_type: HiveMindNodeType = HiveMindNodeType.CANDIDATE_NODE
+    handshake: Optional[HandShake] = None
+    pswd_handshake: Optional[PasswordHandShake] = None
+    socket: Optional[WebSocketHandler] = None
+    crypto_key: Optional[str] = None
+    blacklist: List[str] = field(default_factory=list) # list of ovos message_type to never be sent to this client
+
+    @property
+    def peer(self):
+        # friendly id that ovos components can use to refer to this connection
+        # this is how ovos refers to connected nodes in message.context
+        return f"{self.name}:{self.ip}::{self.sess.session_id}"
+
+    def send(self, message: HiveMessage):
+        LOG.info(f"sending to {self.peer}: {message}")
+        payload = message.serialize()  # json string
+        if self.crypto_key and message.msg_type not in [HiveMessageType.HANDSHAKE,
+                                                    HiveMessageType.HELLO]:
+            payload = encrypt_as_json(self.crypto_key, payload)  # still a json string
+            LOG.info(f"encrypted payload: {len(payload)}")
+        else:
+            LOG.warning(f"sent unencrypted!")
+
+        self.loop.install()
+        self.socket.write_message(payload)
+
+    def decode(self, payload: str):
+        if self.crypto_key:
+            if "ciphertext" in payload:
+                payload = decrypt_from_json(self.crypto_key, payload)
+            else:
+                LOG.warning("Message was unencrypted")
+                # TODO - some error if crypto is required
+        else:
+            pass  # TODO - reject anything except HELLO and HANDSHAKE
+        if isinstance(payload, str):
+            payload = json.loads(payload)
+        return HiveMessage(**payload)
+
+    def authorize(self, message: Message):
+        """ parse the message being injected into ovos-core bus
+        if this client is not authorized to inject it return False"""
+        if message.msg_type in self.blacklist:
+            return False
+
+        # TODO check intent / skill that will trigger
+        # we want for example to be able to block shutdown/reboot intents to random chat users
+        return True
+
+
+@dataclass()
+class HiveMindListenerInternalProtocol:
+    """ this class handles all interactions between a hivemind listener and a ovos-core messagebus"""
+    bus: MessageBusClient
+
+    def register_bus_handlers(self):
+        LOG.info("registering internal mycroft bus handlers")
+        self.bus.on("hive.send.downstream", self.handle_send)
+        self.bus.on("message", self.handle_internal_mycroft)  # catch all
+
+    @property
+    def clients(self):
+        return HiveMindListenerProtocol.clients
+
+    # mycroft handlers  - from master -> slave
+    def handle_send(self, message: Message):
+        """ ovos wants to send a HiveMessage
+
+        a device can be both a master and a slave, downstream messages are handled here
+
+        HiveMindSlaveInternalProtocol will handle requests meant to go upstream
+        """
+
+        payload = message.data.get("payload")
+        peer = message.data.get("peer")
+        msg_type = message.data["msg_type"]
+
+        hmessage = HiveMessage(msg_type,
+                               payload=payload,
+                               target_peers=[peer])
+
+        if msg_type in [HiveMessageType.PROPAGATE, HiveMessageType.BROADCAST]:
+            # this message is meant to be sent to all slave nodes
+            for peer in self.clients:
+                self.clients[peer].send(hmessage)
+        elif msg_type == HiveMessageType.ESCALATE:
+            # only slaves can escalate, ignore silently
+            #   if this device is also a slave to something,
+            #   HiveMindSlaveInternalProtocol will handle the request
+            pass
+
+        # NOT a protocol specific message, send directly to requested peer
+        # ovos component is asking explicitly to send a message to a peer
+        elif peer:
+            if peer in self.clients:
+                # send message to client
+                client = self.clients[peer]
+                client.send(hmessage)
+            else:
+                LOG.error("That client is not connected")
+                self.bus.emit(message.forward(
+                    "hive.client.send.error",
+                    {"error": "That client is not connected", "peer": peer}))
+
+    def handle_internal_mycroft(self, message: str):
+        """ forward internal messages to clients if they are the target
+        here is where the client isolation happens,
+        clients only get responses to their own messages"""
+
+        # "message" event is a special case in ovos-bus-client that is not deserialized
+        message = Message.deserialize(message)
+        target_peers = message.context.get("destination") or []
+        if not isinstance(target_peers, list):
+            target_peers = [target_peers]
+
+        new_sess = Session.from_message(message)
+        for peer, client in self.clients.items():
+            # ovos-core decides the contents of the Session,
+            # let's sync any internal changes
+            if new_sess.session_id == client.sess.session_id:
+                LOG.info(f"syncing session from ovos with {peer}")
+                client.sess = Session.from_message(message)
+
+            if peer in target_peers:
+                # forward internal messages to clients if they are the target
+                LOG.info(f"{message.msg_type} - destination: {peer}")
+                message.context["source"] = "hive"
+                msg = HiveMessage(HiveMessageType.BUS,
+                                  source_peer=peer,
+                                  target_peers=target_peers,
+                                  payload=message)
+                client.send(msg)
+
+
+@dataclass()
+class HiveMindListenerProtocol:
+    loop: AsyncIOMainLoop
+    clients = {}
+    internal_protocol: HiveMindListenerInternalProtocol = None
+    peer: str = "master:0.0.0.0"
+
+    require_crypto: bool = True  # throw error if crypto key not available
+    handshake_enabled: bool = True  # generate a key per session if not pre-shared
+
+    # below are optional callbacks to handle payloads
+    # receives the payload + HiveMindClient that sent it
+    escalate_callback = None  # slave asked to escalate payload
+    illegal_callback = None  # slave asked to broadcast payload (illegal action)
+    propagate_callback = None  # slave asked to propagate payload
+    mycroft_bus_callback = None  # slave asked to inject payload into mycroft bus
+    shared_bus_callback = None  # passive sharing of slave device bus (info)
+
+    def bind(self, websocket, bus=None):
+        websocket.protocol = self
+        if bus is None:
+            bus = MessageBusClient()
+            bus.run_in_thread()
+            bus.connected_event.wait()
+        self.internal_protocol = HiveMindListenerInternalProtocol(bus)
+        self.internal_protocol.register_bus_handlers()
+
+    def handle_new_client(self, client: HiveMindClientConnection):
+        LOG.info(f"new client: {client.peer}")
+        self.clients[client.peer] = client
+        message = Message("hive.client.connect",
+                          {"ip": client.ip, "session_id": client.sess.session_id},
+                          {"source": client.peer})
+        self.internal_protocol.bus.emit(message)
+
+        min_version = ProtocolVersion.ONE if client.crypto_key is None and self.require_crypto \
+            else ProtocolVersion.ZERO
+        max_version = ProtocolVersion.ONE
+
+        msg = HiveMessage(HiveMessageType.HELLO,
+                          payload={"pubkey": client.handshake.pubkey, # allows any node to verify messages are signed with this
+                                   "peer": client.peer,  # this identifies the connected client in ovos message.context
+                                   "node_id": self.peer})
+        LOG.info(f"saying HELLO to: {client.peer}")
+        client.send(msg)
+
+        needs_handshake = not client.crypto_key and self.handshake_enabled
+
+        # request client to start handshake (by sending client pubkey)
+        payload = {
+            "handshake": needs_handshake,  # tell the client it must do a handshake or connection will be dropped
+            "min_protocol_version": min_version,
+            "max_protocol_version": max_version,
+            "preshared_key": client.crypto_key is not None,  # do we have a pre-shared key (V0 proto)
+            "password": client.pswd_handshake is not None,  # is password available (V1 proto, replaces pre-shared key)
+            "crypto_required": self.require_crypto  # do we allow unencrypted payloads
+        }
+        msg = HiveMessage(HiveMessageType.HANDSHAKE, payload)
+        LOG.info(f"starting {client.peer} HANDSHAKE: {payload}")
+        client.send(msg)
+        # if client is in protocol V1 -> self.handle_handshake_message
+        # clients can rotate their pubkey or session_key by sending a new handshake
+
+    def handle_client_disconnected(self, client: HiveMindClientConnection):
+        if client.peer in self.clients:
+            self.clients.pop(client.peer)
+        client.socket.close()
+        message = Message("hive.client.disconnect",
+                          {"ip": client.ip},
+                          {"source": client.peer, "session": client.sess.serialize()})
+        self.internal_protocol.bus.emit(message)
+
+    def handle_invalid_key_connected(self, client: HiveMindClientConnection):
+        LOG.error("Client provided an invalid api key")
+        message = Message("hive.client.connection.error",
+                          {"error": "invalid api key", "peer": client.peer},
+                          {"source": client.peer})
+        self.internal_protocol.bus.emit(message)
+
+    def handle_invalid_protocol_version(self, client: HiveMindClientConnection):
+        LOG.error("Client does not satisfy protocol requirements")
+        message = Message("hive.client.connection.error",
+                          {"error": "protocol error", "peer": client.peer},
+                          {"source": client.peer})
+        self.internal_protocol.bus.emit(message)
+
+    def handle_message(self, message: HiveMessage, client: HiveMindClientConnection):
+        """
+        message (HiveMessage): HiveMind message object
+
+        Process message from client, decide what to do internally here
+        """
+        LOG.info(f"message: {message}")
+        # update internal peer ID
+        message.update_source_peer(client.peer)
+
+        message.update_hop_data()
+
+        if message.msg_type == HiveMessageType.HANDSHAKE:
+            self.handle_handshake_message(message, client)
+
+        # mycroft Message handlers
+        elif message.msg_type == HiveMessageType.BUS:
+            self.handle_bus_message(message, client)
+        elif message.msg_type == HiveMessageType.SHARED_BUS:
+            self.handle_client_bus(message.payload, client)
+
+        # HiveMessage handlers
+        elif message.msg_type == HiveMessageType.PROPAGATE:
+            self.handle_propagate_message(message, client)
+        elif message.msg_type == HiveMessageType.BROADCAST:
+            self.handle_broadcast_message(message, client)
+        elif message.msg_type == HiveMessageType.ESCALATE:
+            self.handle_escalate_message(message, client)
+        else:
+            self.handle_unknown_message(message, client)
+
+    # HiveMind protocol messages -  from slave -> master
+    def handle_unknown_message(self, message: HiveMessage, client: HiveMindClientConnection):
+        """ message handler for non default message types, subclasses can
+        handle their own types here
+
+        message (HiveMessage): HiveMind message object
+        """
+
+    def handle_handshake_message(self, message: HiveMessage,
+                                 client: HiveMindClientConnection):
+        LOG.info("handshake received, generating session key")
+        payload = message.payload
+        if "pubkey" in payload and client.handshake is not None:
+            pub = payload.pop("pubkey")
+            payload["envelope"] = client.handshake.generate_handshake(pub)
+            client.crypto_key = client.handshake.secret  # start using new key
+
+            # client side
+            # LOG.info("Received encryption key")
+            # pub = "pubkey from HELLO message"
+            # if pub:  # validate server from known trusted public key
+            #   self.handshake.receive_and_verify(payload["envelope"], pub)
+            # else:  # implicitly trust server
+            #   self.handshake.receive_handshake(payload["envelope"], pub)
+            # self.crypto_key = self.handshake.secret
+        elif client.pswd_handshake is not None and "envelope" in payload:
+            # while the access key is transmitted, the password never is
+            envelope = payload["envelope"]
+
+            payload["envelope"] = client.pswd_handshake.generate_handshake()
+
+            client.pswd_handshake.receive_handshake(envelope)
+           # if not client.pswd_handshake.receive_and_verify(envelope):
+           #     # TODO - different handles for invalid access key / invalid password
+           #     self.handle_invalid_key_connected(client)
+           #     client.socket.close()
+        #    return
+
+
+            # key is derived safely from password in both sides
+            # the handshake is validating both ends have the same password
+            # the key is never actually transmitted
+            client.crypto_key = client.pswd_handshake.secret
+
+            # client side
+            # LOG.info("Received password envelope")
+            # self.pswd_handshake.receive_and_verify(payload["envelope"])
+            # self.crypto_key = self.pswd_handshake.secret
+        else:
+            # TODO - invalid handshake handler
+            client.socket.close()
+            return
+
+        msg = HiveMessage(HiveMessageType.HANDSHAKE, payload)
+        client.send(msg)  # client can recreate crypto_key on his side now
+
+    def handle_binary_message(self, message: bytes, client: HiveMindClientConnection):
+        pass  # TODO -  binary https://github.com/JarbasHiveMind/hivemind_websocket_client/pull/4
+
+    def handle_bus_message(self, message: HiveMessage,
+                           client: HiveMindClientConnection):
+        self.handle_inject_mycroft_msg(message.payload, client)
+        if self.mycroft_bus_callback:
+            self.mycroft_bus_callback(message.payload)
+
+    def handle_broadcast_message(self, message: HiveMessage, client: HiveMindClientConnection):
+        """
+        message (HiveMessage): HiveMind message object
+        """
+        # Slaves are not allowed to broadcast, by definition broadcast goes
+        # downstream only, use propagate instead
+        LOG.warning("Received broadcast message from downstream, illegal action")
+        if self.illegal_callback:
+            self.illegal_callback(message.payload)
+        # TODO kick client for misbehaviour so it stops doing that?
+
+    def _unpack_message(self, message: HiveMessage, client: HiveMindClientConnection):
+        # propagate message to other peers
+        pload = message.payload
+        # keep info about which nodes this message has been to
+        pload.replace_route(message.route)
+        pload.update_source_peer(self.peer)
+        pload.remove_target_peer(client.peer)
+        return pload
+
+    def handle_propagate_message(self, message: HiveMessage,
+                                 client: HiveMindClientConnection):
+        """
+        message (HiveMessage): HiveMind message object
+        """
+        LOG.debug("ROUTE: " + str(message.route))
+        LOG.debug("PAYLOAD_TYPE: " + message.payload.msg_type)
+        LOG.debug("PAYLOAD: " + str(message.payload.payload))
+
+        payload = self._unpack_message(message, client)
+
+        if self.propagate_callback:
+            self.propagate_callback(payload)
+
+        # propagate message to other peers
+        for peer in payload.target_peers:
+            if peer in self.clients:
+                self.clients[peer].send(payload)
+
+        # send to other masters
+        message = Message("hive.send.upstream", payload,
+                          {"destination": "hive",
+                           "source": self.peer,
+                           "session": client.sess.serialize()})
+        self.internal_protocol.bus.emit(message)
+
+    def handle_escalate_message(self, message: HiveMessage,
+                                client: HiveMindClientConnection):
+        """
+        message (HiveMessage): HiveMind message object
+        """
+        LOG.info("Received escalate message from: " + client.peer)
+        LOG.debug("ROUTE: " + str(message.route))
+        LOG.debug("PAYLOAD_TYPE: " + message.payload.msg_type)
+        LOG.debug("PAYLOAD: " + str(message.payload.payload))
+
+        # unpack message
+        payload = self._unpack_message(message, client)
+
+        if self.escalate_callback:
+            self.escalate_callback(payload)
+
+        # send to other masters
+        message = Message("hive.send.upstream", payload,
+                          {"destination": "hive",
+                           "source": self.peer,
+                           "session": client.sess.serialize()})
+        self.internal_protocol.bus.emit(message)
+
+    # HiveMind mycroft bus messages -  from slave -> master
+    def update_slave_session(self, message: Message, client: HiveMindClientConnection):
+        """ slave injected a message, master decides what the session is unconditionally (active skills etc)
+        handle special message that influence session per client and update HM session as needed here
+        """
+        message.context["session"] = client.sess.serialize()
+        return message
+
+    def handle_inject_mycroft_msg(self, message: Message, client: HiveMindClientConnection):
+        """
+        message (Message): mycroft bus message object
+        """
+        # A Slave wants to inject a message in internal mycroft bus
+        # You are a Master, authorize bus message
+
+        # messages/skills/intents per user
+        if not client.authorize(message):
+            LOG.warning(client.peer + " sent an unauthorized bus message")
+            return
+
+        # ensure client specific session data is injected in query to ovos
+        message.context["session"] = client.sess.serialize()
+        message.context["destination"] = "skills"  # ensure not treated as a broadcast
+
+        # send client message to internal mycroft bus
+        LOG.info(f"Forwarding message to mycroft bus from client: {client.peer}")
+        message.context["peer"] = message.context["source"] = client.peer
+        message.context["source"] = client.peer
+
+        # validate slave session
+        message = self.update_slave_session(message, client)
+
+        self.internal_protocol.bus.emit(message)
+
+        if self.mycroft_bus_callback:
+            self.mycroft_bus_callback(message)
+
+    def handle_client_bus(self, message: Message, client: HiveMindClientConnection):
+        # this message is going inside the client bus
+        # take any metrics you need
+        LOG.info("Monitoring bus from client: " + client.peer)
+        if self.shared_bus_callback:
+            self.shared_bus_callback(message)
diff --git a/hivemind_core/scripts.py b/hivemind_core/scripts.py
new file mode 100644
index 0000000..7285fb1
--- /dev/null
+++ b/hivemind_core/scripts.py
@@ -0,0 +1,119 @@
+import os
+
+import click
+from rich.console import Console
+from rich.table import Table
+from ovos_config import Configuration
+from hivemind_core.database import ClientDatabase
+from ovos_utils.xdg_utils import xdg_data_home
+
+
+@click.group()
+def hmcore_cmds():
+    pass
+
+
+@hmcore_cmds.command(help="add credentials for a client", name="add-client")
+@click.argument("name", required=False)
+@click.argument("access_key", required=False)
+@click.argument("password",  required=False)
+@click.argument("crypto_key",  required=False)
+def add_client(name, access_key, password, crypto_key):
+    key = crypto_key
+    if key:
+        print("WARNING: crypto key is deprecated, use password instead if your client supports it")
+        print("WARNING: for security the encryption key should be randomly generated\n"
+              "Defining your own key is discouraged")
+        if len(key) != 16:
+            print("Encryption key needs to be exactly 16 characters!")
+            raise ValueError
+    else:
+        key = os.urandom(8).hex()
+
+    password = password or os.urandom(16).hex()
+
+    access_key = access_key or os.urandom(16).hex()
+    with ClientDatabase() as db:
+        name = name or f"HiveMind-Node-{db.total_clients()}"
+        db.add_client(name, access_key, crypto_key=key, password=password)
+
+        # verify
+        user = db.get_client_by_api_key(access_key)
+        node_id = db.get_item_id(user)
+
+        print("Credentials added to database!\n")
+        print("Node ID:", node_id)
+        print("Friendly Name:", name)
+        print("Access Key:", access_key)
+        print("Password:", password)
+        print("Encryption Key:", key)
+
+        print("WARNING: Encryption Key is deprecated, only use if your client does not support password")
+
+
+@hmcore_cmds.command(help="remove credentials for a client", name="delete-client")
+@click.argument("node_id", required=True)
+def delete_client(node_id):
+    with ClientDatabase() as db:
+        for x in db:
+            if x["client_id"] == int(node_id):
+                item_id = db.get_item_id(x)
+                db.update_item(item_id, dict(client_id=-1, api_key="revoked"))
+                print(f"Revoked credentials!\n")
+                print("Node ID:", x["client_id"])
+                print("Friendly Name:", x["name"])
+                print("Access Key:", x["api_key"])
+                print("Password:", x["password"])
+                print("Encryption Key:", x["crypto_key"])
+                break
+        else:
+            print("Invalid Node ID!")
+
+
+@hmcore_cmds.command(help="list clients and credentials", name="list-clients")
+def list_clients():
+    console = Console()
+    table = Table(title="HiveMind Credentials:")
+    table.add_column("ID", justify="center")
+    table.add_column("Name", justify="center")
+    table.add_column("Access Key", justify="center")
+    table.add_column("Passwordy", justify="center")
+    table.add_column("Crypto Key", justify="center")
+
+    with ClientDatabase() as db:
+        for x in db:
+            if x["client_id"] != -1:
+                table.add_row(str(x["client_id"]), x["name"], x["api_key"], x["password"], x["crypto_key"])
+
+    console.print(table)
+
+
+@hmcore_cmds.command(help="start listening for HiveMind connections", name="listen")
+@click.option("--host", help="HiveMind host", type=str,
+              default=Configuration().get('websocket', {}).get("host", "0.0.0.0"))
+@click.option("--port", help="HiveMind port number", type=int,
+              default=Configuration().get('websocket', {}).get("port", 5678))
+@click.option("--ssl", help="use wss://", type=bool,
+              default=Configuration().get('websocket', {}).get("ssl", False))
+@click.option("--cert_dir", help="HiveMind SSL certificate directory", type=str,
+              default=Configuration().get('websocket', {}).get("cert_dir", f"{xdg_data_home()}/hivemind"))
+@click.option("--cert_name", help="HiveMind SSL certificate file name", type=str,
+              default=Configuration().get('websocket', {}).get("cert_name", "hivemind"))
+def listen(host: str, port: int, ssl: bool, cert_dir: str, cert_name: str):
+    from hivemind_core.service import HiveMindService
+
+    websocket_config = {
+        "host": host,
+        "port": port,
+        "ssl": ssl,
+        "cert_dir": cert_dir,
+        "cert_name": cert_name
+    }
+
+    service = HiveMindService(websocket_config=websocket_config)
+    service.start()
+
+
+if __name__ == "__main__":
+    hmcore_cmds()
+
diff --git a/hivemind_core/service.py b/hivemind_core/service.py
new file mode 100644
index 0000000..58aa17b
--- /dev/null
+++ b/hivemind_core/service.py
@@ -0,0 +1,235 @@
+import asyncio
+import base64
+import os
+import os.path
+import random
+from os import makedirs
+from os.path import exists, join
+from socket import gethostname
+from threading import Thread
+
+import tornado.platform.asyncio
+from OpenSSL import crypto
+from ovos_config import Configuration
+from ovos_utils.log import LOG
+from ovos_utils.process_utils import ProcessStatus, StatusCallbackMap
+from ovos_bus_client.session import Session
+from ovos_utils.xdg_utils import xdg_data_home
+from poorman_handshake import HandShake, PasswordHandShake
+from pyee import EventEmitter
+from tornado import web
+from tornado.websocket import WebSocketHandler
+
+from hivemind_bus_client.identity import NodeIdentity
+from hivemind_core.database import ClientDatabase
+from hivemind_core.protocol import HiveMindListenerProtocol, HiveMindClientConnection, HiveMindNodeType
+from hivemind_presence import LocalPresence
+from ovos_bus_client import MessageBusClient
+
+
+def create_self_signed_cert(cert_dir=f"{xdg_data_home()}/hivemind",
+                            name="hivemind"):
+    """
+    If name.crt and name.key don't exist in cert_dir, create a new
+    self-signed cert and key pair and write them into that directory.
+    """
+    CERT_FILE = name + ".crt"
+    KEY_FILE = name + ".key"
+    cert_path = join(cert_dir, CERT_FILE)
+    key_path = join(cert_dir, KEY_FILE)
+    makedirs(cert_dir, exist_ok=True)
+
+    if not exists(join(cert_dir, CERT_FILE)) \
+            or not exists(join(cert_dir, KEY_FILE)):
+        # create a key pair
+        k = crypto.PKey()
+        k.generate_key(crypto.TYPE_RSA, 2048)
+
+        # create a self-signed cert
+        cert = crypto.X509()
+        cert.get_subject().C = "PT"
+        cert.get_subject().ST = "Europe"
+        cert.get_subject().L = "Mountains"
+        cert.get_subject().O = "Jarbas AI"
+        cert.get_subject().OU = "Powered by HiveMind"
+        cert.get_subject().CN = gethostname()
+        cert.set_serial_number(random.randint(0, 2000))
+        cert.gmtime_adj_notBefore(0)
+        cert.gmtime_adj_notAfter(10 * 365 * 24 * 60 * 60)
+        cert.set_issuer(cert.get_subject())
+        cert.set_pubkey(k)
+        # TODO don't use sha1
+        cert.sign(k, 'sha1')
+
+        if not exists(cert_dir):
+            makedirs(cert_dir)
+        open(cert_path, "wb").write(
+            crypto.dump_certificate(crypto.FILETYPE_PEM, cert))
+        open(join(cert_dir, KEY_FILE), "wb").write(
+            crypto.dump_privatekey(crypto.FILETYPE_PEM, k))
+
+    return cert_path, key_path
+
+
+def on_ready():
+    LOG.info('HiveMind bus service ready!')
+
+
+def on_alive():
+    LOG.info('HiveMind bus service alive')
+
+
+def on_started():
+    LOG.info('HiveMind bus service started!')
+
+
+def on_error(e='Unknown'):
+    LOG.info('HiveMind bus failed to start ({})'.format(repr(e)))
+
+
+def on_stopping():
+    LOG.info('HiveMind bus is shutting down...')
+
+
+class MessageBusEventHandler(WebSocketHandler):
+    protocol: HiveMindListenerProtocol = None
+
+    def __init__(self, application, request, **kwargs):
+        super().__init__(application, request, **kwargs)
+        self.emitter = EventEmitter()
+
+    @staticmethod
+    def decode_auth(auth):
+        userpass_encoded = bytes(auth, encoding="utf-8")
+        userpass_decoded = base64.b64decode(userpass_encoded).decode("utf-8")
+        name, key = userpass_decoded.split(":")
+        return name, key
+
+    def on(self, event_name, handler):
+        self.emitter.on(event_name, handler)
+
+    def on_message(self, message):
+        if isinstance(message, bytes):
+            LOG.info(f"received binary data: {len(message)}")
+            self.protocol.handle_binary_message(message, self.client)
+        else:
+            message = self.client.decode(message)
+            LOG.info(f"received {self.client.peer} message: {message}")
+            self.protocol.handle_message(message, self.client)
+
+    def open(self):
+        auth = self.request.uri.split("/?authorization=")[-1]
+        name, key = self.decode_auth(auth)
+        LOG.info(f"authorizing client: {name}")
+
+        # in regular handshake an asymmetric key pair is used
+        handshake = HandShake(HiveMindService.identity.private_key)
+        self.client = HiveMindClientConnection(key=key, name=name,
+                                               ip=self.request.remote_ip, socket=self, sess=Session(),
+                                               handshake=handshake, loop=self.protocol.loop)
+
+        with ClientDatabase() as users:
+            user = users.get_client_by_api_key(key)
+            if not user:
+                LOG.error("Client provided an invalid api key")
+                self.protocol.handle_invalid_key_connected(self.client)
+                self.close()
+                return
+
+            self.client.crypto_key = users.get_crypto_key(key)
+            pswd = users.get_password(key)
+            if pswd:
+                # pre-shared password to derive aes_key
+                self.client.pswd_handshake = PasswordHandShake(pswd)
+
+            self.client.node_type = HiveMindNodeType.NODE  # TODO . placeholder
+
+            if not self.client.crypto_key and \
+                    not self.protocol.handshake_enabled \
+                    and self.protocol.require_crypto:
+                LOG.error("No pre-shared crypto key for client and handshake disabled, "
+                          "but configured to require crypto!")
+                # clients requiring handshake support might fail here
+                self.protocol.handle_invalid_protocol_version(self.client)
+                self.close()
+                return
+
+        self.protocol.handle_new_client(self.client)
+        # self.write_message(Message("connected").serialize())
+
+    def on_close(self):
+        LOG.info(f"disconnecting client: {self.client.peer}")
+        self.protocol.handle_client_disconnected(self.client)
+
+    def check_origin(self, origin):
+        return True
+
+
+class HiveMindService(Thread):
+    identity = NodeIdentity()
+
+    def __init__(self, alive_hook=on_alive, started_hook=on_started, ready_hook=on_ready,
+                 error_hook=on_error, stopping_hook=on_stopping, websocket_config=None):
+        super().__init__()
+        websocket_config = websocket_config or Configuration().get('hivemind_websocket', {})
+        callbacks = StatusCallbackMap(on_started=started_hook,
+                                      on_alive=alive_hook,
+                                      on_ready=ready_hook,
+                                      on_error=error_hook,
+                                      on_stopping=stopping_hook)
+
+        self.bus = MessageBusClient(emitter=EventEmitter())
+        self.bus.run_in_thread()
+        self.bus.connected_event.wait()
+
+        self.status = ProcessStatus('HiveMind', callback_map=callbacks)
+        self.host = websocket_config.get('host') or "0.0.0.0"
+        self.port = websocket_config.get('port') or 5678
+        self.ssl = websocket_config.get('ssl', False)
+        self.cert_dir = websocket_config.get('cert_dir') or f"{xdg_data_home()}/hivemind"
+        self.cert_name = websocket_config.get('cert_name') or "hivemind"  # name + ".crt"/".key"
+
+        self.presence = LocalPresence(name=self.identity.name,
+                                      service_type=HiveMindNodeType.MIND,
+                                      upnp=websocket_config.get('upnp', False),
+                                      port=self.port,
+                                      zeroconf=websocket_config.get('zeroconf', False))
+
+    def run(self):
+        self.status.set_alive()
+        asyncio.set_event_loop_policy(tornado.platform.asyncio.AnyThreadEventLoopPolicy())
+
+        loop = tornado.ioloop.IOLoop.current()
+
+        self.protocol = HiveMindListenerProtocol(loop=loop)
+        self.protocol.bind(MessageBusEventHandler, self.bus)
+        self.status.bind(self.bus)
+        self.status.set_started()
+
+        routes = [("/", MessageBusEventHandler)]
+        application = web.Application(routes)
+
+        if self.ssl:
+            CERT_FILE = f"{self.cert_dir}/{self.cert_name}.crt"
+            KEY_FILE = f"{self.cert_dir}/{self.cert_name}.key"
+            if not os.path.isfile(KEY_FILE):
+                LOG.info(f"generating self-signed SSL certificate")
+                CERT_FILE, KEY_FILE = create_self_signed_cert(self.cert_dir, self.cert_name)
+            LOG.debug("using ssl key at " + KEY_FILE)
+            LOG.debug("using ssl certificate at " + CERT_FILE)
+            ssl_options = {"certfile": CERT_FILE, "keyfile": KEY_FILE}
+
+            LOG.info("wss connection started")
+            application.listen(self.port, self.host, ssl_options=ssl_options)
+        else:
+            LOG.info("ws connection started")
+            application.listen(self.port, self.host)
+
+        self.presence.start()
+
+        self.status.set_ready()
+
+        loop.start()
+
+        self.status.set_stopping()
+        self.presence.stop()
diff --git a/jarbas_hive_mind/version.py b/hivemind_core/version.py
similarity index 79%
rename from jarbas_hive_mind/version.py
rename to hivemind_core/version.py
index 048f884..c025fc9 100644
--- a/jarbas_hive_mind/version.py
+++ b/hivemind_core/version.py
@@ -1,7 +1,7 @@
 # The following lines are replaced during the release process.
 # START_VERSION_BLOCK
 VERSION_MAJOR = 0
-VERSION_MINOR = 11
+VERSION_MINOR = 13
 VERSION_BUILD = 0
-VERSION_ALPHA = 3
+VERSION_ALPHA = 1
 # END_VERSION_BLOCK
diff --git a/jarbas_hive_mind/__init__.py b/jarbas_hive_mind/__init__.py
deleted file mode 100644
index 1c6ebf0..0000000
--- a/jarbas_hive_mind/__init__.py
+++ /dev/null
@@ -1,14 +0,0 @@
-from jarbas_hive_mind.nodes.master import HiveMind, HiveMindProtocol
-from jarbas_hive_mind.configuration import CONFIGURATION
-from jarbas_hive_mind.settings import DEFAULT_PORT
-from jarbas_hive_mind.utils import create_self_signed_cert
-from jarbas_hive_mind.exceptions import SecureConnectionFailed, ConnectionError
-from jarbas_hive_mind.backends import HiveMindConnection, HiveMindListener
-
-
-def get_listener(port=DEFAULT_PORT, max_connections=-1, bus=None):
-    return HiveMindListener(port, max_connections, bus)
-
-
-def get_connection(host="127.0.0.1", port=DEFAULT_PORT):
-    return HiveMindConnection(host, port)
diff --git a/jarbas_hive_mind/__main__.py b/jarbas_hive_mind/__main__.py
deleted file mode 100644
index 90c8430..0000000
--- a/jarbas_hive_mind/__main__.py
+++ /dev/null
@@ -1,18 +0,0 @@
-from jarbas_hive_mind import get_listener
-from jarbas_hive_mind.configuration import CONFIGURATION
-
-def main():
-    import argparse
-    parser = argparse.ArgumentParser(description='Start HiveMind as a server')
-    parser.add_argument("--port", help="HiveMind port number", type=int)
-    args = parser.parse_args()
-    config = CONFIGURATION
-    listener = get_listener()
-    listener.load_config(config)
-    # Replace defined values
-    if args.port is not None:
-        listener.port = args.port
-    listener.listen()
-
-if __name__ == '__main__':
-    main()
diff --git a/jarbas_hive_mind/backends/__init__.py b/jarbas_hive_mind/backends/__init__.py
deleted file mode 100644
index aa794b2..0000000
--- a/jarbas_hive_mind/backends/__init__.py
+++ /dev/null
@@ -1,18 +0,0 @@
-from jarbas_hive_mind.configuration import CONFIGURATION
-
-if CONFIGURATION['twisted']:
-    try:
-        from jarbas_hive_mind.backends.twisted_reactor import WebSocketServerProtocol,\
-            WebSocketServerFactory, WebSocketClientFactory, WebSocketClientProtocol,\
-            HiveMindTwistedListener as HiveMindListener, \
-            HiveMindTwistedConnection as HiveMindConnection
-    except ImportError:
-        # only asyncio available / twisted not installed
-        CONFIGURATION['twisted'] = False
-
-if not CONFIGURATION['twisted']:
-    from jarbas_hive_mind.backends.asyncio_loop import WebSocketServerProtocol,\
-        WebSocketServerFactory, WebSocketClientFactory, WebSocketClientProtocol,\
-        HiveMindAsyncioListener as HiveMindListener, \
-        HiveMindAsyncioConnection as HiveMindConnection
-
diff --git a/jarbas_hive_mind/backends/asyncio_loop.py b/jarbas_hive_mind/backends/asyncio_loop.py
deleted file mode 100644
index 234c4ea..0000000
--- a/jarbas_hive_mind/backends/asyncio_loop.py
+++ /dev/null
@@ -1,55 +0,0 @@
-from autobahn.asyncio.websocket import WebSocketServerProtocol, \
-    WebSocketServerFactory, WebSocketClientFactory, \
-    WebSocketClientProtocol
-import asyncio
-import ssl
-from jarbas_hive_mind.backends.base import HiveMindAbstractListener, HiveMindAbstractConnection
-from ovos_utils.log import LOG
-
-
-class HiveMindAsyncioConnection(HiveMindAbstractConnection):
-    def run(self):
-        self.loop = asyncio.get_event_loop()
-        if self.is_secure:
-            ssl_context = ssl.create_default_context()
-            if self.accept_self_signed:
-                ssl_context.check_hostname = False
-                ssl_context.verify_mode = ssl.CERT_NONE
-            coro = self.loop.create_connection(self.ws, self.host,
-                                               self.port, ssl=ssl_context)
-        else:
-            coro = self.loop.create_connection(self.ws, self.host, self.port)
-        self.loop.run_until_complete(coro)
-        self.loop.run_forever()
-        self.loop.close()
-
-    def close(self):
-        if self.loop:
-            self.loop.close()
-
-
-class HiveMindAsyncioListener(HiveMindAbstractListener):
-    def run(self):
-        self.loop = asyncio.get_event_loop()
-        if self.is_secure:
-            ssl_context = ssl.create_default_context()
-            if self.accept_self_signed:
-                ssl_context.check_hostname = False
-                ssl_context.verify_mode = ssl.CERT_NONE
-            ssl_context.load_cert_chain(self.ssl_cert, self.ssl_key)
-
-            coro = self.loop.create_server(self.factory, self.host,
-                                           self.port, ssl=ssl_context)
-            LOG.info("HiveMind Listening: " + self.address)
-        else:
-            coro = self.loop.create_server(self.factory, self.host, self.port)
-            LOG.info("HiveMind Listening (UNSECURED): " + self.address)
-
-        self.server = self.loop.run_until_complete(coro)
-        self.loop.run_forever()
-        self.stop()
-
-    def stop(self):
-        self.server.close()
-        self.loop.close()
-
diff --git a/jarbas_hive_mind/backends/base.py b/jarbas_hive_mind/backends/base.py
deleted file mode 100644
index 480d0ac..0000000
--- a/jarbas_hive_mind/backends/base.py
+++ /dev/null
@@ -1,213 +0,0 @@
-import base64
-
-from jarbas_hive_mind.nodes.master import HiveMind, HiveMindProtocol
-from jarbas_hive_mind.configuration import CONFIGURATION
-from jarbas_hive_mind.settings import DEFAULT_PORT
-from jarbas_hive_mind.utils import create_self_signed_cert
-from jarbas_hive_mind.exceptions import SecureConnectionFailed, ConnectionError
-from ovos_utils.messagebus import get_mycroft_bus
-from ovos_utils.log import LOG
-from os.path import join, exists, isfile
-
-
-class HiveMindAbstractConnection:
-    _autorun = True
-
-    def __init__(self, host="0.0.0.0", port=DEFAULT_PORT,
-                 accept_self_signed=True):
-        host = host.replace("https://", "wss://").replace("http://", "ws://")
-        if "wss://" in host:
-            self._secure = True
-        else:
-            self._secure = False
-
-        host = host.replace("wss://", "").replace("ws://", "")
-        self.host = host
-        self.port = port
-        self.loop = None
-        self.ws = None
-
-        self.accept_self_signed = accept_self_signed
-
-    @property
-    def is_secure(self):
-        return self._secure
-
-    @property
-    def address(self):
-        if self.is_secure:
-            if "wss://" in self.host:
-                return self.host + u":" + str(self.port)
-            return "wss://" + self.host + u":" + str(self.port)
-        else:
-            if "ws://" in self.host:
-                return self.host + u":" + str(self.port)
-            return "ws://" + self.host + u":" + str(self.port)
-
-    @property
-    def peer(self):
-        return "tcp4:" + self.host + ":" + str(self.port)
-
-    @staticmethod
-    def get_headers(name, key):
-        # Note that keys can be shared across users
-        # name is not used for auth
-        name = name.replace(":", "__")
-        authorization = bytes(name + ":" + key, encoding="utf-8")
-        key = base64.b64encode(authorization)
-        headers = {'authorization': key}
-        return headers
-
-    def secure_connect(self, component):
-        self._secure = True
-        self.ws = component
-        self.ws.bind(self)
-        LOG.info("Connecting securely to " + self.address)
-        self.run()
-
-    def unsafe_connect(self, component):
-        self._secure = False
-        self.ws = component
-        self.ws.bind(self)
-        LOG.info("Connecting to " + self.address)
-        LOG.warning("This listener is unsecured")
-        self.run()
-
-    def run(self):
-        raise NotImplementedError
-
-    def connect(self, component):
-        try:
-            if self.is_secure:
-                try:
-                    return self.secure_connect(component)
-                except ConnectionError:
-                    raise SecureConnectionFailed
-            else:
-                return self.unsafe_connect(component)
-        except Exception as e:
-            LOG.exception(e)
-            raise e
-
-    def close(self):
-        raise NotImplementedError
-
-
-class HiveMindAbstractListener:
-    _autorun = True
-    default_factory = HiveMind
-    default_protocol = HiveMindProtocol
-
-    def __init__(self, port=DEFAULT_PORT, max_cons=-1, bus=None,
-                 host="0.0.0.0", accept_self_signed=True):
-        self.host = host
-        self.port = port
-        self.max_cons = max_cons
-        self._use_ssl = CONFIGURATION["ssl"].get("use_ssl", False)
-        self.certificate_path = CONFIGURATION["ssl"]["certificates"]
-        self._key_file = CONFIGURATION["ssl"].get("ssl_keyfile",
-                                                  "HiveMind.key")
-        self._cert_file = CONFIGURATION["ssl"].get("ssl_certfile",
-                                                   "HiveMind.crt")
-        self.bus = bus
-        self.accept_self_signed = accept_self_signed
-
-    def bind(self, bus=None):
-        # TODO read config for bus options
-        self.bus = bus or get_mycroft_bus()
-
-    @property
-    def ssl_key(self):
-        if isfile(self._key_file):
-            return self._key_file
-        return join(self.certificate_path, self._key_file)
-
-    @property
-    def ssl_cert(self):
-        if isfile(self._cert_file):
-            return self._cert_file
-        return join(self.certificate_path, self._cert_file)
-
-    def load_config(self, config=CONFIGURATION):
-        # read configuration
-        self.port = config["port"]
-        self.max_cons = config.get("max_connections", -1)
-
-        ssl_config = config.get("ssl", {})
-        self.certificate_path = ssl_config.get("certificates",
-                                               self.certificate_path)
-        self._key_file = ssl_config.get("ssl_keyfile", self._key_file)
-        self._cert_file = ssl_config.get("ssl_certfile", self._cert_file)
-        self._use_ssl = ssl_config.get("use_ssl", True)
-
-        # generate self signed keys
-        if not exists(self.ssl_key) and self.accept_self_signed and \
-                self.is_secure:
-            LOG.warning("ssl keys dont exist, creating self signed certificates")
-            self.generate_keys(self.certificate_path)
-
-    @property
-    def is_secure(self):
-        return self._use_ssl
-
-    @property
-    def address(self):
-        if self.is_secure:
-            return "wss://" + self.host + ":" + str(self.port)
-        return "ws://" + self.host + ":" + str(self.port)
-
-    @property
-    def peer(self):
-        return "tcp4:" + self.host + ":" + str(self.port)
-
-    @staticmethod
-    def generate_keys(path=CONFIGURATION["ssl"]["certificates"],
-                      key_name="HiveMind"):
-        LOG.info("creating self signed SSL keys")
-        name = key_name.split("/")[-1].replace(".key", "")
-        create_self_signed_cert(path, name)
-        cert = path + "/" + name + ".crt"
-        key = path + "/" + name + ".key"
-        LOG.info("key created at: " + key)
-        LOG.info("crt created at: " + cert)
-
-    def secure_listen(self, key=None, cert=None, factory=None, protocol=None):
-        self._use_ssl = True
-        self._ssl_key = key or self.ssl_key
-        self._ssl_cert = cert or self.ssl_cert
-
-        self.factory = factory or self.default_factory(bus=self.bus)
-        self.factory.protocol = protocol or self.default_protocol
-        if self.max_cons >= 0:
-            self.factory.setProtocolOptions(maxConnections=self.max_cons)
-        self.factory.bind(self)
-
-        if self._autorun:
-            self.run()
-        return factory
-
-    def unsafe_listen(self, factory=None, protocol=None):
-        self._use_ssl = False
-        self.factory = factory or self.default_factory(bus=self.bus)
-        self.factory.protocol = protocol or self.default_protocol
-        if self.max_cons >= 0:
-            self.factory.setProtocolOptions(maxConnections=self.max_cons)
-        self.factory.bind(self)
-
-        if self._autorun:
-            self.run()
-        return factory
-
-    def run(self):
-        raise NotImplementedError
-
-    def listen(self, factory=None, protocol=None):
-        if self.is_secure:
-            return self.secure_listen(factory=factory, protocol=protocol)
-        else:
-            return self.unsafe_listen(factory=factory, protocol=protocol)
-
-    def stop(self):
-        raise NotImplementedError
-
-
diff --git a/jarbas_hive_mind/backends/twisted_reactor.py b/jarbas_hive_mind/backends/twisted_reactor.py
deleted file mode 100644
index fadf112..0000000
--- a/jarbas_hive_mind/backends/twisted_reactor.py
+++ /dev/null
@@ -1,58 +0,0 @@
-from autobahn.twisted.websocket import WebSocketServerProtocol, \
-    WebSocketServerFactory, WebSocketClientFactory, \
-    WebSocketClientProtocol
-from twisted.internet.protocol import ReconnectingClientFactory
-from twisted.internet.error import ReactorNotRunning
-from ovos_utils.log import LOG
-from jarbas_hive_mind.backends.base import HiveMindAbstractListener, HiveMindAbstractConnection
-from twisted.internet import reactor, ssl
-
-
-class HiveMindTwistedConnection(HiveMindAbstractConnection):
-    def run(self):
-        if self._secure:
-            contextFactory = ssl.ClientContextFactory()
-            reactor.connectSSL(self.host, self.port, self.ws, contextFactory)
-        else:
-            reactor.connectTCP(self.host, self.port, self.ws)
-        if not reactor.running and self._autorun:
-            try:
-                reactor.run()
-            except Exception as e:
-                LOG.exception(e)
-
-    def close(self):
-        if reactor.running:
-            reactor.stop()
-
-
-class HiveMindTwistedListener(HiveMindAbstractListener):
-
-    def run(self):
-        if self._use_ssl:
-            # SSL server context: load server key and certificate
-            contextFactory = ssl.DefaultOpenSSLContextFactory(self._ssl_key,
-                                                              self._ssl_cert)
-            reactor.listenSSL(self.port, self.factory, contextFactory)
-        else:
-            reactor.listenTCP(self.port, self.factory)
-
-        if not reactor.running and self._autorun:
-            try:
-                reactor.run()
-            except Exception as e:
-                LOG.exception(e)
-
-    def stop(self):
-        try:
-            reactor.stop()
-        except ReactorNotRunning:
-            LOG.info("twisted reactor stopped")
-        except Exception as e:
-            try:
-                reactor.callFromThread(reactor.stop)
-                for p in reactor.getDelayedCalls():
-                    if p.active():
-                        p.cancel()
-            except Exception as e:
-                LOG.error(e)
diff --git a/jarbas_hive_mind/configuration/__init__.py b/jarbas_hive_mind/configuration/__init__.py
deleted file mode 100644
index 42e2694..0000000
--- a/jarbas_hive_mind/configuration/__init__.py
+++ /dev/null
@@ -1,59 +0,0 @@
-from json_database import JsonStorageXDG
-from os.path import join, exists, isdir
-from os import makedirs
-from jarbas_hive_mind.settings import DEFAULT_PORT, DATA_PATH, \
-    MYCROFT_WEBSOCKET_CONFIG
-from ovos_utils.xdg_utils import xdg_config_home
-
-
-def default_config():
-    return {'data_path': DATA_PATH,
-            'database': join(DATA_PATH, "database", "clients.db"),
-            'log_blacklist': [],
-            'max_connections': -1,
-            # asyncio is used by default
-            # set this to use twisted instead
-            'twisted': False,
-            'mycroft_bus': MYCROFT_WEBSOCKET_CONFIG,
-            'port': DEFAULT_PORT,
-            'ssl': {'certificates': join(DATA_PATH, "certs"),
-                    'ssl_certfile': 'HiveMind.crt',
-                    'ssl_keyfile': 'HiveMind.key'}
-            }
-
-
-# TODO use ovos_utils merge_dict
-def _merge_defaults(base, default=None):
-    """
-        Recursively merging configuration dictionaries.
-
-        Args:
-            base:  Target for merge
-            default: Dictionary to merge into base if key not present
-    """
-    default = default or default_config()
-    for k, dv in default.items():
-        bv = base.get(k)
-        if isinstance(dv, dict) and isinstance(bv, dict):
-            _merge_defaults(bv, dv)
-        elif k not in base:
-            base[k] = dv
-    return base
-
-
-CONFIGURATION = JsonStorageXDG("hivemind",
-                               xdg_folder=xdg_config_home(),
-                               subfolder="jarbasHiveMind",
-                               extension="conf")
-
-CONFIGURATION = _merge_defaults(CONFIGURATION)
-
-# ensure directories exist
-if not exists(CONFIGURATION.path):
-    CONFIGURATION.store()
-
-if not isdir(CONFIGURATION["data_path"]):
-    makedirs(CONFIGURATION["data_path"])
-
-if not isdir(CONFIGURATION["ssl"]["certificates"]):
-    makedirs(CONFIGURATION["ssl"]["certificates"])
diff --git a/jarbas_hive_mind/exceptions.py b/jarbas_hive_mind/exceptions.py
deleted file mode 100644
index b0a717e..0000000
--- a/jarbas_hive_mind/exceptions.py
+++ /dev/null
@@ -1,32 +0,0 @@
-
-
-class HiveMindException(Exception):
-    """ An Exception inside the HiveMind"""
-
-
-class UnauthorizedKeyError(HiveMindException):
-    """ Invalid Key provided """
-
-
-class WrongEncryptionKey(HiveMindException):
-    """ Wrong Encryption Key"""
-
-
-class DecryptionKeyError(WrongEncryptionKey):
-    """ Could not decrypt payload """
-
-
-class EncryptionKeyError(WrongEncryptionKey):
-    """ Could not encrypt payload """
-
-
-class ConnectionError(HiveMindException):
-    """ Could not connect to the HiveMind"""
-
-
-class SecureConnectionFailed(ConnectionError):
-    """ Could not connect by SSL """
-
-
-class HiveMindEntryPointNotFound(ConnectionError):
-    """ can not connect to provided address """
diff --git a/jarbas_hive_mind/interface.py b/jarbas_hive_mind/interface.py
deleted file mode 100644
index 3930885..0000000
--- a/jarbas_hive_mind/interface.py
+++ /dev/null
@@ -1,209 +0,0 @@
-import json
-from jarbas_hive_mind.utils import serialize_message
-from jarbas_hive_mind.message import HiveMessage, HiveMessageType
-from ovos_utils.messagebus import Message
-
-
-def _get_payload(msg):
-    if isinstance(msg, HiveMessage):
-        msg = msg.payload
-    if isinstance(msg, Message):
-        msg = msg.serialize()
-    if isinstance(msg, str):
-        msg = json.loads(msg)
-    return msg
-
-
-def _get_hivemsg(msg):
-    if isinstance(msg, str):
-        msg = json.loads(msg)
-    if isinstance(msg, dict):
-        msg = HiveMessage(**msg)
-    if isinstance(msg, Message):
-        msg = HiveMessage(msg_type=HiveMessageType.BUS, payload=msg)
-    assert isinstance(msg, HiveMessage)
-    return msg
-
-
-class HiveMindSlaveInterface:
-    def __init__(self, connection):
-        self.client = connection
-
-    @property
-    def bus(self):
-        # if bus is None -> Terminal
-        # else -> Slave
-        return self.client.bus
-
-    @property
-    def node_id(self):
-        return self.client.node_id
-
-    @property
-    def peer(self):
-        return self.client.peer
-
-    def send(self, payload):
-        payload = serialize_message(payload)
-        if not isinstance(payload, bytes):
-            payload = bytes(payload, encoding="utf-8")
-        self.client.sendMessage(payload)
-
-    def send_to_hivemind_bus(self, payload):
-        payload = {"msg_type": HiveMessageType.BUS,
-                   "payload": _get_payload(payload),
-                   "node": self.node_id,
-                   "source_peer": self.peer
-                   }
-        self.send(payload)
-
-    def broadcast(self, message):
-        message = _get_hivemsg(message)
-        payload = {"msg_type": HiveMessageType.BROADCAST,
-                   "payload": message,
-                   "route": message.route,
-                   "source_peer": self.peer,
-                   "node": self.client.node_id
-                   }
-        if self.bus:
-            message = Message("hive.send", payload,
-                              {"destination": "hive",
-                               "source": self.peer})
-            self.bus.emit(message)
-
-    def propagate(self, message):
-        message = _get_hivemsg(message)
-        payload = {"msg_type": HiveMessageType.PROPAGATE,
-                   "payload": message,
-                   "route": message.route,
-                   "source_peer": self.peer,
-                   "node": self.client.node_id
-                   }
-
-        no_send = [n["source"] for n in message.route]
-        if self.peer not in no_send:
-            self.send(payload)
-
-        if self.bus and payload.get("node", "") == self.peer + ":MASTER":
-            message = Message("hive.send", payload,
-                              {"destination": "hive",
-                               "source": self.peer})
-            self.bus.emit(message)
-
-    def escalate(self, message):
-        message = _get_hivemsg(message)
-        # when master receives this it will
-        # update "targets" field with it's own id
-        route_data = {"source": self.peer,
-                      "targets": []}
-        message.update_hop_data(route_data)
-        payload = {"msg_type": HiveMessageType.ESCALATE,
-                   "payload": message,
-                   "route": message.route,
-                   "source_peer": self.peer,
-                   "node": self.client.node_id
-                   }
-        self.send(payload)
-
-    def query(self, payload, msg_data=None):
-        raise NotImplementedError
-
-    def cascade(self, payload, msg_data=None):
-        raise NotImplementedError
-
-
-class HiveMindMasterInterface:
-    def __init__(self, listener):
-        self.listener = listener
-
-    @property
-    def peer(self):
-        return self.listener.peer
-
-    @property
-    def clients(self):
-        return [c["instance"] for c in
-                self.listener.clients.values()]
-
-    @property
-    def node_id(self):
-        return self.listener.node_id
-
-    @property
-    def bus(self):
-        return self.listener.bus
-
-    def send(self, payload, client):
-        payload = serialize_message(payload)
-        if isinstance(client, str):
-            client = self.listener.clients[client]
-        if not isinstance(payload, bytes):
-            payload = bytes(payload, encoding="utf-8")
-
-        client.sendMessage(payload)
-
-    def send_to_many(self, payload, no_send=None):
-        no_send = no_send or []
-        payload["route"] = payload["route"] or []
-        route_data = {"source": self.peer,
-                      "targets": [c.peer for c in self.clients]}
-        # No resend
-        if route_data in payload["route"]:
-            return
-
-        payload["route"].append(route_data)
-
-        for client in self.clients:
-            if client.peer not in no_send:
-                self.send(payload, client)
-
-    def broadcast(self, message):
-        message = _get_hivemsg(message)
-        payload = {"msg_type": HiveMessageType.BROADCAST,
-                   "payload": message,
-                   "route": message.route,
-                   "source_peer": self.peer,
-                   "node": self.node_id
-                   }
-        no_send = [n["source"] for n in message.route]
-        self.send_to_many(payload, no_send)
-
-    def propagate(self, message):
-        message = _get_hivemsg(message)
-        payload = {"msg_type": HiveMessageType.PROPAGATE,
-                   "payload": message,
-                   "route": message.route,
-                   "source_peer": self.peer,
-                   "node": self.node_id
-                   }
-        no_send = [n["source"] for n in message.route]
-        self.send_to_many(payload, no_send)
-
-        # tell Slave to propagate upstream
-        if self.bus and message.source_peer != self.peer:
-            message = Message("hive.send", payload,
-                              {"destination": "hive",
-                               "source": self.peer})
-            self.bus.emit(message)
-
-    def escalate(self, message):
-        message = _get_hivemsg(message)
-        payload = {"msg_type": HiveMessageType.ESCALATE,
-                   "payload": message,
-                   "route": message.route,
-                   "source_peer": self.peer,
-                   "node": self.node_id
-                   }
-        # tell Slave to escalate upstream
-        if self.bus and message.source_peer != self.peer:
-            message = Message("hive.send", payload,
-                              {"destination": "hive",
-                               "source": self.peer})
-            self.bus.emit(message)
-
-    def query(self, payload, msg_data=None):
-        raise NotImplementedError
-
-    def cascade(self, payload, msg_data=None):
-        raise NotImplementedError
-
diff --git a/jarbas_hive_mind/message.py b/jarbas_hive_mind/message.py
deleted file mode 100644
index 5105f20..0000000
--- a/jarbas_hive_mind/message.py
+++ /dev/null
@@ -1,143 +0,0 @@
-from enum import Enum
-import json
-from ovos_utils.json_helper import merge_dict
-from mycroft_bus_client import Message
-
-
-class HiveMessageType(str, Enum):
-    HANDSHAKE = "shake"  # negotiate initial connection
-    BUS = "bus"  # request meant for internal mycroft-bus in master
-    SHARED_BUS = "shared_bus"  # passive sharing of message
-    # from mycroft-bus in slave
-    BROADCAST = "broadcast"  # forward message to all slaves
-    PROPAGATE = "propagate"  # forward message to all slaves and masters
-    ESCALATE = "escalate"  # forward message up the authority chain to all
-    # masters
-    HELLO = "hello"  # like escalate, used to announce the device
-    QUERY = "query"  # like escalate, but stops once one of the nodes can
-    # send a response
-    CASCADE = "cascade"  # like propagate, but expects a response back from
-    # all nodes in the hive (responses optional)
-    PING = "ping"  # like cascade, but used to map the network
-    RENDEZVOUS = "rendezvous"  # reserved for rendezvous-nodes
-
-    THIRDPRTY = "3rdparty"  # user land message, do whatever you want
-
-
-class HiveMessage:
-    def __init__(self, msg_type, payload=None, node=None, source_peer=None,
-                 route=None, target_peers=None):
-        #  except for the hivemind node classes receiving the message and
-        #  creating the object nothing should be able to change these values
-        #  node classes might change them a runtime by the private attribute
-        #  but end-users should consider them read_only
-        if msg_type not in [m.value for m in HiveMessageType]:
-            raise ValueError("Unknown HiveMessage.msg_type")
-
-        self._msg_type = msg_type
-        # the payload is more or less a free for all
-        # the msg_type determines what happens to the message, but the
-        # payload can simply be ignored by the receiving module
-
-        # some msg_types might return HiveMessage, others (mycroft) Message
-        # we should support the dict/json format, json is used at the
-        # transport layer before converting into any of these formats
-        if isinstance(payload, Message):
-            payload = {"type": payload.msg_type,
-                       "data": payload.data,
-                       "context": payload.context}
-        elif isinstance(payload, str):
-            payload = json.loads(payload)
-        self._payload = payload or {}
-
-        self._node = node  # node semi-unique identifier
-        self._source_peer = source_peer  # peer_id
-        self._route = route or []  # where did this message come from
-        self._targets = target_peers or []  # where will it be sent
-
-    @property
-    def msg_type(self):
-        return self._msg_type
-
-    @property
-    def node_id(self):
-        return self._node
-
-    @property
-    def source_peer(self):
-        return self._source_peer
-
-    @property
-    def target_peers(self):
-        if self.source_peer:
-            return self._targets or [self._source_peer]
-        return self._targets
-
-    @property
-    def route(self):
-        return [r for r in self._route if r.get("targets") and r.get("source")]
-
-    @property
-    def payload(self):
-        if self.msg_type in [HiveMessageType.BUS, HiveMessageType.SHARED_BUS]:
-            return Message(self._payload["type"],
-                           data=self._payload.get("data"),
-                           context=self._payload.get("context"))
-        if self.msg_type in [HiveMessageType.BROADCAST,
-                             HiveMessageType.PROPAGATE,
-                             HiveMessageType.CASCADE,
-                             HiveMessageType.ESCALATE]:
-            return HiveMessage(**self._payload)
-        return self._payload
-
-    @property
-    def as_dict(self):
-        pload = self._payload
-        if isinstance(pload, HiveMessage):
-            pload = pload.as_json
-        elif isinstance(pload, Message):
-            pload = pload.serialize()
-        if isinstance(pload, str):
-            pload = json.loads(pload)
-        return {"msg_type": self.msg_type,
-                "payload": pload,
-                "route": self.route,
-                "node": self.node_id,
-                "source_peer": self.source_peer}
-
-    @property
-    def as_json(self):
-        return json.dumps(self.as_dict)
-
-    def serialize(self):
-        return self.as_json
-
-    def __getitem__(self, item):
-        return self._payload.get(item)
-
-    def __setitem__(self, key, value):
-        self._payload[key] = value
-
-    def __str__(self):
-        return self.as_json
-
-    def update_hop_data(self, data=None, **kwargs):
-        if not self._route or self._route[-1]["source"] != self.source_peer:
-            self._route += [{"source": self.source_peer,
-                             "targets": self.target_peers}]
-        if self._route and data:
-            self._route[-1] = merge_dict(self._route[-1], data, **kwargs)
-
-    def replace_route(self, route):
-        self._route = route
-
-    def update_source_peer(self, peer):
-        self._source_peer = peer
-        return self
-
-    def add_target_peer(self, peer):
-        self._targets.append(peer)
-
-    def remove_target_peer(self, peer):
-        if peer in self._targets:
-            self._targets.pop(peer)
diff --git a/jarbas_hive_mind/nodes/__init__.py b/jarbas_hive_mind/nodes/__init__.py
deleted file mode 100644
index 4d32a8f..0000000
--- a/jarbas_hive_mind/nodes/__init__.py
+++ /dev/null
@@ -1,21 +0,0 @@
-from enum import Enum
-
-
-class HiveMindNodeType(str, Enum):
-    CANDIDATE_NODE = "candidate"  # potential node, if it manages to connect...
-    NODE = "node"  # anything connected to the hivemind is a "node"
-    MIND = "mind"  # listening for connections and providing mycroft-core
-                   # (mycroft itself may be running in a different "mind")
-    FAKECROFT = "fakecroft"  # a mind, that pretends to be running mycroft
-                             # but is actually using a different stack
-    # (mycroft itself may be running in a different "mind")
-    SLAVE = "slave"  # node that can be partially controlled by a "mind"
-    TERMINAL = "terminal"  # user facing endpoint that connects to some Mind
-                           # and does not itself accept connections
-    BRIDGE = "bridge"  # connects some external service to the hive
-
-    # RESERVED
-    HIVE = "hive"  # a collection of nodes
-    MASTER_MIND = "master"  # the top level node, not connected to anything
-                            # but receiving connections
-
diff --git a/jarbas_hive_mind/nodes/fakecroft.py b/jarbas_hive_mind/nodes/fakecroft.py
deleted file mode 100644
index 9f83d1e..0000000
--- a/jarbas_hive_mind/nodes/fakecroft.py
+++ /dev/null
@@ -1,33 +0,0 @@
-from jarbas_hive_mind.nodes.master import HiveMind, HiveMindProtocol
-from jarbas_hive_mind.message import HiveMessage, HiveMessageType
-from jarbas_hive_mind.nodes import HiveMindNodeType
-from ovos_utils.log import LOG
-
-
-platform = "FakeCroftMindV0.1"
-
-
-class FakeCroftMindProtocol(HiveMindProtocol):
-    """"""
-
-
-class FakeCroftMind(HiveMind):
-    protocol = FakeCroftMindProtocol
-    node_type = HiveMindNodeType.FAKECROFT
-
-    def __init__(self, *args, **kwargs):
-        super().__init__(bus=False, *args, **kwargs)
-
-    def handle_incoming_mycroft(self, message, client):
-        """
-        message (Message): mycroft bus message object
-        """
-        LOG.debug(f"Mycroft bus message received: {message.msg_type}")
-        LOG.debug(f"data: {message.data}")
-        LOG.debug(f"context: {message.context}")
-
-        answer = "mycroft is dead! long live mycroft!"
-
-        payload = HiveMessage(HiveMessageType.BUS,
-                              message.reply("speak", {"utterance": answer}))
-        self.interface.send(payload, client)
diff --git a/jarbas_hive_mind/nodes/master.py b/jarbas_hive_mind/nodes/master.py
deleted file mode 100644
index 2f4d8bf..0000000
--- a/jarbas_hive_mind/nodes/master.py
+++ /dev/null
@@ -1,442 +0,0 @@
-import base64
-from jarbas_hive_mind.database import ClientDatabase
-from jarbas_hive_mind.exceptions import UnauthorizedKeyError
-from ovos_utils.log import LOG
-from ovos_utils.messagebus import Message, get_mycroft_bus
-from jarbas_hive_mind.utils import decrypt_from_json, encrypt_as_json
-from jarbas_hive_mind.interface import HiveMindMasterInterface
-import json
-from jarbas_hive_mind.message import HiveMessage, HiveMessageType
-from jarbas_hive_mind.nodes import HiveMindNodeType
-from ovos_utils.messagebus import FakeBus
-from HiveMind_presence import LocalPresence
-
-
-# protocol
-class HiveMindProtocol:
-    platform = "HiveMindV0.7"
-
-    def __new__(cls, *args, **kwargs):
-        # this non sense is changing the base class
-        # this allows subclassing either from twisted or asyncio
-        # but you only know which at runtime
-        from jarbas_hive_mind.backends import WebSocketServerProtocol
-        x = type(cls.__name__, (HiveMindProtocol, WebSocketServerProtocol), {})
-        # print(x, x.__bases__)
-        return super(HiveMindProtocol, cls).__new__(x)
-
-    @staticmethod
-    def decode_auth(request):
-        # see if params were passed in url
-        auth = request.params.get("authorization")
-        if auth:
-            auth = auth[0]
-            userpass_encoded = bytes(auth, encoding="utf-8")
-        else:
-            # regular websocket auth wss://user:pass@url:port
-            auth = request.headers.get("authorization")
-            userpass_encoded = bytes(auth, encoding="utf-8")
-            if userpass_encoded.startswith(b"Basic "):
-                userpass_encoded = userpass_encoded[6:-2]
-            else:
-                userpass_encoded = userpass_encoded[2:-1]
-        userpass_decoded = base64.b64decode(userpass_encoded).decode("utf-8")
-        name, key = userpass_decoded.split(":")
-        return name, key
-
-    def onConnect(self, request):
-        LOG.info("Client connecting: {0}".format(request.peer))
-
-        name, key = self.decode_auth(request)
-
-        ip = request.peer.split(":")[1]
-        context = {"source": self.peer}
-        self.platform = request.headers.get("platform", "unknown")
-
-        try:
-            with ClientDatabase() as users:
-                user = users.get_client_by_api_key(key)
-                if not user:
-                    raise UnauthorizedKeyError
-                self.crypto_key = users.get_crypto_key(key)
-        except UnauthorizedKeyError:
-            LOG.error("Client provided an invalid api key")
-            self.factory.mycroft_send("hive.client.connection.error",
-                                      {"error": "invalid api key",
-                                       "ip": ip,
-                                       "api_key": key,
-                                       "platform": self.platform},
-                                      context)
-            raise
-
-        # send message to internal mycroft bus
-        data = {"ip": ip, "headers": request.headers}
-        with ClientDatabase() as users:
-            self.blacklist = users.get_blacklist_by_api_key(key)
-        self.factory.mycroft_send("hive.client.connect", data, context)
-        # return a pair with WS protocol spoken (or None for any) and
-        # custom headers to send in initial WS opening handshake HTTP response
-        headers = {"server": self.platform}
-        return (None, headers)
-
-    def onOpen(self):
-        """
-       Connection from client is opened. Fires after opening
-       websockets handshake has been completed and we can send
-       and receive messages.
-
-       Register client in factory, so that it is able to track it.
-       """
-        LOG.info("WebSocket connection open.")
-        self.factory.register_client(self, self.platform)
-
-    def onMessage(self, payload, isBinary):
-
-        if isBinary:
-            LOG.debug(
-                "Binary message received: {0} bytes".format(len(payload)))
-        else:
-            payload = self.decode(payload)
-
-        self.factory.on_message(self, payload, isBinary)
-
-    def onClose(self, wasClean, code, reason):
-        self.factory.unregister_client(self, reason="connection closed")
-        LOG.info("WebSocket connection closed: {0}".format(reason))
-        ip = self.peer.split(":")[1]
-        data = {"ip": ip, "code": code, "reason": "connection closed",
-                "wasClean": wasClean}
-        context = {"source": self.peer}
-        self.factory.mycroft_send("hive.client.disconnect", data, context)
-
-    def connectionLost(self, reason):
-        """
-       Client lost connection, either disconnected or some error.
-       Remove client from list of tracked connections.
-       """
-        self.factory.unregister_client(self, reason="connection lost")
-        LOG.info("WebSocket connection lost: {0}".format(reason))
-        ip = self.peer.split(":")[1]
-        data = {"ip": ip, "reason": "connection lost"}
-        context = {"source": self.peer}
-        self.factory.mycroft_send("hive.client.disconnect", data, context)
-
-    def decode(self, payload):
-        payload = payload.decode("utf-8")
-        if self.crypto_key:
-            if "ciphertext" in payload:
-                payload = decrypt_from_json(self.crypto_key, payload)
-            else:
-                LOG.warning("Message was unencrypted")
-        if isinstance(payload, str):
-            payload = json.loads(payload)
-        return HiveMessage(**payload)
-
-    def sendMessage(self,
-                    payload,
-                    isBinary=False,
-                    fragmentSize=None,
-                    sync=False,
-                    doNotCompress=False):
-        if isinstance(payload, dict):
-            payload = json.dumps(payload)
-        if self.crypto_key and not isBinary:
-            payload = encrypt_as_json(self.crypto_key, payload)
-        if isinstance(payload, str):
-            payload = bytes(payload, encoding="utf-8")
-        super().sendMessage(payload, isBinary,
-                            fragmentSize=fragmentSize,
-                            sync=sync,
-                            doNotCompress=doNotCompress)
-
-
-class HiveMind:
-    node_type = HiveMindNodeType.MIND
-
-    def __new__(cls, *args, **kwargs):
-        # this non sense is changing the base class
-        # this allows subclassing either from twisted or asyncio
-        # but you only know which at runtime
-        from jarbas_hive_mind.backends import WebSocketServerFactory
-        x = type(cls.__name__, (HiveMind, WebSocketServerFactory), {})
-        # print(x, x.__bases__)
-        return super(HiveMind, cls).__new__(x)
-
-    def __init__(self, bus=None, announce=True, *args, **kwargs):
-        super().__init__(*args, **kwargs)
-        # list of clients
-        self.listener = None
-        self.clients = {}
-        # ip block policy
-        self.ip_list = []
-        self.blacklist = True  # if False, ip_list is a whitelist
-        # mycroft_ws
-        if bus is False:
-            self.bus = FakeBus()
-        else:
-            self.bus = bus or get_mycroft_bus()
-        self.register_mycroft_messages()
-
-        self.interface = HiveMindMasterInterface(self)
-        self.announce = announce
-        self.presence = None
-
-    def start_announcing(self):
-        self.presence = LocalPresence(port=self.listener.port, ssl=self.listener.is_secure)
-        self.presence.start()
-
-    def bind(self, listener):
-        self.listener = listener
-        if self.announce:
-            self.start_announcing()
-
-    @property
-    def peer(self):
-        if self.listener:
-            return self.listener.peer
-        return None
-
-    @property
-    def node_id(self):
-        """ semi-unique id, only meaningful for the client that is
-        connected, other hive nodes do not know what node this is"""
-        return f"{self.peer}:{self.node_type}"
-
-    def mycroft_send(self, msg_type, data=None, context=None):
-        data = data or {}
-        context = context or {}
-        if "client_name" not in context:
-            context["client_name"] = self.protocol.platform
-        self.bus.emit(Message(msg_type, data, context))
-
-    def register_mycroft_messages(self):
-        self.bus.on("message", self.handle_outgoing_mycroft)
-        self.bus.on('hive.send', self.handle_send)
-
-    def shutdown(self):
-        if self.presence:
-            self.presence.stop()
-        self.bus.remove('message', self.handle_outgoing_mycroft)
-        self.bus.remove('hive.send', self.handle_send)
-
-    # websocket handlers
-    def handle_register(self, client, platform):
-        """ called before registering a client, subclasses can take
-        additional actions here """
-
-    def register_client(self, client, platform=None):
-        """
-       Add client to list of managed connections.
-       """
-        platform = platform or "unknown"
-        LOG.info("registering client: " + str(client.peer))
-        t, ip, sock = client.peer.split(":")
-        # see if ip address is blacklisted
-        if ip in self.ip_list and self.blacklist:
-            LOG.warning("Blacklisted ip tried to connect: " + ip)
-            self.unregister_client(client, reason="Blacklisted ip")
-            return
-        # see if ip address is whitelisted
-        elif ip not in self.ip_list and not self.blacklist:
-            LOG.warning("Unknown ip tried to connect: " + ip)
-            #  if not whitelisted kick
-            self.unregister_client(client, reason="Unknown ip")
-            return
-        self.clients[client.peer] = {"instance": client,
-                                     "status": "connected",
-                                     "platform": platform}
-        self.handle_register(client, platform)
-
-    def handle_unregister(self, client, code, reason, context):
-        """ called before unregistering a client, subclasses can take
-        additional actions here """
-
-    def unregister_client(self, client, code=3078,
-                          reason="unregister client request"):
-        """
-       Remove client from list of managed connections.
-       """
-
-        LOG.info("deregistering client: " + str(client.peer))
-        if client.peer in self.clients.keys():
-            client_data = self.clients[client.peer] or {}
-            j, ip, sock_num = client.peer.split(":")
-            context = {"user": client_data.get("names", ["unknown_user"])[0],
-                       "source": client.peer}
-            self.handle_unregister(client, code, reason, context)
-            self.bus.emit(
-                Message("hive.client.disconnect",
-                        {"reason": reason, "ip": ip, "sock": sock_num},
-                        context))
-            client.sendClose(code, reason)
-            self.clients.pop(client.peer)
-
-    def handle_binary_message(self, client, payload):
-        """ binary data handler, can be for example an audio stream """
-
-    def handle_message(self, message, client):
-        """ message handler for non default message types, subclasses can
-        handle their own types here
-
-        data contains payload, msg_type, source_peer, route
-
-        message (HiveMessage): HiveMind message object
-        """
-
-    def on_message(self, client, message, isBinary):
-        """
-        message (HiveMessage): HiveMind message object
-
-       Process message from client, decide what to do internally here
-       """
-        client_protocol, ip, sock_num = client.peer.split(":")
-
-        if isBinary:
-            self.handle_binary_message(client, message)
-        else:
-            # update internal peer ID
-            message.update_source_peer(client.peer)
-            message.add_target_peer(self.peer)
-            message.update_hop_data()
-
-            # mycroft Message handlers
-            if message.msg_type == HiveMessageType.BUS:
-                self.handle_bus_message(message, client)
-            elif message.msg_type == HiveMessageType.SHARED_BUS:
-                self.handle_client_bus(message.payload, client)
-
-            # HiveMessage handlers
-            elif message.msg_type == HiveMessageType.PROPAGATE:
-                self.handle_propagate_message(message, client)
-            elif message.msg_type == HiveMessageType.BROADCAST:
-                self.handle_broadcast_message(message, client)
-            elif message.msg_type == HiveMessageType.ESCALATE:
-                self.handle_escalate_message(message, client)
-            else:
-                self.handle_message(message, client)
-
-    # HiveMind protocol messages -  from slave -> master
-    def handle_bus_message(self, message, client):
-        self.handle_incoming_mycroft(message.payload, client)
-
-    def handle_broadcast_message(self, message, client):
-        """
-        message (HiveMessage): HiveMind message object
-        """
-        # Slaves are not allowed to broadcast, by definition broadcast goes
-        # downstream only, use propagate instead
-        LOG.debug("Ignoring broadcast message from downstream, illegal action")
-        # TODO kick client for misbehaviour so it stops doing that?
-
-    def handle_propagate_message(self, message, client):
-        """
-        message (HiveMessage): HiveMind message object
-        """
-        LOG.debug("ROUTE: " + str(message.route))
-        LOG.debug("PAYLOAD_TYPE: " + message.payload.msg_type)
-        LOG.debug("PAYLOAD: " + str(message.payload.payload))
-
-        # unpack message
-        pload = message.payload
-        pload.replace_route(message.route)
-        self.interface.propagate(pload)
-
-    def handle_escalate_message(self, message, client):
-        """
-        message (HiveMessage): HiveMind message object
-        """
-        LOG.info("Received escalate message at: " + self.node_id)
-        LOG.debug("ROUTE: " + str(message.route))
-        LOG.debug("PAYLOAD_TYPE: " + message.payload.msg_type)
-        LOG.debug("PAYLOAD: " + str(message.payload.payload))
-
-        # unpack message
-        pload = message.payload
-        pload.replace_route(message.route)
-        self.interface.escalate(pload)
-
-    # HiveMind mycroft bus messages -  from slave -> master
-    def handle_incoming_mycroft(self, message, client):
-        """
-        message (Message): mycroft bus message object
-        """
-        # A Slave wants to inject a message in internal mycroft bus
-        # You are a Master, authorize bus message
-
-        client_protocol, ip, sock_num = client.peer.split(":")
-
-        # messages/skills/intents per user
-        if message.msg_type in client.blacklist.get("messages", []):
-            LOG.warning(client.peer + " sent a blacklisted message "
-                                      "type: " + message.msg_type)
-            return
-        # TODO check intent / skill that will trigger
-
-        # send client message to internal mycroft bus
-        LOG.info("Forwarding message to mycroft bus from client: " +
-                 str(client.peer))
-        message.context["peer"] = message.context["source"] = client.peer
-        self.mycroft_send(message.msg_type, message.data, message.context)
-
-    def handle_client_bus(self, message, client):
-        # this message is going inside the client bus
-        # take any metrics you need
-        LOG.info("Monitoring bus from client: " + client.peer)
-        assert isinstance(message, Message)
-
-    # mycroft handlers  - from master -> slave
-    def handle_send(self, message):
-        # mycroft wants to send a HiveMessage
-        # a device can be both a master and a slave, ocasionally the
-        # protocol messages each process can't handle will be emitted to the
-        # bus, the other process can then listen to them
-        payload = message.data.get("payload")
-        peer = message.data.get("peer")
-        msg_type = message.data["msg_type"]
-
-        if msg_type == HiveMessageType.PROPAGATE:
-            self.interface.propagate(payload)
-
-        elif msg_type == HiveMessageType.BROADCAST:
-            self.interface.broadcast(payload)
-
-        elif msg_type == HiveMessageType.ESCALATE:
-            # only slaves can escalate, ignore silently
-            # if this device is also a slave to something,
-            # the slave service will also react to this message
-            # and handle the escalate request
-            pass
-
-        # NOT a protocol specific message, send directly to requested peer
-        elif peer:
-            if peer in self.clients:
-                # send message to client
-                client = self.clients[peer].get("instance")
-                self.interface.send(payload, client)
-            else:
-                LOG.error("That client is not connected")
-                self.mycroft_send("hive.client.send.error",
-                                  {"error": "That client is not connected",
-                                   "peer": peer}, message.context)
-
-    def handle_outgoing_mycroft(self, message=None):
-        # forward internal messages to clients if they are the target
-        if isinstance(message, dict):
-            message = json.dumps(message)
-        if isinstance(message, str):
-            message = Message.deserialize(message)
-        if message.msg_type == "complete_intent_failure":
-            message.msg_type = "hive.complete_intent_failure"
-        message.context = message.context or {}
-        peers = message.context.get("destination") or []
-
-        if not isinstance(peers, list):
-            peers = [peers]
-
-        for peer in peers:
-            if peer and peer in self.clients:
-                client = self.clients[peer].get("instance")
-                msg = HiveMessage(HiveMessageType.BUS,
-                                  source_peer=self.peer,
-                                  payload=message)
-                self.interface.send(msg, client)
diff --git a/jarbas_hive_mind/nodes/slave.py b/jarbas_hive_mind/nodes/slave.py
deleted file mode 100644
index 8370f43..0000000
--- a/jarbas_hive_mind/nodes/slave.py
+++ /dev/null
@@ -1,109 +0,0 @@
-from jarbas_hive_mind.nodes.terminal import HiveMindTerminal, \
-    HiveMindTerminalProtocol
-from jarbas_hive_mind.message import HiveMessageType, HiveMessage
-from jarbas_hive_mind.nodes import HiveMindNodeType
-from ovos_utils.log import LOG
-from ovos_utils.messagebus import Message, get_mycroft_bus
-import json
-
-
-class HiveMindSlaveProtocol(HiveMindTerminalProtocol):
-    platform = "HiveMindSlaveV0.2"
-
-    @property
-    def bus(self):
-        return self.factory.bus
-
-    def onConnect(self, response):
-        super().onConnect(response)
-        self.bus.emit(Message("hive.mind.connected",
-                              {"server_id": response.headers["server"]}))
-
-    def onOpen(self):
-        super().onOpen()
-        self.bus.emit(Message("hive.mind.websocket.open"))
-
-    def onClose(self, wasClean, code, reason):
-        super().onClose(wasClean, code, reason)
-        self.bus.emit(Message("hive.mind.client.closed",
-                              {"wasClean": wasClean,
-                               "reason": reason,
-                               "code": code}))
-
-
-class HiveMindSlave(HiveMindTerminal):
-    protocol = HiveMindSlaveProtocol
-    node_type = HiveMindNodeType.SLAVE
-
-    def __init__(self, bus=None, *args, **kwargs):
-        super(HiveMindSlave, self).__init__(*args, **kwargs)
-        # mycroft_ws
-        self.bus = bus or get_mycroft_bus()
-        self.register_mycroft_messages()
-
-    # mycroft methods
-    def register_mycroft_messages(self):
-        self.bus.on("message", self.handle_outgoing_mycroft)
-        self.bus.on("hive.send", self.handle_send)
-
-    def shutdown(self):
-        self.bus.remove("message", self.handle_outgoing_mycroft)
-        self.bus.remove("hive.send", self.handle_send)
-
-    def handle_send(self, message):
-        msg_type = message.data["msg_type"]
-        pload = message.data["payload"]
-
-        if msg_type == HiveMessageType.BUS:
-            self.send_to_hivemind_bus(pload)
-        elif msg_type == HiveMessageType.PROPAGATE:
-            self.interface.propagate(pload)
-        elif msg_type == HiveMessageType.BROADCAST:
-            # Ignore silently, if a Master is connected to bus it will
-            # handle it
-            pass
-        elif msg_type == HiveMessageType.ESCALATE:
-            self.interface.escalate(pload)
-        else:
-            LOG.error("Unknown HiveMind protocol msg_type")
-
-    def handle_outgoing_mycroft(self, message=None):
-        if not self.client:
-            return  # not connected to hivemind yet
-
-        # forward internal messages to connections if they are the target
-        if isinstance(message, dict):
-            message = json.dumps(message)
-        if isinstance(message, str):
-            message = Message.deserialize(message)
-        if message.msg_type == "complete_intent_failure":
-            message.msg_type = "hive.complete_intent_failure"
-
-        message.context = message.context or {}
-        message.context["source"] = self.client.peer
-        peer = message.context.get("destination")
-        if peer and peer == self.client.peer:
-            msg = HiveMessage(HiveMessageType.BUS,
-                              source_peer=self.client.peer,
-                              payload=message.serialize())
-            self.interface.send(msg)
-
-    # parsed protocol messages
-    def handle_incoming_mycroft(self, payload):
-        """ HiveMind is sending a mycroft bus message"""
-        # you are a slave_connection, just signal it in the bus
-        self.bus.emit(Message("hive.message.received", payload))
-        # and then actually inject it no questions asked
-        # TODO make behaviour configurable
-        super().handle_incoming_message(payload)
-
-    # websocket handlers
-    def on_binary(self, payload):
-        # TODO receive binary file
-        LOG.info("[BINARY MESSAGE]")
-
-    def send_to_hivemind_bus(self, payload):
-        super().send_to_hivemind_bus(payload)
-        self.bus.emit(Message("hive.message.sent",
-                              {"payload": payload}))
-
diff --git a/jarbas_hive_mind/nodes/terminal.py b/jarbas_hive_mind/nodes/terminal.py
deleted file mode 100644
index df474bf..0000000
--- a/jarbas_hive_mind/nodes/terminal.py
+++ /dev/null
@@ -1,223 +0,0 @@
-from ovos_utils.log import LOG
-from jarbas_hive_mind.message import HiveMessage, HiveMessageType
-from jarbas_hive_mind.utils import encrypt_as_json, decrypt_from_json
-from jarbas_hive_mind.interface import HiveMindSlaveInterface
-from jarbas_hive_mind.nodes import HiveMindNodeType
-from ovos_utils.messagebus import Message
-import json
-
-
-class HiveMindTerminalProtocol:
-    platform = "HiveMindTerminalv0.2"
-
-    def __new__(cls, *args, **kwargs):
-        # this non sense is changing the base class
-        # this allows subclassing either from twisted or asyncio
-        # but you only know which at runtime
-        from jarbas_hive_mind.backends import WebSocketClientProtocol
-        x = type(cls.__name__, (HiveMindTerminalProtocol, WebSocketClientProtocol), {})
-        # print(x, x.__bases__)
-        return super(HiveMindTerminalProtocol, cls).__new__(x)
-
-    def onConnect(self, response):
-        LOG.info(f"HiveMind connected: {response.peer}")
-        self.factory.client = self
-        self.factory.status = "connected"
-
-    def onOpen(self):
-        LOG.info("HiveMind websocket connection open. ")
-
-    def onMessage(self, payload, isBinary):
-        if not isBinary:
-            payload = self.decode(payload)
-            data = {"payload": payload, "isBinary": isBinary}
-        else:
-            data = {"payload": None, "isBinary": isBinary}
-        self.factory.handle_incoming_message(data)
-
-    def onClose(self, wasClean, code, reason):
-        LOG.warning(f"HiveMind websocket connection closed: {reason}")
-        self.factory.client = None
-        self.factory.status = "disconnected"
-        if "WebSocket connection upgrade failed" in reason:
-            # key rejected
-            LOG.error("Key rejected")
-            # raise UnauthorizedKeyError
-
-        elif self.factory.connection.is_secure:
-            if "WebSocket opening handshake timeout" in reason:
-                LOG.error("SecureConnectionFailed: " + reason)
-                # raise SecureConnectionFailed
-
-        else:
-            LOG.error("ConnectionError")
-
-    def decode(self, payload):
-        payload = payload.decode("utf-8")
-        if self.factory.crypto_key:
-            payload = decrypt_from_json(self.factory.crypto_key, payload)
-        if isinstance(payload, str):
-            payload = json.loads(payload)
-        return HiveMessage(**payload)
-
-    def sendMessage(self,
-                    payload,
-                    isBinary=False,
-                    fragmentSize=None,
-                    sync=False,
-                    doNotCompress=False):
-        if self.factory.crypto_key and not isBinary:
-            payload = encrypt_as_json(self.factory.crypto_key, payload)
-        if isinstance(payload, dict):
-            payload = json.dumps(payload)
-        if isinstance(payload, str):
-            payload = bytes(payload, encoding="utf-8")
-        super().sendMessage(payload, isBinary, fragmentSize=fragmentSize,
-                            sync=sync, doNotCompress=doNotCompress)
-
-
-class HiveMindTerminal:
-    protocol = HiveMindTerminalProtocol
-    node_type = HiveMindNodeType.TERMINAL
-    announce = False
-
-    def __new__(cls, *args, **kwargs):
-        # this non sense is changing the base class
-        # this allows subclassing either from twisted or asyncio
-        # but you only know which at runtime
-        from jarbas_hive_mind.backends import WebSocketClientFactory
-        x = type(cls.__name__, (HiveMindTerminal, WebSocketClientFactory), {})
-        # print(x, x.__bases__)
-        return super(HiveMindTerminal, cls).__new__(x)
-
-    def __init__(self, crypto_key=None, connection=None,
-                 auto_reconnect=False, *args, **kwargs):
-        super(HiveMindTerminal, self).__init__(*args, **kwargs)
-        self.status = "disconnected"
-        self.client = None
-        self.crypto_key = crypto_key
-        self.connection = None
-        self.interface = HiveMindSlaveInterface(self)
-        self.auto_reconnect = auto_reconnect
-        self.upnp_server = None
-        self.ssdp = None
-        if connection:
-            self.bind(connection)
-
-    def bind(self, connection):
-        self.connection = connection
-
-    @property
-    def node_id(self):
-        """ semi-unique id, only meaningful for the master the node is
-        connected to, other hive nodes do not know what node this is,
-        only the master recognizes this uid """
-        if not self.peer:
-            # not yet connected
-            return f"{self.__class__.__name__}" \
-                   f".{HiveMindNodeType.CANDIDATE_NODE}:{self.node_type}"
-        return f"{self.peer}:{self.node_type}"
-
-    @property
-    def peer(self):
-        if self.client:
-            return self.client.peer
-        return None
-
-    @property
-    def master(self):
-        return self.connection.peer
-
-    # parsed protocol messages
-    def handle_incoming_mycroft(self, message):
-        assert isinstance(message, Message)
-        LOG.debug("[Mycroft Message] " + message.serialize())
-
-    # HiveMind protocol messages - from UPstream
-    def handle_bus_message(self, payload):
-        # Generate mycroft Message
-        message = payload
-        if isinstance(payload, HiveMessage):
-            payload = payload.payload
-        if isinstance(payload, str):
-            payload = json.loads(payload)
-        if isinstance(payload, dict):
-            payload = json.loads(payload)
-            msg_type = payload.get("msg_type") or payload["type"]
-            data = payload.get("data") or {}
-            context = payload.get("context") or {}
-            message = Message(msg_type, data, context)
-
-        assert isinstance(message, Message)
-        message.context["source"] = self.client.peer
-        message.context["destination"] = "skills"
-        if "platform" not in message.context:
-            message.context["platform"] = self.protocol.platform
-        self.handle_incoming_mycroft(message)
-
-    def handle_propagate_message(self, message):
-        LOG.info("Received propagate message at: " + self.node_id)
-        LOG.debug("ROUTE: " + str(message.route))
-        LOG.debug("PAYLOAD: " + str(message.payload))
-        self.interface.propagate(message.payload)
-
-    def handle_broadcast_message(self, message):
-        LOG.info("Received broadcast message at: " + self.node_id)
-        LOG.debug("ROUTE: " + str(message.route))
-        LOG.debug("PAYLOAD: " + str(message.payload))
-        self.interface.broadcast(message.payload)
-
-    def handle_escalate_message(self, message):
-        # only Slaves are allowed to escalate, by definition escalate
-        # goes upstream only
-        LOG.debug("Ignoring escalate message from upstream, illegal action")
-
-    def handle_incoming_message(self, data):
-        payload = data.get("payload")
-        if data.get("isBinary"):
-            self.on_binary(payload)
-        else:
-            self.on_message(payload)
-
-    # websocket handlers
-    def send_to_hivemind_bus(self, payload):
-        if isinstance(payload, Message):
-            payload = payload.serialize()
-        self.interface.send_to_hivemind_bus(payload)
-        return payload
-
-    def sendBinary(self, payload):
-        if self.client is None:
-            LOG.error("Client is none")
-            return
-        self.client.sendMessage(payload, isBinary=True)
-
-    def sendMessage(self, payload):
-        if self.client is None:
-            LOG.error("Client is none")
-            return
-        self.client.sendMessage(payload, isBinary=False)
-
-    def on_binary(self, payload):
-        LOG.info("[BINARY MESSAGE]")
-
-    def on_message(self, payload):
-        msg = payload
-        if isinstance(payload, str):
-            payload = json.loads(payload)
-        if isinstance(payload, dict):
-            msg = HiveMessage(**payload)
-
-        assert isinstance(msg, HiveMessage)
-
-        # Parse hive protocol
-        if msg.msg_type == HiveMessageType.BUS:
-            self.handle_bus_message(msg.payload)
-        elif msg.msg_type == HiveMessageType.PROPAGATE:
-            self.handle_propagate_message(msg)
-        elif msg.msg_type == HiveMessageType.BROADCAST:
-            self.handle_broadcast_message(msg)
-        elif msg.msg_type == HiveMessageType.ESCALATE:
-            self.handle_escalate_message(msg)
-        else:
-            LOG.error("Unknown HiveMind protocol msg_type")
diff --git a/jarbas_hive_mind/settings.py b/jarbas_hive_mind/settings.py
deleted file mode 100644
index 550a0c7..0000000
--- a/jarbas_hive_mind/settings.py
+++ /dev/null
@@ -1,19 +0,0 @@
-from os import makedirs
-from os.path import isdir, join
-from ovos_utils.xdg_utils import xdg_data_home
-
-DATA_PATH = join(xdg_data_home(), "jarbasHiveMind")
-if not isdir(DATA_PATH):
-    makedirs(DATA_PATH)
-
-DEFAULT_PORT = 5678
-USE_SSL = True
-
-LOG_BLACKLIST = []
-
-MYCROFT_WEBSOCKET_CONFIG = {
-    "host": "0.0.0.0",
-    "port": 8181,
-    "route": "/core",
-    "ssl": False
-}
diff --git a/jarbas_hive_mind/utils/__init__.py b/jarbas_hive_mind/utils/__init__.py
deleted file mode 100644
index b9c4ec9..0000000
--- a/jarbas_hive_mind/utils/__init__.py
+++ /dev/null
@@ -1,165 +0,0 @@
-import logging
-import random
-from os import makedirs
-from os.path import exists, join
-from socket import gethostname
-
-from OpenSSL import crypto
-import json
-from jarbas_hive_mind.settings import LOG_BLACKLIST, DATA_PATH
-from jarbas_hive_mind.exceptions import DecryptionKeyError, EncryptionKeyError
-from ovos_utils.log import LOG
-from ovos_utils.security import encrypt, decrypt
-from binascii import hexlify, unhexlify
-
-# TODO ovos_utils for all of these
-# this used to be a method here, keep here for now in case something is
-# importing it, TODO deprecate
-from ovos_utils import get_ip
-
-
-def validate_param(value, name):
-    if not value:
-        raise ValueError("Missing or empty %s in conf " % name)
-
-
-def create_self_signed_cert(cert_dir=join(DATA_PATH, "certs"), name="HiveMind"):
-    """
-    If name.crt and name.key don't exist in cert_dir, create a new
-    self-signed cert and key pair and write them into that directory.
-    """
-
-    CERT_FILE = name + ".crt"
-    KEY_FILE = name + ".key"
-    cert_path = join(cert_dir, CERT_FILE)
-    key_path = join(cert_dir, KEY_FILE)
-
-    if not exists(join(cert_dir, CERT_FILE)) \
-            or not exists(join(cert_dir, KEY_FILE)):
-        # create a key pair
-        k = crypto.PKey()
-        k.generate_key(crypto.TYPE_RSA, 2048)
-
-        # create a self-signed cert
-        cert = crypto.X509()
-        cert.get_subject().C = "PT"
-        cert.get_subject().ST = "Europe"
-        cert.get_subject().L = "Mountains"
-        cert.get_subject().O = "Jarbas AI"
-        cert.get_subject().OU = "Powered by Mycroft-Core"
-        cert.get_subject().CN = gethostname()
-        cert.set_serial_number(random.randint(0, 2000))
-        cert.gmtime_adj_notBefore(0)
-        cert.gmtime_adj_notAfter(10 * 365 * 24 * 60 * 60)
-        cert.set_issuer(cert.get_subject())
-        cert.set_pubkey(k)
-        cert.sign(k, 'sha1')
-        if not exists(cert_dir):
-            makedirs(cert_dir)
-        open(cert_path, "wb").write(
-            crypto.dump_certificate(crypto.FILETYPE_PEM, cert))
-        open(join(cert_dir, KEY_FILE), "wb").write(
-            crypto.dump_privatekey(crypto.FILETYPE_PEM, k))
-
-    return cert_path, key_path
-
-
-def create_echo_function(name, whitelist=None):
-    """ Standard logging mechanism for Mycroft processes.
-
-    This handles the setup of the basic logging for all Mycroft
-    messagebus-based processes.
-
-    Args:
-        name (str): Reference name of the process
-        whitelist (list, optional): List of "type" strings.  If defined, only
-                                    messages in this list will be logged.
-
-    Returns:
-        func: The echo function
-    """
-    blacklist = LOG_BLACKLIST
-
-    def echo(message):
-        global _log_all_bus_messages
-        try:
-            msg = json.loads(message)
-
-            if whitelist and msg.get("type") not in whitelist:
-                return
-
-            if blacklist and msg.get("type") in blacklist:
-                return
-
-            if msg.get("type") == "mycroft.debug.log":
-                # Respond to requests to adjust the logger settings
-                lvl = msg["data"].get("level", "").upper()
-                if lvl in ["CRITICAL", "ERROR", "WARNING", "INFO", "DEBUG"]:
-                    LOG.level = lvl
-                    LOG(name).info("Changing log level to: {}".format(lvl))
-                    try:
-                        logging.getLogger('urllib3').setLevel(lvl)
-                    except Exception:
-                        pass  # We don't really care about if this fails...
-
-                # Allow enable/disable of messagebus traffic
-                log_bus = msg["data"].get("bus", None)
-                if log_bus is not None:
-                    LOG(name).info("Bus logging: " + str(log_bus))
-                    _log_all_bus_messages = log_bus
-            elif msg.get("type") == "registration":
-                # do not log tokens from registration messages
-                msg["data"]["token"] = None
-                message = json.dumps(msg)
-        except Exception:
-            pass
-
-        if _log_all_bus_messages:
-            # Listen for messages and echo them for logging
-            LOG(name).debug(message)
-
-    return echo
-
-
-def serialize_message(message):
-    # convert a Message object into raw data that can be sent over
-    # websocket
-    if hasattr(message, 'serialize'):
-        return message.serialize()
-    elif isinstance(message, dict):
-        message = {k: v if not hasattr(v, 'serialize') else serialize_message(v)
-                   for k, v in message.items()}
-        return json.dumps(message)
-    else:
-        return json.dumps(message.__dict__)
-
-
-def encrypt_as_json(key, data, nonce=None):
-    if isinstance(data, dict):
-        data = json.dumps(data)
-    if len(key) > 16:
-        key = key[0:16]
-    try:
-        ciphertext, tag, nonce = encrypt(key, data, nonce=nonce)
-    except:
-        raise EncryptionKeyError
-    return json.dumps({"ciphertext": hexlify(ciphertext).decode('utf-8'),
-            "tag": hexlify(tag).decode('utf-8'),
-            "nonce": hexlify(nonce).decode('utf-8')})
-
-
-def decrypt_from_json(key, data):
-    if isinstance(data, str):
-        data = json.loads(data)
-    if len(key) > 16:
-        key = key[0:16]
-    ciphertext = unhexlify(data["ciphertext"])
-    if data.get("tag") is None:  # web crypto
-        ciphertext, tag = ciphertext[:-16], ciphertext[-16:]
-    else:
-        tag = unhexlify(data["tag"])
-    nonce = unhexlify(data["nonce"])
-    try:
-        return decrypt(key, ciphertext, tag, nonce)
-    except ValueError:
-        raise DecryptionKeyError
diff --git a/jarbas_hive_mind/utils/emulation.py b/jarbas_hive_mind/utils/emulation.py
deleted file mode 100644
index 0cb1e99..0000000
--- a/jarbas_hive_mind/utils/emulation.py
+++ /dev/null
@@ -1,73 +0,0 @@
-from jarbas_hive_mind import get_listener, get_connection
-from jarbas_hive_mind.nodes.slave import HiveMindSlave
-from jarbas_hive_mind.configuration import CONFIGURATION
-from ovos_utils.messagebus import FakeBus
-
-
-class FakeMycroft:
-
-    def __init__(self, port, key="dummy_key", config=None):
-        self.bus = FakeBus()
-        self.config = config or CONFIGURATION
-        self.port = port
-        self.user_agent = "fakeCroft:{n}".format(n=self.port)
-        self.key = key
-        # hive mind objects
-        self.hive = None
-        self.slave_connection = None
-        self.connection = None
-        self.listener = None
-
-    @property
-    def interface(self):
-        if self.hive_interface:
-            return self.hive_interface
-        elif self.slave_interface:
-            return self.slave_interface
-        else:
-            return None
-
-    @property
-    def hive_interface(self):
-        # the master can send requests downstream with this interface
-        if self.hive:
-            return self.hive.interface
-        return None
-
-    @property
-    def slave_interface(self):
-        # the slave_connection can send requests upstream with this interface
-        if self.slave_connection:
-            return self.slave_connection.interface
-        return None
-
-    def start_listener(self):
-        # listen
-        self.listener = get_listener(port=self.port,
-                                     bus=self.bus)
-
-        # this flag is not meant for the end user
-        # used here so the twisted reactor can be started manually
-        self.listener._autorun = False
-
-        # returns a HiveMind object
-        self.hive = self.listener.listen()
-
-    def connect(self, port, host="0.0.0.0"):
-        self.connection = get_connection(host, port)
-
-        # this flag is not meant for the end user
-        # used here so the twisted reactor can be started manually
-        self.connection._autorun = False
-
-        con = HiveMindSlave(bus=self.bus,
-                            headers=self.connection.get_headers(self.user_agent,
-                                                                self.key),
-                            useragent=self.user_agent)
-
-        # returns a HiveMindSlave object
-        self.slave_connection = self.connection.connect(con)
-
-    def run(self):
-        self.hive.run()
-
diff --git a/readme.md b/readme.md
index 95e57cc..8e312e1 100644
--- a/readme.md
+++ b/readme.md
@@ -2,25 +2,75 @@
   <img src="https://github.com/JarbasHiveMind/HiveMind-assets/raw/master/logo/hivemind-512.png">
 </p>
 
-HiveMind is a community-developed superset or extension of [Mycroft](https://www.github.com/MycroftAI/mycroft-core), the open-source voice assistant.
+HiveMind is a community-developed superset or extension of [OpenVoiceOS](https://github.com/OpenVoiceOS/), the open-source voice assistant.
 
-With HiveMind, you can extend one (or more, but usually just one!) instance of Mycroft to as many devices as you want, including devices that can't ordinarily run Mycroft!
+With HiveMind, you can extend one (or more, but usually just one!) instance of ovos-core to as many devices as you want, including devices that can't ordinarily run OpenVoiceOS!
 
-HiveMind's developers have successfully connected to Mycroft from a PinePhone, a 2009 MacBook, and a Raspberry Pi 0, among other devices. Mycroft itself usually runs on our desktop computers or our home servers, but you can use any Mycroft-branded device, or [OpenVoiceOS](https://github.com/OpenVoiceOS/), as your central unit.
+HiveMind's developers have successfully connected to OpenVoiceOS from a PinePhone, a 2009 MacBook, and a Raspberry Pi 0, among other devices. OpenVoiceOS itself usually runs on our desktop computers or our home servers, but you can use any OpenVoiceOS-branded device as your central unit.
 
-Work in progress documentation can be found in the [wiki](https://github.com/JarbasHiveMind/HiveMind-core/wiki)
+Work in progress documentation can be found in the [docs](https://jarbashivemind.github.io/HiveMind-community-docs)
 
 You can also join the [Hivemind Matrix chat](https://matrix.to/#/#jarbashivemind:matrix.org) for general news, support and chit chat
 
-# Stats:
+# Usage
 
-| [![GitHub stars](https://img.shields.io/github/stars/OpenJarbas/HiveMind-core.svg)](https://github.com/OpenJarbas/HiveMind-core/stargazers)  | [![GitHub last commit](https://img.shields.io/github/last-commit/OpenJarbas/HiveMind-core.svg)](https://github.com/OpenJarbas/HiveMind-core/commits/dev) |
-|:---:|:---:|
-| Please :star: this repo if you find it useful| This shows when this repo was updated for the last time |
-|[![License: Apache License 2.0](https://img.shields.io/crates/l/rustc-serialize.svg)](http://www.apache.org/licenses/LICENSE-2.0.html)| [![contributions welcome](https://img.shields.io/badge/contributions-welcome-blue.svg?style=flat)](https://github.com/OpenJarbas/HiveMind-core/pulls) |
-| I'm using the Apache License 2.0 which means commercial use is allowed | If you have any ideas, they're always welcome.  Either submit an issue or a PR! |
-| [![Buy me a](https://img.shields.io/badge/BuyMeABeer-Paypal-blue.svg)](https://www.paypal.me/AnaIsabelFerreira) | [![Sponsor](https://img.shields.io/badge/SponsorDevelopment-Liberapay-blue.svg)](https://liberapay.com/jarbasAI/) |
-| If you feel the need, now it's as easy as clicking this button!  | You can help sponsoring HiveMind continued development with recurring donations|
+```
+$ hivemind-core --help
+Usage: hivemind-core [OPTIONS] COMMAND [ARGS]...
+
+Options:
+  --help  Show this message and exit.
+
+Commands:
+  add-client     add credentials for a client
+  delete-client  remove credentials for a client
+  list-clients   list clients and credentials
+  listen         start listening for HiveMind connections
+
+
+$ hivemind-core add-client --help
+Usage: hivemind-core add-client [OPTIONS] [NAME] [ACCESS_KEY] [PASSWORD]
+                                [CRYPTO_KEY]
+
+  add credentials for a client
+
+Options:
+  --help  Show this message and exit.
+
+
+
+$ hivemind-core listen --help
+Usage: hivemind-core listen [OPTIONS]
+
+  start listening for HiveMind connections
+
+Options:
+  --host TEXT       HiveMind host
+  --port INTEGER    HiveMind port number
+  --ssl BOOLEAN     use wss://
+  --cert_dir TEXT   HiveMind SSL certificate directory
+  --cert_name TEXT  HiveMind SSL certificate file name
+  --help            Show this message and exit.
+
+
+$ hivemind-core delete-client --help
+Usage: hivemind-core delete-client [OPTIONS] NODE_ID
+
+  remove credentials for a client
+
+Options:
+  --help  Show this message and exit.
+
+
+$ hivemind-core list-clients --help
+Usage: hivemind-core list-clients [OPTIONS]
+
+  list clients and credentials
+
+Options:
+  --help  Show this message and exit.
+
+```
 
 # HiveMind components
 
diff --git a/requirements.txt b/requirements.txt
index 6b98d0b..a81f994 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,8 +1,9 @@
-pyopenssl
-service_identity
-autobahn
-ovos_utils>=0.0.17
-json_database>=0.2.6
+tornado
+ovos_utils>=0.0.33
 pycryptodomex
 HiveMind_presence~=0.0.2a2
-mycroft-messagebus-client>=0.9.1
+ovos-bus-client
+poorman_handshake>=0.1.0
+click
+click_default_group
+rich
\ No newline at end of file
diff --git a/setup.py b/setup.py
index 38e4020..e5bd41e 100644
--- a/setup.py
+++ b/setup.py
@@ -7,7 +7,7 @@
 def get_version():
     """ Find the version of the package"""
     version = None
-    version_file = os.path.join(BASEDIR, 'jarbas_hive_mind', 'version.py')
+    version_file = os.path.join(BASEDIR, 'hivemind_core', 'version.py')
     major, minor, build, alpha = (None, None, None, None)
     with open(version_file) as f:
         for line in f:
@@ -43,22 +43,17 @@ def required(requirements_file):
 setup(
     name='jarbas_hive_mind',
     version=get_version(),
-    packages=['jarbas_hive_mind',
-              'jarbas_hive_mind.backends',
-              'jarbas_hive_mind.nodes',
-              'jarbas_hive_mind.configuration',
-              'jarbas_hive_mind.database',
-              'jarbas_hive_mind.utils'],
+    packages=['hivemind_core'],
     include_package_data=True,
     install_requires=required("requirements.txt"),
-    url='https://github.com/JarbasAl/hive_mind',
+    url='https://github.com/JarbasHiveMind/HiveMind-core',
     license='MIT',
     author='jarbasAI',
     author_email='jarbasai@mailfence.com',
-    description='Mesh Networking utilities for mycroft core',
+    description='Mesh Networking utilities for OpenVoiceOS',
     entry_points={
         'console_scripts': [
-            'HiveMind-listen=jarbas_hive_mind.__main__:main'
+            'hivemind-core=hivemind_core.scripts:hmcore_cmds'
         ]
     }
 )


Test patch:
diff --git a/.github/workflows/unit_tests.yml b/.github/workflows/unit_tests.yml
index 6ac4206..29e2011 100644
--- a/.github/workflows/unit_tests.yml
+++ b/.github/workflows/unit_tests.yml
@@ -28,6 +28,7 @@ jobs:
           python -m pip install build wheel
       - name: Install repo
         run: |
+          pip install git+https://github.com/JarbasHiveMind/hivemind_websocket_client@rewrite
           pip install .
       - name: Install test dependencies
         run: |
diff --git a/examples/testmind/broadcast/end.py b/examples/testmind/broadcast/end.py
deleted file mode 100644
index a857b20..0000000
--- a/examples/testmind/broadcast/end.py
+++ /dev/null
@@ -1,40 +0,0 @@
-from jarbas_hive_mind.utils.emulation import FakeMycroft
-from jarbas_hive_mind.message import HiveMessage, HiveMessageType
-from ovos_utils import create_daemon
-from time import sleep
-
-
-# Here is a minimal test hive mind
-#
-#           Master
-#         /       \
-#       Mid      Mid2
-#     /   \          \
-#  end    end2       end3
-#
-
-FakeCroft = FakeMycroft(30000)
-FakeCroft.connect(20000)
-
-
-def test_broadcast():
-    msg = HiveMessage(msg_type=HiveMessageType.THIRDPRTY,
-                      payload={"ping": "END"})
-    print(msg)
-
-    def broadcast_test():
-        while True:
-            sleep(5)
-            print("\nTESTING BROADCAST FROM end\n")
-            FakeCroft.interface.broadcast(msg)
-
-    create_daemon(broadcast_test)
-
-
-# send broadcast message every 5 seconds
-test_broadcast()
-
-FakeCroft.run()
-
-
-# Expected output - Nothing
diff --git a/examples/testmind/broadcast/end2.py b/examples/testmind/broadcast/end2.py
deleted file mode 100644
index 46c0531..0000000
--- a/examples/testmind/broadcast/end2.py
+++ /dev/null
@@ -1,40 +0,0 @@
-from jarbas_hive_mind.utils.emulation import FakeMycroft
-from jarbas_hive_mind.message import HiveMessage, HiveMessageType
-from ovos_utils import create_daemon
-from time import sleep
-
-
-# Here is a minimal test hive mind
-#
-#           Master
-#         /       \
-#       Mid      Mid2
-#     /   \          \
-#  end    end2       end3
-#
-
-FakeCroft = FakeMycroft(30001)
-FakeCroft.connect(20000)
-
-
-def test_broadcast():
-    msg = HiveMessage(msg_type=HiveMessageType.THIRDPRTY,
-                      payload={"ping": "END2"})
-    print(msg)
-
-    def broadcast_test():
-        while True:
-            sleep(5)
-            print("\nTESTING BROADCAST FROM end2\n")
-            FakeCroft.interface.broadcast(msg)
-
-    create_daemon(broadcast_test)
-
-
-# send broadcast message every 5 seconds
-test_broadcast()
-
-FakeCroft.run()
-
-
-# Expected output - Nothing
diff --git a/examples/testmind/broadcast/end3.py b/examples/testmind/broadcast/end3.py
deleted file mode 100644
index b53cbc6..0000000
--- a/examples/testmind/broadcast/end3.py
+++ /dev/null
@@ -1,40 +0,0 @@
-from jarbas_hive_mind.utils.emulation import FakeMycroft
-from jarbas_hive_mind.message import HiveMessage, HiveMessageType
-from ovos_utils import create_daemon
-from time import sleep
-
-
-# Here is a minimal test hive mind
-#
-#           Master
-#         /       \
-#       Mid      Mid2
-#     /   \          \
-#  end    end2       end3
-#
-
-FakeCroft = FakeMycroft(30002)
-FakeCroft.connect(20001)
-
-
-def test_broadcast():
-    msg = HiveMessage(msg_type=HiveMessageType.THIRDPRTY,
-                      payload={"ping": "END3"})
-    print(msg)
-
-    def broadcast_test():
-        while True:
-            sleep(5)
-            print("\nTESTING BROADCAST FROM end3\n")
-            FakeCroft.interface.broadcast(msg)
-
-    create_daemon(broadcast_test)
-
-
-# send broadcast message every 5 seconds
-test_broadcast()
-
-FakeCroft.run()
-
-
-# Expected output - Nothing
diff --git a/examples/testmind/broadcast/master.py b/examples/testmind/broadcast/master.py
deleted file mode 100644
index 238db19..0000000
--- a/examples/testmind/broadcast/master.py
+++ /dev/null
@@ -1,57 +0,0 @@
-from jarbas_hive_mind.utils.emulation import FakeMycroft
-from jarbas_hive_mind.message import HiveMessage, HiveMessageType
-from ovos_utils import create_daemon
-from time import sleep
-
-
-# Here is a minimal test hive mind
-#
-#           Master
-#         /       \
-#       Mid      Mid2
-#     /   \          \
-#  end    end2       end3
-#
-
-
-FakeCroft = FakeMycroft(10000)
-FakeCroft.start_listener()
-
-
-def test_broadcast():
-    msg = HiveMessage(msg_type=HiveMessageType.THIRDPRTY,
-                      payload={"ping": "MASTER"})
-    print(msg)
-
-    def broadcast_test():
-        while True:
-            sleep(5)
-            print("\nTESTING BROADCAST FROM MASTER\n")
-            FakeCroft.interface.broadcast(msg)
-
-    create_daemon(broadcast_test)
-
-
-# send broadcast message every 5 seconds
-test_broadcast()
-
-FakeCroft.run()
-
-
-# Expected output
-
-# Mid + Mid2
-# 16:15:36.148 - jarbas_hive_mind.slave.terminal:handle_broadcast_message:137 - INFO - Received broadcast message at: tcp4:0.0.0.0:10000:SLAVE
-# 16:15:36.150 - jarbas_hive_mind.slave.terminal:handle_broadcast_message:138 - DEBUG - ROUTE: [{'source': 'tcp4:0.0.0.0:10000', 'targets': ['tcp4:127.0.0.1:36078', 'tcp4:127.0.0.1:36080']}]
-# 16:15:36.150 - jarbas_hive_mind.slave.terminal:handle_broadcast_message:139 - DEBUG - PAYLOAD: {'ping': 'pong'}
-
-
-# End  + End 2
-# 16:15:36.152 - jarbas_hive_mind.slave.terminal:handle_broadcast_message:137 - INFO - Received broadcast message at: tcp4:0.0.0.0:20000:SLAVE
-# 16:15:36.152 - jarbas_hive_mind.slave.terminal:handle_broadcast_message:138 - DEBUG - ROUTE: [{'source': 'tcp4:0.0.0.0:10000', 'targets': ['tcp4:127.0.0.1:36078', 'tcp4:127.0.0.1:36080']}, {'source': 'tcp4:0.0.0.0:20000', 'targets': ['tcp4:127.0.0.1:35356', 'tcp4:127.0.0.1:35358']}]
-# 16:15:36.153 - jarbas_hive_mind.slave.terminal:handle_broadcast_message:139 - DEBUG - PAYLOAD: {'ping': 'pong'}
-
-# End 3
-# 16:15:36.151 - jarbas_hive_mind.slave.terminal:handle_broadcast_message:137 - INFO - Received broadcast message at: tcp4:0.0.0.0:20001:SLAVE
-# 16:15:36.152 - jarbas_hive_mind.slave.terminal:handle_broadcast_message:138 - DEBUG - ROUTE: [{'source': 'tcp4:0.0.0.0:10000', 'targets': ['tcp4:127.0.0.1:36078', 'tcp4:127.0.0.1:36080']}, {'source': 'tcp4:0.0.0.0:20001', 'targets': ['tcp4:127.0.0.1:48152']}]
-# 16:15:36.153 - jarbas_hive_mind.slave.terminal:handle_broadcast_message:139 - DEBUG - PAYLOAD: {'ping': 'pong'}
\ No newline at end of file
diff --git a/examples/testmind/broadcast/mid.py b/examples/testmind/broadcast/mid.py
deleted file mode 100644
index 672fe5f..0000000
--- a/examples/testmind/broadcast/mid.py
+++ /dev/null
@@ -1,47 +0,0 @@
-from jarbas_hive_mind.utils.emulation import FakeMycroft
-from jarbas_hive_mind.message import HiveMessage, HiveMessageType
-from ovos_utils import create_daemon
-from time import sleep
-
-
-# Here is a minimal test hive mind
-#
-#           Master
-#         /       \
-#       Mid      Mid2
-#     /   \          \
-#  end    end2       end3
-#
-
-FakeCroft = FakeMycroft(20000)
-FakeCroft.start_listener()
-FakeCroft.connect(10000)
-
-
-def test_broadcast():
-    msg = HiveMessage(msg_type=HiveMessageType.THIRDPRTY,
-                      payload={"ping": "MID"})
-    print(msg)
-
-    def broadcast_test():
-        while True:
-            sleep(5)
-            print("\nTESTING BROADCAST FROM Mid\n")
-            FakeCroft.interface.broadcast(msg)
-
-    create_daemon(broadcast_test)
-
-
-# send broadcast message every 5 seconds
-test_broadcast()
-
-FakeCroft.run()
-
-
-# Expected output
-
-
-# End  + End 2
-# 16:15:36.152 - jarbas_hive_mind.slave.terminal:handle_broadcast_message:137 - INFO - Received broadcast message at: tcp4:0.0.0.0:20000:SLAVE
-# 16:15:36.152 - jarbas_hive_mind.slave.terminal:handle_broadcast_message:138 - DEBUG - ROUTE: [{'source': 'tcp4:0.0.0.0:20000', 'targets': ['tcp4:127.0.0.1:36078', 'tcp4:127.0.0.1:36080']}, {'source': 'tcp4:0.0.0.0:20000', 'targets': ['tcp4:127.0.0.1:35356', 'tcp4:127.0.0.1:35358']}]
-# 16:15:36.153 - jarbas_hive_mind.slave.terminal:handle_broadcast_message:139 - DEBUG - PAYLOAD: {'mid': 'pong'}
diff --git a/examples/testmind/broadcast/mid2.py b/examples/testmind/broadcast/mid2.py
deleted file mode 100644
index ac9d08e..0000000
--- a/examples/testmind/broadcast/mid2.py
+++ /dev/null
@@ -1,48 +0,0 @@
-from jarbas_hive_mind.utils.emulation import FakeMycroft
-from jarbas_hive_mind.message import HiveMessage, HiveMessageType
-from ovos_utils import create_daemon
-from time import sleep
-
-
-# Here is a minimal test hive mind
-#
-#           Master
-#         /       \
-#       Mid      Mid2
-#     /   \          \
-#  end    end2       end3
-#
-
-
-FakeCroft = FakeMycroft(20001)
-FakeCroft.start_listener()
-FakeCroft.connect(10000)
-
-
-def test_broadcast():
-    msg = HiveMessage(msg_type=HiveMessageType.THIRDPRTY,
-                      payload={"ping": "MID2"})
-    print(msg)
-
-    def broadcast_test():
-        while True:
-            sleep(5)
-            print("\nTESTING BROADCAST FROM Mid2\n")
-            FakeCroft.interface.broadcast(msg)
-
-    create_daemon(broadcast_test)
-
-
-# send broadcast message every 5 seconds
-test_broadcast()
-
-FakeCroft.run()
-
-
-# Expected output
-
-
-# End 3
-# 16:15:36.151 - jarbas_hive_mind.slave.terminal:handle_broadcast_message:137 - INFO - Received broadcast message at: tcp4:0.0.0.0:20001:SLAVE
-# 16:15:36.152 - jarbas_hive_mind.slave.terminal:handle_broadcast_message:138 - DEBUG - ROUTE: [{'source': 'tcp4:0.0.0.0:20001', 'targets': ['tcp4:127.0.0.1:36078', 'tcp4:127.0.0.1:36080']}, {'source': 'tcp4:0.0.0.0:20001', 'targets': ['tcp4:127.0.0.1:48152']}]
-# 16:15:36.153 - jarbas_hive_mind.slave.terminal:handle_broadcast_message:139 - DEBUG - PAYLOAD: {'mid2': 'pong'}
\ No newline at end of file
diff --git a/examples/testmind/escalate/end.py b/examples/testmind/escalate/end.py
deleted file mode 100644
index fe1b87c..0000000
--- a/examples/testmind/escalate/end.py
+++ /dev/null
@@ -1,54 +0,0 @@
-from jarbas_hive_mind.utils.emulation import FakeMycroft
-from jarbas_hive_mind.message import HiveMessage, HiveMessageType
-from ovos_utils import create_daemon
-from time import sleep
-
-
-# Here is a minimal test hive mind
-#
-#           Master
-#         /       \
-#       Mid      Mid2
-#     /   \          \
-#  end    end2       end3
-#
-
-FakeCroft = FakeMycroft(30000)
-FakeCroft.connect(20000)
-
-
-def test_escalate():
-    msg = HiveMessage(msg_type=HiveMessageType.THIRDPRTY,
-                      payload={"ping": "END"})
-
-    def escalate_test():
-        while True:
-            sleep(5)
-            print("\nTESTING escalate FROM end\n")
-            FakeCroft.interface.escalate(msg)
-
-    create_daemon(escalate_test)
-
-
-# send escalate message every 5 seconds
-test_escalate()
-
-FakeCroft.run()
-
-
-# Expected output
-
-
-# Mid
-"""
-INFO - Received escalate message at: tcp4:0.0.0.0:20000:MASTER
-DEBUG - ROUTE: [{'source': 'tcp4:127.0.0.1:39742', 'targets': ['tcp4:0.0.0.0:20000']}]
-DEBUG - PAYLOAD: {'ping': 'pong'}
-"""
-
-# Master
-"""
-INFO - Received escalate message at: tcp4:0.0.0.0:10000:MASTER
-DEBUG - ROUTE: [{'source': 'tcp4:127.0.0.1:39742', 'targets': ['tcp4:0.0.0.0:20000']}, {'source': 'tcp4:127.0.0.1:58498', 'targets': ['tcp4:0.0.0.0:10000']}]
-DEBUG - PAYLOAD: {'ping': 'pong'}
-"""
diff --git a/examples/testmind/escalate/end2.py b/examples/testmind/escalate/end2.py
deleted file mode 100644
index ed0cdf9..0000000
--- a/examples/testmind/escalate/end2.py
+++ /dev/null
@@ -1,53 +0,0 @@
-from jarbas_hive_mind.utils.emulation import FakeMycroft
-from jarbas_hive_mind.message import HiveMessage, HiveMessageType
-from ovos_utils import create_daemon
-from time import sleep
-
-
-# Here is a minimal test hive mind
-#
-#           Master
-#         /       \
-#       Mid      Mid2
-#     /   \          \
-#  end    end2       end3
-#
-
-FakeCroft = FakeMycroft(30001)
-FakeCroft.connect(20000)
-
-
-def test_escalate():
-    msg = HiveMessage(msg_type=HiveMessageType.THIRDPRTY,
-                      payload={"ping": "END2"})
-
-    def escalate_test():
-        while True:
-            sleep(5)
-            print("\nTESTING escalate FROM end2\n")
-            FakeCroft.interface.escalate(msg)
-
-    create_daemon(escalate_test)
-
-
-# send escalate message every 5 seconds
-test_escalate()
-
-FakeCroft.run()
-
-
-# Expected output
-
-# Mid
-"""
-INFO - Received escalate message at: tcp4:0.0.0.0:20000:MASTER
-DEBUG - ROUTE: [{'source': 'tcp4:127.0.0.1:39742', 'targets': ['tcp4:0.0.0.0:20000']}]
-DEBUG - PAYLOAD: {'ping': 'pong'}
-"""
-
-# Master
-"""
-INFO - Received escalate message at: tcp4:0.0.0.0:10000:MASTER
-DEBUG - ROUTE: [{'source': 'tcp4:127.0.0.1:39742', 'targets': ['tcp4:0.0.0.0:20000']}, {'source': 'tcp4:127.0.0.1:58498', 'targets': ['tcp4:0.0.0.0:10000']}]
-DEBUG - PAYLOAD: {'ping': 'pong'}
-"""
diff --git a/examples/testmind/escalate/end3.py b/examples/testmind/escalate/end3.py
deleted file mode 100644
index e71ac63..0000000
--- a/examples/testmind/escalate/end3.py
+++ /dev/null
@@ -1,53 +0,0 @@
-from jarbas_hive_mind.utils.emulation import FakeMycroft
-from jarbas_hive_mind.message import HiveMessage, HiveMessageType
-from ovos_utils import create_daemon
-from time import sleep
-
-
-# Here is a minimal test hive mind
-#
-#           Master
-#         /       \
-#       Mid      Mid2
-#     /   \          \
-#  end    end2       end3
-#
-
-FakeCroft = FakeMycroft(30002)
-FakeCroft.connect(20001)
-
-
-def test_escalate():
-    msg = HiveMessage(msg_type=HiveMessageType.THIRDPRTY,
-                      payload={"ping": "END3"})
-
-    def escalate_test():
-        while True:
-            sleep(5)
-            print("\nTESTING escalate FROM end3\n")
-            FakeCroft.interface.escalate(msg)
-
-    create_daemon(escalate_test)
-
-
-# send escalate message every 5 seconds
-test_escalate()
-
-FakeCroft.run()
-
-
-# Expected output
-
-# Mid2
-"""
-INFO - Received escalate message at: tcp4:0.0.0.0:20001:MASTER
-DEBUG - ROUTE: [{'source': 'tcp4:127.0.0.1:39742', 'targets': ['tcp4:0.0.0.0:20001']}]
-DEBUG - PAYLOAD: {'ping': 'pong'}
-"""
-
-# Master
-"""
-INFO - Received escalate message at: tcp4:0.0.0.0:10000:MASTER
-DEBUG - ROUTE: [{'source': 'tcp4:127.0.0.1:39742', 'targets': ['tcp4:0.0.0.0:20001']}, {'source': 'tcp4:127.0.0.1:58498', 'targets': ['tcp4:0.0.0.0:10000']}]
-DEBUG - PAYLOAD: {'ping': 'pong'}
-"""
diff --git a/examples/testmind/escalate/master.py b/examples/testmind/escalate/master.py
deleted file mode 100644
index 1e04e65..0000000
--- a/examples/testmind/escalate/master.py
+++ /dev/null
@@ -1,40 +0,0 @@
-from jarbas_hive_mind.utils.emulation import FakeMycroft
-from jarbas_hive_mind.message import HiveMessage, HiveMessageType
-from ovos_utils import create_daemon
-from time import sleep
-
-
-# Here is a minimal test hive mind
-#
-#           Master
-#         /       \
-#       Mid      Mid2
-#     /   \          \
-#  end    end2       end3
-#
-
-
-FakeCroft = FakeMycroft(10000)
-FakeCroft.start_listener()
-
-
-def test_escalate():
-    msg = HiveMessage(msg_type=HiveMessageType.THIRDPRTY,
-                      payload={"ping": "pong"})
-
-    def escalate_test():
-        while True:
-            sleep(5)
-            print("\nTESTING escalate FROM MASTER\n")
-            FakeCroft.interface.escalate(msg)
-
-    create_daemon(escalate_test)
-
-
-# send escalate message every 5 seconds
-test_escalate()
-
-FakeCroft.run()
-
-
-# Expected output - Nothing
diff --git a/examples/testmind/escalate/mid.py b/examples/testmind/escalate/mid.py
deleted file mode 100644
index f864ece..0000000
--- a/examples/testmind/escalate/mid.py
+++ /dev/null
@@ -1,48 +0,0 @@
-from jarbas_hive_mind.utils.emulation import FakeMycroft
-from jarbas_hive_mind.message import HiveMessage, HiveMessageType
-from ovos_utils import create_daemon
-from time import sleep
-
-
-# Here is a minimal test hive mind
-#
-#           Master
-#         /       \
-#       Mid      Mid2
-#     /   \          \
-#  end    end2       end3
-#
-
-FakeCroft = FakeMycroft(20000)
-FakeCroft.start_listener()
-FakeCroft.connect(10000)
-
-
-def test_escalate():
-    msg = HiveMessage(msg_type=HiveMessageType.THIRDPRTY,
-                      payload={"ping": "MID"})
-    print(msg)
-    def escalate_test():
-        while True:
-            sleep(5)
-            print("\nTESTING escalate FROM Mid\n")
-            FakeCroft.interface.escalate(msg)
-
-    create_daemon(escalate_test)
-
-
-# send escalate message every 5 seconds
-test_escalate()
-
-FakeCroft.run()
-
-
-# Expected output
-
-
-# Master
-"""
-INFO - Received escalate message at: tcp4:0.0.0.0:10000:MASTER
-DEBUG - ROUTE: [{'source': 'tcp4:127.0.0.1:58496', 'targets': ['tcp4:0.0.0.0:10000']}]
-DEBUG - PAYLOAD: {'mid': 'pong'}
-"""
\ No newline at end of file
diff --git a/examples/testmind/escalate/mid2.py b/examples/testmind/escalate/mid2.py
deleted file mode 100644
index 3c138a4..0000000
--- a/examples/testmind/escalate/mid2.py
+++ /dev/null
@@ -1,49 +0,0 @@
-from jarbas_hive_mind.utils.emulation import FakeMycroft
-from jarbas_hive_mind.message import HiveMessage, HiveMessageType
-from ovos_utils import create_daemon
-from time import sleep
-
-
-# Here is a minimal test hive mind
-#
-#           Master
-#         /       \
-#       Mid      Mid2
-#     /   \          \
-#  end    end2       end3
-#
-
-
-FakeCroft = FakeMycroft(20001)
-FakeCroft.start_listener()
-FakeCroft.connect(10000)
-
-
-def test_escalate():
-    msg = HiveMessage(msg_type=HiveMessageType.THIRDPRTY,
-                      payload={"ping": "MID2"})
-
-    def escalate_test():
-        while True:
-            sleep(5)
-            print("\nTESTING escalate FROM Mid2\n")
-            FakeCroft.interface.escalate(msg)
-
-    create_daemon(escalate_test)
-
-
-# send escalate message every 5 seconds
-test_escalate()
-
-FakeCroft.run()
-
-
-# Expected output
-
-
-# Master
-"""
-INFO - Received escalate message at: tcp4:0.0.0.0:10000:MASTER
-DEBUG - ROUTE: [{'source': 'tcp4:127.0.0.1:58496', 'targets': ['tcp4:0.0.0.0:10000']}]
-DEBUG - PAYLOAD: {'mid2': 'pong'}
-"""
\ No newline at end of file
diff --git a/test/license_tests.py b/test/license_tests.py
deleted file mode 100644
index 9acd600..0000000
--- a/test/license_tests.py
+++ /dev/null
@@ -1,58 +0,0 @@
-import unittest
-from pprint import pprint
-
-from lichecker import LicenseChecker
-
-# these packages dont define license in setup.py
-# manually verified and injected
-license_overrides = {
-    "kthread": "MIT",
-    'yt-dlp': "Unlicense",
-    'pyxdg': 'GPL-2.0',
-    'ptyprocess': 'ISC',
-    'psutil': 'BSD3',
-    'setuptools': "MIT"
-}
-# explicitly allow these packages that would fail otherwise
-whitelist = [
-    'typing-extensions',  # PSFL
-    'idna',  # BSD-like
-    'python-dateutil',  # 'Simplified BSD'
-    'cryptography',  # 'BSD or Apache License, Version 2.0',
-    'pycryptodomex',  # 'BSD, Public Domain',
-]
-
-# validation flags
-allow_nonfree = False
-allow_viral = False
-allow_unknown = False
-allow_unlicense = True
-allow_ambiguous = False
-
-pkg_name = "jarbas_hive_mind"
-
-
-class TestLicensing(unittest.TestCase):
-    licheck = LicenseChecker(pkg_name,
-                             license_overrides=license_overrides,
-                             whitelisted_packages=whitelist,
-                             allow_ambiguous=allow_ambiguous,
-                             allow_unlicense=allow_unlicense,
-                             allow_unknown=allow_unknown,
-                             allow_viral=allow_viral,
-                             allow_nonfree=allow_nonfree)
-
-    def test_license_compliance(self):
-        print("Package", pkg_name)
-        print("Version", self.licheck.version)
-        print("License", self.licheck.license)
-        print("Transient Requirements (dependencies of dependencies)")
-        pprint(self.licheck.transient_dependencies)
-
-        print("Package Versions")
-        pprint(self.licheck.versions)
-
-        print("Dependency Licenses")
-        pprint(self.licheck.licenses)
-
-        self.licheck.validate()
diff --git a/test/unittests/test_bus.py b/test/unittests/test_bus.py
new file mode 100644
index 0000000..9a97580
--- /dev/null
+++ b/test/unittests/test_bus.py
@@ -0,0 +1,800 @@
+import random
+from time import sleep
+from unittest import TestCase, skip
+
+from ovos_bus_client.message import Message
+from hivemind_core.database import ClientDatabase
+from hivemind_bus_client import HiveMessageType, HiveNodeClient, HiveMessage
+from jarbas_hive_mind.utils.emulation import FakeMycroft
+
+# TODO - rewrite tests
+
+def get_hive():
+    # TODO add/mock db for the test
+    key = "dummy_key"
+    crypto_key = "6e9941197be7f949"
+
+    with ClientDatabase() as db:
+        client = db.get_client_by_api_key(key)
+        if client:
+            db.change_crypto_key(key, crypto_key)
+        else:
+            n = db.total_clients()
+            name = f"HiveMind-UNITTESTS-Node-{n}"
+            db.add_client(name, key, crypto_key=crypto_key)
+
+    # Here is a minimal test hive mind
+    #
+    #           Master
+    #         /       \
+    #       Mid      Mid2
+    #     /   \          \
+    #  end    end2       end3
+    #
+    MASTER_PORT = 10000
+    MID_PORT = 20000
+    MID2_PORT = 20001
+    END_PORT = 30000
+    END2_PORT = 30001
+    END3_PORT = 30003
+
+    master = FakeMycroft(MASTER_PORT)
+    master.start()
+
+    sleep(1)
+
+    mid = FakeMycroft(MID_PORT, connection=HiveNodeClient(
+        key=key, crypto_key=crypto_key, port=MASTER_PORT, ssl=False))
+    mid.start()
+
+    mid2 = FakeMycroft(MID2_PORT, connection=HiveNodeClient(
+        key=key, crypto_key=crypto_key, port=MASTER_PORT, ssl=False))
+    mid2.start()
+
+    sleep(1)
+
+    end = FakeMycroft(END_PORT, connection=HiveNodeClient(
+        key=key, crypto_key=crypto_key, port=MID_PORT, ssl=False))
+    end.start()
+    end2 = FakeMycroft(END2_PORT, connection=HiveNodeClient(
+        key=key, crypto_key=crypto_key, port=MID_PORT, ssl=False))
+    end2.start()
+    end3 = FakeMycroft(END3_PORT, connection=HiveNodeClient(
+        key=key, crypto_key=crypto_key, port=MID2_PORT, ssl=False))
+    end3.start()
+
+    sleep(10)  # allow hive to fully connect
+
+    return [master, mid, mid2, end, end2, end3]
+
+
+master, mid, mid2, end, end2, end3 = get_hive()
+
+
+class TestConnections(TestCase):
+    def test_hello(self):
+        # assert node_ids received via "hello" message
+        self.assertEqual(mid.connection.node_id, master.hive.node_id)
+        self.assertEqual(mid2.connection.node_id, master.hive.node_id)
+        self.assertEqual(end.connection.node_id, mid.hive.node_id)
+        self.assertEqual(end2.connection.node_id, mid.hive.node_id)
+        self.assertEqual(end3.connection.node_id, mid2.hive.node_id)
+
+    def test_connections(self):
+        # assert clients connected to servers
+        self.assertTrue(mid.connection.peer in master.hive.clients)
+        self.assertTrue(mid2.connection.peer in master.hive.clients)
+        self.assertTrue(end.connection.peer in mid.hive.clients)
+        self.assertTrue(end2.connection.peer in mid.hive.clients)
+        self.assertTrue(end3.connection.peer in mid2.hive.clients)
+
+
+class TestHiveBus(TestCase):
+
+    def test_midtomaster(self):
+
+        #
+        #           Master
+        #          *
+        #         /
+        #       Mid      Mid2
+        #
+        #  end    end2       end3
+        #
+
+        rcv_master = []
+        rcv_mid = []
+        rcv_mid2 = []
+
+        def handle_master_bus(message):
+            rcv_master.append(message)
+
+        def handle_mid_bus(message):
+            rcv_mid.append(message)
+
+        def handle_mid2_bus(message):
+            rcv_mid2.append(message)
+
+        master.handle_downstream_bus = handle_master_bus
+        mid.handle_downstream_bus = handle_mid_bus
+        mid2.handle_downstream_bus = handle_mid2_bus
+        master.register_downstream_handlers()
+        mid.register_downstream_handlers()
+        mid2.register_downstream_handlers()
+
+        pload = HiveMessage(msg_type=HiveMessageType.BUS,
+                            payload=Message("test", {"ping": "pong"}))
+        mid.connection.emit(pload)
+        sleep(2)
+
+        # check that only master received the message
+        self.assertEqual(len(rcv_master), 1)
+        self.assertEqual(len(rcv_mid), 0)
+        self.assertEqual(len(rcv_mid2), 0)
+
+        # TODO debug where sometimes payload isnt unpacked
+        for message in rcv_master:
+            continue
+            self.assertEqual(message.msg_type, HiveMessageType.BUS)
+            self.assertTrue(isinstance(message.payload, Message))
+
+    def test_end3tomid2(self):
+        #
+        #           Master
+        #
+        #       Mid      Mid2
+        #                   *
+        #                    \
+        #  end    end2       end3
+        #
+        rcv_master = []
+        rcv_mid = []
+        rcv_mid2 = []
+
+        def handle_master_bus(message):
+            rcv_master.append(message)
+
+        def handle_mid_bus(message):
+            rcv_mid.append(message)
+
+        def handle_mid2_bus(message):
+            rcv_mid2.append(message)
+
+        master.handle_downstream_bus = handle_master_bus
+        mid.handle_downstream_bus = handle_mid_bus
+        mid2.handle_downstream_bus = handle_mid2_bus
+        master.register_downstream_handlers()
+        mid.register_downstream_handlers()
+        mid2.register_downstream_handlers()
+
+        pload = HiveMessage(msg_type=HiveMessageType.BUS,
+                            payload=Message("test", {"ping": "pong"}))
+        end3.connection.emit(pload)
+        sleep(2)
+
+        # check that only mid2 received the message
+        self.assertEqual(len(rcv_master), 0)
+        self.assertEqual(len(rcv_mid), 0)
+        self.assertEqual(len(rcv_mid2), 1)
+
+        # TODO debug where sometimes payload isnt unpacked
+        for message in rcv_mid2:
+            continue
+            self.assertEqual(message.msg_type, HiveMessageType.BUS)
+            self.assertTrue(isinstance(message.payload, Message))
+
+    def test_mastertomid(self):
+        #
+        #           Master
+        #         /
+        #        *
+        #       Mid      Mid2
+        #
+        #  end    end2       end3
+        #
+
+        rcv_mid = []
+        rcv_mid2 = []
+        rcv_end = []
+        rcv_end2 = []
+        rcv_end3 = []
+
+        def handle_mid_bus(message):
+            rcv_mid.append(message)
+
+        def handle_mid2_bus(message):
+            rcv_mid2.append(message)
+
+        def handle_end_bus(message):
+            rcv_end.append(message)
+
+        def handle_end2_bus(message):
+            rcv_end2.append(message)
+
+        def handle_end3_bus(message):
+            rcv_end3.append(message)
+
+        mid.handle_upstream_bus = handle_mid_bus
+        mid2.handle_upstream_bus = handle_mid2_bus
+        end.handle_upstream_bus = handle_end_bus
+        end2.handle_upstream_bus = handle_end2_bus
+        end3.handle_upstream_bus = handle_end3_bus
+        mid.register_upstream_handlers()
+        mid2.register_upstream_handlers()
+        end.register_upstream_handlers()
+        end2.register_upstream_handlers()
+        end2.register_upstream_handlers()
+
+        pload = HiveMessage(msg_type=HiveMessageType.BUS,
+                            payload=Message("test", {"ping": "pong"}))
+        master.interface.send(pload, mid.connection.peer)
+        sleep(0.5)
+
+        # check that only mid received the message
+        self.assertEqual(len(rcv_mid), 1)
+        self.assertEqual(len(rcv_mid2), 0)
+        self.assertEqual(len(rcv_end), 0)
+        self.assertEqual(len(rcv_end2), 0)
+        self.assertEqual(len(rcv_end3), 0)
+
+        for message in rcv_mid:
+            self.assertEqual(message.msg_type, HiveMessageType.BUS)
+            self.assertTrue(isinstance(message.payload, Message))
+
+    def test_midtoend(self):
+        #
+        #           Master
+        #
+        #       Mid      Mid2
+        #     /
+        #    *
+        #  end    end2       end3
+        #
+
+        rcv_mid = []
+        rcv_mid2 = []
+        rcv_end = []
+        rcv_end2 = []
+        rcv_end3 = []
+
+        def handle_mid_bus(message):
+            rcv_mid.append(message)
+
+        def handle_mid2_bus(message):
+            rcv_mid2.append(message)
+
+        def handle_end_bus(message):
+            rcv_end.append(message)
+
+        def handle_end2_bus(message):
+            rcv_end2.append(message)
+
+        def handle_end3_bus(message):
+            rcv_end3.append(message)
+
+        mid.handle_upstream_bus = handle_mid_bus
+        mid2.handle_upstream_bus = handle_mid2_bus
+        end.handle_upstream_bus = handle_end_bus
+        end2.handle_upstream_bus = handle_end2_bus
+        end3.handle_upstream_bus = handle_end3_bus
+        mid.register_upstream_handlers()
+        mid2.register_upstream_handlers()
+        end.register_upstream_handlers()
+        end2.register_upstream_handlers()
+        end2.register_upstream_handlers()
+
+        pload = HiveMessage(msg_type=HiveMessageType.BUS,
+                            payload=Message("test", {"ping": "pong"}))
+        mid.interface.send(pload, end.connection.peer)
+        sleep(0.5)
+
+        # check that only mid received the message
+        self.assertEqual(len(rcv_mid), 0)
+        self.assertEqual(len(rcv_mid2), 0)
+        self.assertEqual(len(rcv_end), 1)
+        self.assertEqual(len(rcv_end2), 0)
+        self.assertEqual(len(rcv_end3), 0)
+
+        for message in rcv_mid:
+            self.assertEqual(message.msg_type, HiveMessageType.BUS)
+            self.assertTrue(isinstance(message.payload, Message))
+
+
+class TestEscalate(TestCase):
+
+    def test_midtomaster(self):
+
+        #
+        #           Master
+        #          *
+        #         /
+        #       Mid      Mid2
+        #
+        #  end    end2       end3
+        #
+
+        rcv_master = []
+        rcv_mid = []
+        rcv_mid2 = []
+
+        def handle_master_escalate(message):
+            rcv_master.append(message)
+
+        def handle_mid_escalate(message):
+            rcv_mid.append(message)
+
+        def handle_mid2_escalate(message):
+            rcv_mid2.append(message)
+
+        master.handle_downstream_escalate = handle_master_escalate
+        mid.handle_downstream_escalate = handle_mid_escalate
+        mid2.handle_downstream_escalate = handle_mid2_escalate
+        master.register_downstream_handlers()
+        mid.register_downstream_handlers()
+        mid2.register_downstream_handlers()
+
+        pload = HiveMessage(msg_type=HiveMessageType.THIRDPRTY,
+                            payload=Message("test", {"ping": "pong"}))
+        pload = HiveMessage(msg_type=HiveMessageType.ESCALATE,  payload=pload)
+        mid.connection.emit(pload)
+        sleep(2)
+
+        # check that only master received the message
+        self.assertEqual(len(rcv_master), 1)
+        self.assertEqual(len(rcv_mid), 0)
+        self.assertEqual(len(rcv_mid2), 0)
+
+        # TODO debug where sometimes payload isnt unpacked
+        for message in rcv_master + rcv_mid + rcv_mid2:
+            print(message)
+            continue
+            self.assertEqual(message.msg_type, HiveMessageType.BUS)
+            self.assertTrue(isinstance(message.payload, Message))
+
+    def test_endtomaster(self):
+        #
+        #         Master
+        #        *
+        #       /
+        #     Mid      Mid2
+        #     *
+        #    /
+        #  end    end2      end3
+        #
+        rcv_master = []
+        rcv_mid = []
+        rcv_mid2 = []
+
+        def handle_master_escalate(message):
+            rcv_master.append(message)
+
+        def handle_mid_escalate(message):
+            rcv_mid.append(message)
+
+        def handle_mid2_escalate(message):
+            rcv_mid2.append(message)
+
+        master.handle_downstream_escalate = handle_master_escalate
+        mid.handle_downstream_escalate = handle_mid_escalate
+        mid2.handle_downstream_escalate = handle_mid2_escalate
+        master.register_downstream_handlers()
+        mid.register_downstream_handlers()
+        mid2.register_downstream_handlers()
+
+        pload = HiveMessage(msg_type=HiveMessageType.THIRDPRTY,
+                            payload=Message("test", {"ping": "pong"}))
+        pload = HiveMessage(msg_type=HiveMessageType.ESCALATE, payload=pload)
+        end.connection.emit(pload)
+        sleep(2)
+
+        # check that master and mid2received the message
+        self.assertEqual(len(rcv_master), 1)
+        self.assertEqual(len(rcv_mid), 1)
+        self.assertEqual(len(rcv_mid2), 0)
+
+        # TODO debug where sometimes payload isnt unpacked
+        for message in rcv_master + rcv_mid + rcv_mid2:
+            continue
+            self.assertEqual(message.msg_type, HiveMessageType.BUS)
+            self.assertTrue(isinstance(message.payload, Message))
+
+    def test_end3tomaster(self):
+        #
+        #           Master
+        #                *
+        #                 \
+        #       Mid      Mid2
+        #                   *
+        #                    \
+        #  end    end2       end3
+        #
+        rcv_master = []
+        rcv_mid = []
+        rcv_mid2 = []
+
+        def handle_master_escalate(message):
+            rcv_master.append(message)
+
+        def handle_mid_escalate(message):
+            rcv_mid.append(message)
+
+        def handle_mid2_escalate(message):
+            rcv_mid2.append(message)
+
+        master.handle_downstream_escalate = handle_master_escalate
+        mid.handle_downstream_escalate = handle_mid_escalate
+        mid2.handle_downstream_escalate = handle_mid2_escalate
+        master.register_downstream_handlers()
+        mid.register_downstream_handlers()
+        mid2.register_downstream_handlers()
+
+        pload = HiveMessage(msg_type=HiveMessageType.THIRDPRTY,
+                            payload=Message("test", {"ping": "pong"}))
+        pload = HiveMessage(msg_type=HiveMessageType.ESCALATE, payload=pload)
+        end3.connection.emit(pload)
+        sleep(2)
+
+        # check that master and mid2 received the message
+        self.assertEqual(len(rcv_master), 1)
+        self.assertEqual(len(rcv_mid), 0)
+        self.assertEqual(len(rcv_mid2), 1)
+
+        # TODO debug where sometimes payload isnt unpacked
+        for message in rcv_master + rcv_mid + rcv_mid2:
+            continue
+            self.assertEqual(message.msg_type, HiveMessageType.BUS)
+            self.assertTrue(isinstance(message.payload, Message))
+
+
+class TestHiveBroadcast(TestCase):
+
+    def test_master(self):
+        #           Master
+        #         /       \
+        #        *         *
+        #       Mid      Mid2
+        #     /   \          \
+        #    *     *          *
+        #  end    end2       end3
+        #
+
+        rcv_mid = []
+        rcv_mid2 = []
+        rcv_end = []
+        rcv_end2 = []
+        rcv_end3 = []
+
+        def handle_mid_broadcast(message):
+            rcv_mid.append(message)
+
+        def handle_mid2_broadcast(message):
+            rcv_mid2.append(message)
+
+        def handle_end_broadcast(message):
+            rcv_end.append(message)
+
+        def handle_end2_broadcast(message):
+            rcv_end2.append(message)
+
+        def handle_end3_broadcast(message):
+            rcv_end3.append(message)
+
+        mid.handle_upstream_broadcast = handle_mid_broadcast
+        mid2.handle_upstream_broadcast = handle_mid2_broadcast
+        end.handle_upstream_broadcast = handle_end_broadcast
+        end2.handle_upstream_broadcast = handle_end2_broadcast
+        end3.handle_upstream_broadcast = handle_end3_broadcast
+        mid.register_upstream_handlers()
+        mid2.register_upstream_handlers()
+        end.register_upstream_handlers()
+        end2.register_upstream_handlers()
+        end3.register_upstream_handlers()
+
+        pload = HiveMessage(msg_type=HiveMessageType.THIRDPRTY,
+                            payload=Message("test", {"ping": "pong"}))
+        master.interface.broadcast(pload)
+        sleep(1)
+
+        # check that all nodes received the message
+        self.assertEqual(len(rcv_mid), 1)
+        self.assertEqual(len(rcv_mid2), 1)
+        self.assertEqual(len(rcv_end), 1)
+        self.assertEqual(len(rcv_end2), 1)
+        self.assertEqual(len(rcv_end3), 1)
+
+        # TODO debug where sometimes payload isnt unpacked
+        for message in rcv_mid + rcv_mid2 + rcv_end + rcv_end2 + rcv_end3:
+            print(message)
+            continue
+            self.assertEqual(message.msg_type, HiveMessageType.THIRDPRTY)
+            self.assertTrue(isinstance(message, HiveMessage))
+
+    def test_mid(self):
+        #           Master
+        #
+        #       Mid      Mid2
+        #     /   \
+        #    *     *
+        #  end    end2       end3
+        #
+
+        rcv_mid = []
+        rcv_mid2 = []
+        rcv_end = []
+        rcv_end2 = []
+        rcv_end3 = []
+
+        def handle_mid_broadcast(message):
+            rcv_mid.append(message)
+
+        def handle_mid2_broadcast(message):
+            rcv_mid2.append(message)
+
+        def handle_end_broadcast(message):
+            rcv_end.append(message)
+
+        def handle_end2_broadcast(message):
+            rcv_end2.append(message)
+
+        def handle_end3_broadcast(message):
+            rcv_end3.append(message)
+
+        mid.handle_upstream_broadcast = handle_mid_broadcast
+        mid2.handle_upstream_broadcast = handle_mid2_broadcast
+        end.handle_upstream_broadcast = handle_end_broadcast
+        end2.handle_upstream_broadcast = handle_end2_broadcast
+        end3.handle_upstream_broadcast = handle_end3_broadcast
+        mid.register_upstream_handlers()
+        mid2.register_upstream_handlers()
+        end.register_upstream_handlers()
+        end2.register_upstream_handlers()
+        end3.register_upstream_handlers()
+
+        pload = HiveMessage(msg_type=HiveMessageType.THIRDPRTY,
+                            payload=Message("test", {"ping": "pong"}))
+        mid.interface.broadcast(pload)
+        sleep(1)
+
+        # check that all nodes received the message
+        self.assertEqual(len(rcv_mid), 0)
+        self.assertEqual(len(rcv_mid2), 0)
+        self.assertEqual(len(rcv_end), 1)
+        self.assertEqual(len(rcv_end2), 1)
+        self.assertEqual(len(rcv_end3), 0)
+
+        # TODO debug where sometimes payload isnt unpacked
+        for message in rcv_mid + rcv_mid2 + rcv_end + rcv_end2 + rcv_end3:
+            print(message)
+            continue
+            self.assertEqual(message.msg_type, HiveMessageType.THIRDPRTY)
+            self.assertTrue(isinstance(message, HiveMessage))
+
+
+@skip("TODO Fix me")
+class TestPropagate(TestCase):
+
+    def test_mid(self):
+
+        #
+        #           Master
+        #          *     \
+        #         /       *
+        #       Mid      Mid2
+        #      /   \         \
+        #     *     *         *
+        #   end    end2       end3
+        #
+
+        rcv_master = []
+        rcv_mid = []
+        rcv_mid2 = []
+        rcv_end = []
+        rcv_end2 = []
+        rcv_end3 = []
+
+        def handle_master_propagate(message):
+            rcv_master.append(message)
+
+        def handle_mid_propagate(message):
+            rcv_mid.append(message)
+
+        def handle_mid2_propagate(message):
+            rcv_mid2.append(message)
+
+        def handle_end_propagate(message):
+            rcv_end.append(message)
+
+        def handle_end2_propagate(message):
+            rcv_end2.append(message)
+
+        def handle_end3_propagate(message):
+            rcv_end3.append(message)
+
+        master.handle_downstream_propagate = handle_master_propagate
+        mid.handle_downstream_propagate = handle_mid_propagate
+        mid2.handle_downstream_propagate = handle_mid2_propagate
+        mid.handle_upstream_propagate = handle_mid_propagate
+        mid2.handle_upstream_propagate = handle_mid2_propagate
+        end.handle_upstream_propagate = handle_end_propagate
+        end2.handle_upstream_propagate = handle_end2_propagate
+        end3.handle_upstream_propagate = handle_end3_propagate
+
+        master.register_downstream_handlers()
+        mid.register_downstream_handlers()
+        mid2.register_downstream_handlers()
+        mid.register_upstream_handlers()
+        mid2.register_upstream_handlers()
+        end.register_upstream_handlers()
+        end2.register_upstream_handlers()
+        end3.register_upstream_handlers()
+
+        pload = HiveMessage(msg_type=HiveMessageType.THIRDPRTY,
+                            payload=Message("test", {"ping": "pong"}))
+        pload = HiveMessage(msg_type=HiveMessageType.PROPAGATE,  payload=pload)
+        mid.connection.emit(pload)
+        sleep(2)
+
+        # check that every node received the message
+        self.assertEqual(len(rcv_master), 1)
+        self.assertEqual(len(rcv_mid), 1)
+        self.assertEqual(len(rcv_mid2), 1)
+        self.assertEqual(len(rcv_end), 1)
+        self.assertEqual(len(rcv_end2), 1)
+        self.assertEqual(len(rcv_end3), 1)
+
+        # TODO debug where sometimes payload isnt unpacked
+        for message in rcv_master + rcv_mid + rcv_mid2:
+            print(message)
+            continue
+            self.assertEqual(message.msg_type, HiveMessageType.BUS)
+            self.assertTrue(isinstance(message.payload, Message))
+
+    def test_end(self):
+        #
+        #           Master
+        #          *     \
+        #         /       *
+        #       Mid      Mid2
+        #      *   \         \
+        #     /     *         *
+        #   end    end2       end3
+        #
+        rcv_master = []
+        rcv_mid = []
+        rcv_mid2 = []
+        rcv_end = []
+        rcv_end2 = []
+        rcv_end3 = []
+
+        def handle_master_propagate(message):
+            rcv_master.append(message)
+
+        def handle_mid_propagate(message):
+            rcv_mid.append(message)
+
+        def handle_mid2_propagate(message):
+            rcv_mid2.append(message)
+
+        def handle_end_propagate(message):
+            rcv_end.append(message)
+
+        def handle_end2_propagate(message):
+            rcv_end2.append(message)
+
+        def handle_end3_propagate(message):
+            rcv_end3.append(message)
+
+        master.handle_downstream_propagate = handle_master_propagate
+        mid.handle_downstream_propagate = handle_mid_propagate
+        mid2.handle_downstream_propagate = handle_mid2_propagate
+        mid.handle_upstream_propagate = handle_mid_propagate
+        mid2.handle_upstream_propagate = handle_mid2_propagate
+        end.handle_upstream_propagate = handle_end_propagate
+        end2.handle_upstream_propagate = handle_end2_propagate
+        end3.handle_upstream_propagate = handle_end3_propagate
+
+        master.register_downstream_handlers()
+        mid.register_downstream_handlers()
+        mid2.register_downstream_handlers()
+        mid.register_upstream_handlers()
+        mid2.register_upstream_handlers()
+        end.register_upstream_handlers()
+        end2.register_upstream_handlers()
+        end3.register_upstream_handlers()
+
+        pload = HiveMessage(msg_type=HiveMessageType.THIRDPRTY,
+                            payload=Message("test", {"ping": "pong"}))
+        pload = HiveMessage(msg_type=HiveMessageType.PROPAGATE, payload=pload)
+        end.connection.emit(pload)
+        sleep(2)
+
+        # check that every node received the message
+        for message in rcv_master + rcv_mid + rcv_mid2 + rcv_end + rcv_end2 + rcv_end3:
+            print(66, message)
+
+        self.assertEqual(len(rcv_master), 1)
+        self.assertEqual(len(rcv_mid), 1)
+        self.assertEqual(len(rcv_mid2), 1)
+        self.assertEqual(len(rcv_end), 1)
+        self.assertEqual(len(rcv_end2), 1)
+        self.assertEqual(len(rcv_end3), 1)
+
+        # TODO debug where sometimes payload isnt unpacked
+        for message in rcv_master + rcv_mid + rcv_mid2 + rcv_end + rcv_end2 + rcv_end3:
+            continue
+            self.assertEqual(message.msg_type, HiveMessageType.BUS)
+            self.assertTrue(isinstance(message.payload, Message))
+
+    def test_end3(self):
+        #
+        #           Master
+        #          /     *
+        #         *       \
+        #       Mid      Mid2
+        #      /   \         *
+        #     *     *         \
+        #   end    end2       end3
+        #
+        rcv_master = []
+        rcv_mid = []
+        rcv_mid2 = []
+        rcv_end = []
+        rcv_end2 = []
+        rcv_end3 = []
+
+        def handle_master_propagate(message):
+            rcv_master.append(message)
+
+        def handle_mid_propagate(message):
+            rcv_mid.append(message)
+
+        def handle_mid2_propagate(message):
+            rcv_mid2.append(message)
+
+        def handle_end_propagate(message):
+            rcv_end.append(message)
+
+        def handle_end2_propagate(message):
+            rcv_end2.append(message)
+
+        def handle_end3_propagate(message):
+            rcv_end3.append(message)
+
+        master.handle_downstream_propagate = handle_master_propagate
+        mid.handle_downstream_propagate = handle_mid_propagate
+        mid2.handle_downstream_propagate = handle_mid2_propagate
+        mid.handle_upstream_propagate = handle_mid_propagate
+        mid2.handle_upstream_propagate = handle_mid2_propagate
+        end.handle_upstream_propagate = handle_end_propagate
+        end2.handle_upstream_propagate = handle_end2_propagate
+        end3.handle_upstream_propagate = handle_end3_propagate
+
+        master.register_downstream_handlers()
+        mid.register_downstream_handlers()
+        mid2.register_downstream_handlers()
+        mid.register_upstream_handlers()
+        mid2.register_upstream_handlers()
+        end.register_upstream_handlers()
+        end2.register_upstream_handlers()
+        end3.register_upstream_handlers()
+
+        pload = HiveMessage(msg_type=HiveMessageType.THIRDPRTY,
+                            payload=Message("test", {"ping": "pong"}))
+        pload = HiveMessage(msg_type=HiveMessageType.PROPAGATE, payload=pload)
+        end3.connection.emit(pload)
+        sleep(2)
+
+        # check that every node received the message
+        self.assertEqual(len(rcv_master), 1)
+        self.assertEqual(len(rcv_mid), 1)
+        self.assertEqual(len(rcv_mid2), 1)
+        self.assertEqual(len(rcv_end), 1)
+        self.assertEqual(len(rcv_end2), 1)
+        self.assertEqual(len(rcv_end3), 1)
+
+        # TODO debug where sometimes payload isnt unpacked
+        for message in rcv_master + rcv_mid + rcv_mid2:
+            continue
+            self.assertEqual(message.msg_type, HiveMessageType.BUS)
+            self.assertTrue(isinstance(message.payload, Message))
+
diff --git a/test/unittests/test_db.py b/test/unittests/test_db.py
index 93c2930..5fbce6e 100644
--- a/test/unittests/test_db.py
+++ b/test/unittests/test_db.py
@@ -1,7 +1,7 @@
 import os
 from unittest import TestCase
 
-from jarbas_hive_mind.database import ClientDatabase, Client
+from hivemind_core.database import ClientDatabase, Client
 
 
 class TestDB(TestCase):
@@ -12,7 +12,7 @@ def test_add_entry(self):
         access_key = os.urandom(16).hex()
         password = None
 
-        with ClientDatabase(path="/tmp/test_db") as db:
+        with ClientDatabase() as db:
             n = db.total_clients()
             name = f"HiveMind-Node-{n}"
             user = db.add_client(name, access_key,