diff --git a/.forgejo/workflows/build-release.yaml b/.forgejo/workflows/build-release.yaml
index 1170c10..48e5e92 100644
--- a/.forgejo/workflows/build-release.yaml
+++ b/.forgejo/workflows/build-release.yaml
@@ -6,7 +6,7 @@ on:
       - '[0-9]+\.[0-9]+\.[0-9]'
 
 jobs:
-  backend-pylint:
+  lint-and-typing:
     runs-on: docker
     container: nikolaik/python-nodejs:python3.11-nodejs21
     steps:
@@ -14,16 +14,19 @@ jobs:
         uses: https://code.forgejo.org/actions/checkout@v3
       - name: Install packages
         run: |
-          pip install -e . -q
+          pip install -e .[lint,typing] -q --disable-pip-version-check -q
           python -m pip list --format=columns --disable-pip-version-check
-          pip install pylint~=2.17.7 --disable-pip-version-check -q
       - name: Run pylint
         run: |
           pylint --version
           pylint **/*.py --exit-zero --rc-file pyproject.toml
+      - name: Run mypy
+        run: |
+          mypy --version
+          mypy .
 
   build-artifacts:
-    needs: ["backend-pylint"]
+    needs: ["lint-and-typing"]
     runs-on: docker
     container: nikolaik/python-nodejs:python3.11-nodejs21
     steps:
@@ -53,3 +56,37 @@ jobs:
         run: pip install twine
       - name: Upload package to registry
         run: python -m twine upload --repository-url ${{ secrets.REPOSITORY_URL }} -u ${{ secrets.TWINE_DEPLOY_USER }} -p ${{ secrets.TWINE_DEPLOY_PASSWORD }} dist/*
+
+  build-and-push-container:
+    needs: [ "publish-artifacts" ]
+    runs-on: dind
+    steps:
+      - name: Set up Docker Buildx
+        uses: docker/setup-buildx-action@v3
+      - name: Log into Docker Package Registry
+        uses: docker/login-action@v3
+        with:
+          registry: forgejo.neshweb.net
+          username: ${{ secrets.FORGEJO_USERNAME }}
+          password: ${{ secrets.FORGEJO_TOKEN }}
+      - name: Build and push to Docker Package Registry
+        uses: docker/build-push-action@v5
+        with:
+          build-args: |
+            PACKAGE_VERSION=${{ github.ref_name }}
+          push: true
+          tags: forgejo.neshweb.net/firq/dockge-cli:${{ github.ref_name }}
+
+  release:
+    needs: [ build-and-push-container, publish-artifacts ]
+    if: success()
+    runs-on: docker
+    steps:
+      - name: Release New Version
+        uses: actions/forgejo-release@v1
+        with:
+          direction: upload
+          url: https://forgejo.neshweb.net
+          release-dir: release
+          token: ${{ secrets.FORGEJO_TOKEN }}
+          tag: ${{  github.ref_name }}
\ No newline at end of file
diff --git a/.forgejo/workflows/check.yaml b/.forgejo/workflows/check.yaml
new file mode 100644
index 0000000..55de02a
--- /dev/null
+++ b/.forgejo/workflows/check.yaml
@@ -0,0 +1,34 @@
+on:
+  push:
+    branches: "**"
+
+jobs:
+  pylint:
+    runs-on: docker
+    container: nikolaik/python-nodejs:python3.11-nodejs21
+    steps:
+      - name: Checkout source code
+        uses: https://code.forgejo.org/actions/checkout@v3
+      - name: Install packages
+        run: |
+          pip install -e .[lint] -q --disable-pip-version-check -q
+          python -m pip list --format=columns --disable-pip-version-check
+      - name: Run pylint
+        run: |
+          pylint --version
+          pylint **/*.py --exit-zero --rc-file pyproject.toml
+
+  mypy:
+    runs-on: docker
+    container: nikolaik/python-nodejs:python3.11-nodejs21
+    steps:
+      - name: Checkout source code
+        uses: https://code.forgejo.org/actions/checkout@v3
+      - name: Install packages
+        run: |
+          pip install -e .[typing] -q --disable-pip-version-check -q
+          python -m pip list --format=columns --disable-pip-version-check
+      - name: Run mypy
+        run: |
+          mypy --version
+          mypy .
diff --git a/.forgejo/workflows/lint.yaml b/.forgejo/workflows/lint.yaml
deleted file mode 100644
index a88449c..0000000
--- a/.forgejo/workflows/lint.yaml
+++ /dev/null
@@ -1,20 +0,0 @@
-on:
-  push:
-    branches: "**"
-
-jobs:
-  backend-pylint:
-    runs-on: docker
-    container: nikolaik/python-nodejs:python3.11-nodejs21
-    steps:
-      - name: Checkout source code
-        uses: https://code.forgejo.org/actions/checkout@v3
-      - name: Install packages
-        run: |
-          pip install -e . -q
-          python -m pip list --format=columns --disable-pip-version-check
-          pip install pylint~=2.17.7 --disable-pip-version-check -q
-      - name: Run pylint
-        run: |
-          pylint --version
-          pylint **/*.py --exit-zero --rc-file pyproject.toml
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..88e754d
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,4 @@
+FROM forgejo.neshweb.net/ci-docker-images/python-neshweb:3.11
+
+ARG PACKAGE_VERSION=0.1.0
+RUN pip install dockge-cli==${PACKAGE_VERSION}
diff --git a/README.md b/README.md
index 4c51468..2f946e3 100644
--- a/README.md
+++ b/README.md
@@ -1,3 +1,67 @@
 # dockge-cli
 
 A simple CLI application written in Python for communicating with Dockge using websockets
+
+## Background
+
+Dockge (spoken dock-ge or dockage) is a tool to manage docker-compose stacks from a web ui. It is developed by louislam, who also develops UptimeKuma.
+
+Dockge itself doesn't offer any kind of API or programmatic access, as it is just intended for managing stacks via UI.
+
+My current deployment solution for firq.dev and fgo-ta.com is based on Dockge, and I was over it always having to reload the stack whenever I pushed an update. Instead, I wanted to have this as a separate CI step, automatically redeploying a givens stack.
+
+As Dockge is using a websocket-based system under the hood, it was easy to take a look at how communication occurs. In general, communication is achieved by leveraging socket.io for the data. Since Python already offers a solution for socket.io, it is just a matter of emulating the calls the webui sends and receives.
+
+In the end, this is the current result that works pretty well for my understanding. I am still trying to improve upon some issues (login times out, stability, features), but in general this works as a fine solution for automatic stack updating.
+
+## Installation
+
+Install it from the custom package index using
+
+```shell
+pip install --extra-index-url https://forgejo.neshweb.net/api/packages/Firq/pypi/simple/ dockge-cli
+```
+
+Alternativly, install it using this repository. When installing for development, make sure to install with the additional dependencies
+
+```shell
+pip install -e .[lint,typing]
+```
+
+## Usage
+
+Call the CLI using `dockge-cli` or `dockge`.
+
+```shell
+usage: dockge_cli [-h] [--version] {host,login,logout,list,status,restart,start,stop,down,update,exit,help}
+
+CLI interface for interacting with Dockge
+
+positional arguments:
+  {host,login,logout,list,status,restart,start,stop,down,update,exit,help}
+
+options:
+  -h, --help            show this help message and exit
+  --version             show program's version number and exit
+```
+
+Help for each individual command can be invoked by calling `dockge-cli help <command>`
+
+## The magic behind this
+
+Generally, this makes use of the underlying Websockets API that the Dockge frontend uses to communicate with the server. By analyzing the traffic and looking into the codebase, I was able to reverse most of the packets that are being sent. This allows me to then contruct, send and receive my own packets, making the whole thing work.
+
+There are some things that need to be taken into account for this: For one, dockge uses socket.io for the websocket communication. This meant I had to find the corresponding socket.io version to get the correct version of python-socketio. In addition, I had to find out how the authorization mechanism behind this works.
+
+After finishing up the first prototype, the workings are as follows:
+
+1. A websocket session is established using socket.io - this happens automatically
+2. After the session is ready, the `login` command is sent together with a provided username and password
+3. Once the CLI is authorized, the selected command is sent
+4. The CLI waits for any response values and exits once the command has executed successfully
+
+To provide a smooth experience, both the credentials and the remote host URI are stored on disk. just like the `docker` cli, the credentials are not encrypted, meaning it is advised to either clear the credentials after use OR to use the `--username` and `--password` parameters. This is especially recommended for CI applications.
+
+## Known issues
+
+This CLI does not work when Mullvad is used, as Mullvad actively blocks port forwarding (which python-socketio uses)
diff --git a/dockge_cli/.gitignore b/dockge_cli/.gitignore
deleted file mode 100644
index d36a0d4..0000000
--- a/dockge_cli/.gitignore
+++ /dev/null
@@ -1 +0,0 @@
-.temp/*
\ No newline at end of file
diff --git a/dockge_cli/__main__.py b/dockge_cli/__main__.py
new file mode 100644
index 0000000..660e368
--- /dev/null
+++ b/dockge_cli/__main__.py
@@ -0,0 +1,2 @@
+from .dockge_cli import cli
+cli()
diff --git a/dockge_cli/client/commandprovider/__init__.py b/dockge_cli/client/commandprovider/__init__.py
deleted file mode 100644
index e69de29..0000000
diff --git a/dockge_cli/client/commandprovider/descriptors.py b/dockge_cli/client/commandprovider/descriptors.py
deleted file mode 100644
index cd346fd..0000000
--- a/dockge_cli/client/commandprovider/descriptors.py
+++ /dev/null
@@ -1,88 +0,0 @@
-from .bindings import ExecutionCommands
-
-command_mappings = [
-    {
-        "command": "host",
-        "description": "Sets and gets the URI of the dockge instance. Remove any unnecessary subdomains/protocols from the URI",
-        "args": 1,
-        "optional": True,
-        "binding":    ExecutionCommands.host
-    },
-    {
-        "command": "login",
-        "description": "Logs into a given dockge account, either with an interactive dialogue or by passing --user and --password",
-        "args": 2,
-        "optional": True,
-        "binding":   ExecutionCommands.login
-    },
-    {
-        "command": "logout",
-        "description": "Removes the credentials from the local storage.",
-        "args": 0,
-        "optional": False,
-        "binding": ExecutionCommands.logout
-    },
-    {
-        "command": "list",
-        "description": "Lists all available stacks with their status",
-        "args": 0,
-        "optional": False,
-        "binding": ExecutionCommands.list
-    },
-    {
-        "command": "status",
-        "description": "Returns the status of one stack",
-        "args": 1,
-        "optional": False,
-        "binding": ExecutionCommands.status
-    },
-    {
-        "command": "restart",
-        "description": "Restarts a given stack",
-        "args": 1,
-        "optional": False,
-        "binding": ExecutionCommands.restart
-    },
-    {
-        "command": "start",
-        "description": "Starts a given stack",
-        "args": 1,
-        "optional": False,
-        "binding": ExecutionCommands.start
-    },
-    {
-        "command": "stop",
-        "description": "Stops a given stack",
-        "args": 1,
-        "optional": False,
-        "binding": ExecutionCommands.stop
-    },
-    {
-        "command": "down",
-        "description": "Stop & Downs a given stack",
-        "args": 1,
-        "optional": False,
-        "binding": ExecutionCommands.down
-    },
-    {
-        "command": "update",
-        "description": "Updates a stack",
-        "args": 1,
-        "optional": False,
-        "binding": ExecutionCommands.update
-    },
-    {
-        "command": "exit",
-        "description": "Exits the CLI - this will reset all settings, including credentials and host",
-        "args": 0,
-        "optional": False,
-        "binding": ExecutionCommands.exit
-    },
-    {
-        "command": "help",
-        "description": "Displays helping hints for commands",
-        "args": 1,
-        "optional": True,
-        "binding": ExecutionCommands.help
-    }
-]
diff --git a/dockge_cli/client/commandprovider/factory.py b/dockge_cli/client/commandprovider/factory.py
deleted file mode 100644
index 4bfe95a..0000000
--- a/dockge_cli/client/commandprovider/factory.py
+++ /dev/null
@@ -1,17 +0,0 @@
-from typing import List, Callable
-from pydantic import BaseModel
-from .descriptors import command_mappings
-
-class Command(BaseModel):
-    command: str
-    description: str
-    args: int
-    optional: bool
-    binding: Callable
-
-commands: dict[str, Command] = {}
-descriptors: List[dict[str, object]] = command_mappings
-
-for descriptor in descriptors:
-    c = Command(**descriptor) # type: ignore
-    commands.update({ c.command: c  })
diff --git a/dockge_cli/client/commands/__init__.py b/dockge_cli/client/commands/__init__.py
new file mode 100644
index 0000000..61eb6fc
--- /dev/null
+++ b/dockge_cli/client/commands/__init__.py
@@ -0,0 +1,4 @@
+from ...models import Command
+from .mappings import mapping
+
+commands: dict[str, Command] = { c.cmd: c for c in mapping }
diff --git a/dockge_cli/client/commandprovider/bindings.py b/dockge_cli/client/commands/functions.py
similarity index 50%
rename from dockge_cli/client/commandprovider/bindings.py
rename to dockge_cli/client/commands/functions.py
index 6e813de..3ee695d 100644
--- a/dockge_cli/client/commandprovider/bindings.py
+++ b/dockge_cli/client/commands/functions.py
@@ -1,33 +1,50 @@
 from urllib.parse import urlparse
 from getpass import getpass
+import re
 
-from ...models.parser import Credentials
+from ...models import Credentials
 from ...service import storage
-from ..utils import stack_formatter, status_formatter, generic_formatter, get_credential_parser
-from ...service.communicate import DockgeConnection
+from ...service.connection import DockgeConnection
+from ..utils import stack_formatter, status_formatter, generic_formatter, credential_parser_factory
 
-class ExecutionCommands():
+class FunctionBindings():
+    """
+    Helper class that provides all the static methods in an organized way
+    This is an abstraction layer of the CLI, as those functions only do little preprocessing before calling the actural DockgeConnection
+    """
     @staticmethod
     def __setup():
+        """
+        Creates a connection and logs into Dockge
+        """
         con = DockgeConnection()
         con.connect_and_login()
         return con
 
     @staticmethod
     def host(extra_args):
+        """
+        host command binding
+        """
         if len(extra_args) > 0:
-            res = urlparse(extra_args[0])
+            mat = re.search(r"((\w+\.)?\w+\.\w+(\/.+)?)", extra_args[0], re.IGNORECASE)
+            if mat is None:
+                raise ValueError("Given host did not match regex")
+            res = urlparse(f"https://{mat[0]}")
             if all([res.scheme, res.netloc]):
-                host = extra_args[0].rstrip("/").replace("https://", "").replace("wss://", "")
-                storage.put("host", host)
+                storage.put("host", mat[0])
             else:
                 raise ValueError(f"Malformed URL {extra_args[0]}")
         print(storage.get("host"))
 
     @staticmethod
     def login(extra_args):
+        """
+        login command binding
+        """
+        print(f"WARNING! These credentials will be saved unencrypted in {storage._file.absolute()}")
         if len(extra_args) > 0:
-            credentials = get_credential_parser().parse_args(extra_args, namespace=Credentials)
+            credentials = credential_parser_factory().parse_args(extra_args, namespace=Credentials)
             storage.put("username", credentials.username, encoded=True)
             storage.put("password", credentials.password, encoded=True)
             return
@@ -36,67 +53,85 @@ class ExecutionCommands():
 
     @staticmethod
     def logout(_):
+        """
+        logout command binding
+        """
         storage.remove("username")
         storage.remove("password")
 
     @staticmethod
     def exit(_):
+        """
+        exit command binding
+        """
         storage.clear()
 
     @staticmethod
     def list(_):
-        con = ExecutionCommands.__setup()
+        """
+        list command binding
+        """
+        con = FunctionBindings.__setup()
         stack_formatter(con.list_stacks())
         con.disconnect()
 
     @staticmethod
     def status(extra_args):
-        if extra_args is None:
-            raise ValueError
-        con = ExecutionCommands.__setup()
+        """
+        status command binding
+        """
+        con = FunctionBindings.__setup()
         status_formatter(con.list_stack(extra_args[0]))
         con.disconnect()
 
     @staticmethod
     def restart(extra_args):
-        if extra_args is None:
-            raise ValueError
-        con = ExecutionCommands.__setup()
+        """
+        restart command binding
+        """
+        con = FunctionBindings.__setup()
         generic_formatter(con.restart(extra_args[0]))
         con.disconnect()
 
     @staticmethod
     def update(extra_args):
-        if extra_args is None:
-            raise ValueError
-        con = ExecutionCommands.__setup()
+        """
+        update command binding
+        """
+        con = FunctionBindings.__setup()
         generic_formatter(con.update(extra_args[0]))
         con.disconnect()
 
     @staticmethod
     def stop(extra_args):
-        if extra_args is None:
-            raise ValueError
-        con = ExecutionCommands.__setup()
+        """
+        stop command binding
+        """
+        con = FunctionBindings.__setup()
         generic_formatter(con.stop(extra_args[0]))
         con.disconnect()
 
     @staticmethod
     def start(extra_args):
-        if extra_args is None:
-            raise ValueError
-        con = ExecutionCommands.__setup()
+        """
+        start command binding
+        """
+        con = FunctionBindings.__setup()
         generic_formatter(con.start(extra_args[0]))
         con.disconnect()
 
     @staticmethod
     def down(extra_args):
-        if extra_args is None:
-            raise ValueError
-        con = ExecutionCommands.__setup()
+        """
+        down command binding
+        """
+        con = FunctionBindings.__setup()
         generic_formatter(con.down(extra_args[0]))
         con.disconnect()
 
     @staticmethod
     def help():
+        """
+        exit command binding - This should never be invoked
+        """
         print("WTF")
diff --git a/dockge_cli/client/commands/mappings.py b/dockge_cli/client/commands/mappings.py
new file mode 100644
index 0000000..a6ca52f
--- /dev/null
+++ b/dockge_cli/client/commands/mappings.py
@@ -0,0 +1,90 @@
+from typing import List
+from ...models import Command
+from .functions import FunctionBindings
+
+mapping: List[Command] = [
+    Command(
+        cmd="host",
+        description="Sets and gets the URI of the dockge instance. Remove any unnecessary subdomains/protocols from the URI",
+        args=1,
+        optional=True,
+        func=FunctionBindings.host
+    ),
+    Command(
+        cmd="login",
+        description="Logs into a given dockge account, either with an interactive dialogue or by passing --user and --password",
+        args=2,
+        optional=True,
+        func=FunctionBindings.login
+    ),
+    Command(
+        cmd="logout",
+        description="Removes the credentials from the local storage.",
+        args=0,
+        optional=False,
+        func=FunctionBindings.logout
+    ),
+    Command(
+        cmd="list",
+        description="Lists all available stacks with their status",
+        args=0,
+        optional=False,
+        func=FunctionBindings.list
+    ),
+    Command(
+        cmd="status",
+        description="Returns the status of one stack",
+        args=1,
+        optional=False,
+        func=FunctionBindings.status
+    ),
+    Command(
+        cmd="restart",
+        description="Restarts a given stack",
+        args=1,
+        optional=False,
+        func=FunctionBindings.restart
+    ),
+    Command(
+        cmd="start",
+        description="Starts a given stack",
+        args=1,
+        optional=False,
+        func=FunctionBindings.start
+    ),
+    Command(
+        cmd="stop",
+        description="Stops a given stack",
+        args=1,
+        optional=False,
+        func=FunctionBindings.stop
+    ),
+    Command(
+        cmd="down",
+        description="Stop & Downs a given stack",
+        args=1,
+        optional=False,
+        func=FunctionBindings.down
+    ),
+    Command(
+        cmd="update",
+        description="Updates a stack",
+        args=1,
+        optional=False,
+        func=FunctionBindings.update
+    ),
+    Command(
+        cmd="exit",
+        description="Exits the CLI - this will reset all settings, including credentials and host",
+        args=0,
+        optional=False,
+        func=FunctionBindings.exit
+    ),
+    Command(
+        cmd="help",
+        description="Displays helping hints for commands",
+        args=1,
+        optional=True,
+        func=FunctionBindings.help
+    )
+]
diff --git a/dockge_cli/client/parser.py b/dockge_cli/client/parser.py
index 2efbc30..f84628b 100644
--- a/dockge_cli/client/parser.py
+++ b/dockge_cli/client/parser.py
@@ -2,10 +2,14 @@ import argparse
 import sys
 
 from .. import __version__
-from ..models.parser import Arguments
-from .commandprovider.factory import commands
+from ..models import Arguments
+from .commands import commands
 
 def parse_arguments():
+    """
+    Create a parser and parse the arguments of sys.argv based on the Arguments Namespace
+    Returns arguments and extra arguments separately
+    """
     parser = argparse.ArgumentParser(
                         prog="dockge_cli",
                         description="CLI interface for interacting with Dockge",)
diff --git a/dockge_cli/client/run.py b/dockge_cli/client/run.py
index 105f380..22d0a2a 100644
--- a/dockge_cli/client/run.py
+++ b/dockge_cli/client/run.py
@@ -1,6 +1,9 @@
-from .commandprovider.factory import commands
+from .commands import commands
 
 def display_help(extra_args):
+    """
+    Display help dialogues for each command
+    """
     if not extra_args:
         print(f"{commands['help'].description}")
         return
@@ -11,6 +14,10 @@ def display_help(extra_args):
     print(f"{commands[extra_args[0]].description}")
 
 def run(command, args):
+    """
+    Runs a given command with the provided args
+    Alsso automatically maps the given command string to the correct Command class
+    """
     if command not in commands:
         raise ValueError("Invalid Command")
 
@@ -25,4 +32,4 @@ def run(command, args):
         display_help(args)
         return
 
-    c.binding(args)
+    c.func(args)
diff --git a/dockge_cli/client/utils.py b/dockge_cli/client/utils.py
index 6e9fa87..ebddf26 100644
--- a/dockge_cli/client/utils.py
+++ b/dockge_cli/client/utils.py
@@ -2,7 +2,10 @@ import argparse
 from tabulate import tabulate
 from ..models import StackStatus
 
-def get_credential_parser():
+def credential_parser_factory():
+    """
+    Creates a new parser for login credentials
+    """
     credentialparser = argparse.ArgumentParser(
         prog="login",
         description="Subparser for login credentials provided by CI"
@@ -12,21 +15,30 @@ def get_credential_parser():
     return credentialparser
 
 def stack_formatter(stacks):
+    """
+    Prints a given stack list formatted as a table
+    """
     if not stacks["ok"]:
         raise RuntimeError("Stack GET didn't work")
 
     table, headers = [], ["Stackname", "Status"]
     for key, val in stacks["stackList"].items():
-        table.append([key, StackStatus(val['status']).name])
+        table.append([key, StackStatus(val['status']).name.lower()])
 
     print(tabulate(table, headers=headers, tablefmt="github"), "\n")
 
 def status_formatter(status):
+    """
+    Prints the status for a given stack
+    """
     print(f"Is Stack Ok? {'Yes' if status['ok'] else 'No'}")
     headers = ["Container", "Status"]
     table = [[k, v] for k, v in status["serviceStatusList"].items()]
     print(tabulate(table, headers=headers, tablefmt="github"), "\n")
 
 def generic_formatter(status):
+    """
+    Prints a generic dockge message
+    """
     print(f"Is Ok? {'Yes' if status['ok'] else 'No'}")
     print(f"Stack status: {status['msg']}")
diff --git a/dockge_cli/dockge_cli.py b/dockge_cli/dockge_cli.py
index 25c437f..af77054 100644
--- a/dockge_cli/dockge_cli.py
+++ b/dockge_cli/dockge_cli.py
@@ -2,5 +2,8 @@ from .client.parser import parse_arguments
 from .client.run import run
 
 def cli():
+    """
+    main function for cli invocation
+    """
     command, args= parse_arguments()
     run(command.command, args)
diff --git a/dockge_cli/models/__init__.py b/dockge_cli/models/__init__.py
index ef15c9b..ac7af80 100644
--- a/dockge_cli/models/__init__.py
+++ b/dockge_cli/models/__init__.py
@@ -1 +1,3 @@
 from .codes import StackStatus
+from .command import Command
+from .parser import Arguments, Credentials
diff --git a/dockge_cli/models/codes.py b/dockge_cli/models/codes.py
index b8f9a07..569ce1d 100644
--- a/dockge_cli/models/codes.py
+++ b/dockge_cli/models/codes.py
@@ -1,7 +1,9 @@
 from enum import Enum
 
 class StackStatus(Enum):
-    # pylint: disable=invalid-name
-    inactive = 1
-    running = 3
-    exited = 4
+    """
+    mapping for plaintext vs statuscode
+    """
+    INACTIVE = 1
+    RUNNING = 3
+    EXITED = 4
diff --git a/dockge_cli/models/command.py b/dockge_cli/models/command.py
new file mode 100644
index 0000000..61a13b3
--- /dev/null
+++ b/dockge_cli/models/command.py
@@ -0,0 +1,12 @@
+from typing import Callable
+from pydantic import BaseModel
+
+class Command(BaseModel):
+    """
+    Basic command structure for the CLI to automatically generate valid commands
+    """
+    cmd: str
+    func: Callable
+    args: int
+    optional: bool
+    description: str
diff --git a/dockge_cli/models/parser.py b/dockge_cli/models/parser.py
index c836e62..e590ce9 100644
--- a/dockge_cli/models/parser.py
+++ b/dockge_cli/models/parser.py
@@ -2,9 +2,15 @@ import argparse
 
 # pylint: disable=too-few-public-methods
 class Arguments(argparse.Namespace):
+    """
+    Default Arguments when calling the CLI
+    """
     command: str
 
 # pylint: disable=too-few-public-methods
 class Credentials(argparse.Namespace):
+    """
+    Special Argument Namespace for login credentials of the login commands
+    """
     username: str
     password: str
diff --git a/dockge_cli/service/.gitignore b/dockge_cli/service/.gitignore
new file mode 100644
index 0000000..9e62042
--- /dev/null
+++ b/dockge_cli/service/.gitignore
@@ -0,0 +1 @@
+.storage/*
\ No newline at end of file
diff --git a/dockge_cli/service/communicate.py b/dockge_cli/service/connection.py
similarity index 69%
rename from dockge_cli/service/communicate.py
rename to dockge_cli/service/connection.py
index 4eb645d..78e7582 100644
--- a/dockge_cli/service/communicate.py
+++ b/dockge_cli/service/connection.py
@@ -5,8 +5,14 @@ import socketio.exceptions
 from . import storage
 
 class DockgeConnection:
+    """
+    Provider class for Dockge
+    Provides all the functionality for connecting, logging in and executing commands
+    """
     class LoginException(BaseException):
-        pass
+        """
+        Special exception when login fails too often
+        """
 
     _sio: socketio.Client
     _host: str
@@ -26,7 +32,7 @@ class DockgeConnection:
     def _init_events(self):
         @self._sio.event
         def connect():
-            self.connect()
+            self.login()
             print("Connected!")
 
         @self._sio.event
@@ -57,20 +63,34 @@ class DockgeConnection:
             success = True
         else:
             print("Issue with login procedure")
+            print(data)
         return success
 
     # Functions
     def connect_and_login(self):
-        self._sio.connect(f"https://{self._host}/socket.io/", transports=['websocket'])
-        self.connect()
+        """
+        Connect to the websocket
+        """
+        # Dockge uses Socket.io for the websockets, so this URI and params are always the same
+        self._sio.connect(f"https://{self._host}/socket.io/")
+        self.login()
 
-    def connect(self):
+    def login(self):
+        """
+        Log into dockge using basicauth
+        Retries 5 times when timeouts occur
+        """
         if self._logged_in:
             return
 
         data = None
         retry, count = True, 0
 
+        if not storage.exists("username"):
+            raise ValueError("Missing username")
+        if not storage.exists("password"):
+            raise ValueError("Missing password")
+
         while retry and count < 5:
             try:
                 data = self._sio.call(
@@ -78,12 +98,13 @@ class DockgeConnection:
                     {
                         "username": storage.get("username", encoded=True),
                         "password": storage.get("password", encoded=True),
-                        "token":""
+                        "token": ""
                     },
-                    timeout=5
+                    timeout=10
                 )
                 retry = False
             except socketio.exceptions.TimeoutError:
+                print("Reached timeout for login, retrying ...")
                 retry = True
                 count += 1
 
@@ -92,6 +113,9 @@ class DockgeConnection:
         self._logged_in = True
 
     def list_stacks(self):
+        """
+        Requests stack list from dockge, returns list when event was sent
+        """
         self._sio.emit("agent", ("", "requestStackList"))
         while self._stacklist is None:
             time.sleep(0.5)
@@ -100,29 +124,50 @@ class DockgeConnection:
         return retval
 
     def list_stack(self, name: str):
-        ret = self._sio.call("agent", ("", "serviceStatusList", name), timeout=5)
+        """
+        Lists status for a stack
+        """
+        ret = self._sio.call("agent", ("", "serviceStatusList", name), timeout=10)
         return ret
 
     def restart(self, name):
-        ret = self._sio.call("agent", ("", "restartStack", name), timeout=10)
+        """
+        Restarts a given stack
+        """
+        ret = self._sio.call("agent", ("", "restartStack", name), timeout=30)
         return ret
 
     def update(self, name):
-        ret = self._sio.call("agent", ("", "updateStack", name), timeout=10)
+        """
+        Updates a given stack
+        """
+        ret = self._sio.call("agent", ("", "updateStack", name), timeout=30)
         return ret
 
     def stop(self, name):
-        ret = self._sio.call("agent", ("", "stopStack", name), timeout=10)
+        """
+        Stops a given stack
+        """
+        ret = self._sio.call("agent", ("", "stopStack", name), timeout=30)
         return ret
 
     def start(self, name):
-        ret = self._sio.call("agent", ("", "startStack", name), timeout=10)
+        """
+        Starts a given stack
+        """
+        ret = self._sio.call("agent", ("", "startStack", name), timeout=30)
         return ret
 
     def down(self, name):
-        ret = self._sio.call("agent", ("", "downStack", name), timeout=10)
+        """
+        Stops and downs a given stack
+        """
+        ret = self._sio.call("agent", ("", "downStack", name), timeout=30)
         return ret
 
     def disconnect(self):
+        """
+        Logs out of dockge
+        """
         self._sio.emit("logout")
         self._sio.disconnect()
diff --git a/dockge_cli/service/storage.py b/dockge_cli/service/storage.py
index 0a3a0f8..3edf415 100644
--- a/dockge_cli/service/storage.py
+++ b/dockge_cli/service/storage.py
@@ -3,42 +3,82 @@ import pathlib
 import base64
 import yaml
 
-_storagepath = pathlib.Path(__file__).parents[1] / ".temp"
+_storagepath = pathlib.Path(__file__).parent / ".storage"
+_storagepath.mkdir(exist_ok=True, parents=True)
 _file = _storagepath / "storage.yaml"
 
-_storagepath.mkdir(exist_ok=True, parents=True)
+def create_file_when_missing():
+    """
+    Checks if storage file does exist, creates it when necessary
+    """
+    if _file.exists():
+        return
+    with open(_file, 'a', encoding="utf-8"):
+        os.utime(_file, None)
 
-def fileexists():
+def exists(key: str) -> bool:
+    """
+    Checks if a given key exists in the storage file
+    """
     if not _file.exists():
-        with open(_file, 'a', encoding="utf-8"):
-            os.utime(_file, None)
+        return False
+
+    with open(_file, "r", encoding="utf-8") as file:
+        content: dict[str, str] = yaml.load(file, Loader=yaml.SafeLoader)
+    
+    return key in content
+
 
 def put(key: str, value: str, encoded=False):
-    fileexists()
-    with open(_file, "r+", encoding="utf-8") as file:
+    """
+    Puts a given value with a given key into the storage file
+    Encodes the data as base64 when encoded is set to true
+    """
+    if not _file.exists():
+        create_file_when_missing()
+
+    with open(_file, "r", encoding="utf-8") as file:
         content: dict[str, str] = yaml.load(file, Loader=yaml.SafeLoader) or {}
-        content.update({ key: str(base64.b64encode(value.encode())) if encoded else value })
+        content.update({ key: str(base64.b64encode(value.encode()), "utf-8") if encoded else value })
+
     with open(_file, "w+", encoding="utf-8") as file:
         yaml.dump(content, file, Dumper=yaml.SafeDumper)
 
 def remove(key: str):
-    fileexists()
+    """
+    Removed a given key from the storage file
+    """
+    if not _file.exists():
+        create_file_when_missing()
+
     with open(_file, "r", encoding="utf-8") as file:
         content: dict[str, str] = yaml.load(file, Loader=yaml.SafeLoader) or {}
         content.pop(key, None)
+
     with open(_file, "w+", encoding="utf-8") as file:
         yaml.dump(content, file, Dumper=yaml.SafeDumper)
 
 def get(key: str, encoded=False):
+    """
+    Retrieves a value for a given key from the storage file
+    If the value was encoded, encoded needs to be set True to decode it again
+    """
     value: str | None = None
     if not _file.exists():
         return None
+
     with open(_file, "r", encoding="utf-8") as file:
         content: dict[str, str] = yaml.load(file, Loader=yaml.SafeLoader)
         value = content.get(key, None)
+
     if value is None:
         return None
-    return base64.b64decode(value).decode() if encoded else value
+    return base64.b64decode(value.encode()).decode() if encoded else value
 
 def clear():
+    """
+    Deletes the storage file
+    """
+    if not _file.exists():
+        return
     _file.unlink()
diff --git a/pyproject.toml b/pyproject.toml
index 50b898b..b706b37 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,9 +1,10 @@
 [project]
 name = "dockge_cli"
-version = "0.0.1-c.1"
+version = "0.1.2"
 dependencies = [
     "pyyaml~=6.0.1",
     "pydantic~=2.8.0",
+    "requests~=2.32.3",
     "python-socketio~=5.11.3",
     "websocket-client~=1.8.0",
     "tabulate ~=0.9.0",
@@ -11,7 +12,7 @@ dependencies = [
 requires-python = ">= 3.10"
 authors = [{name = "Firq", email = "firelp42@gmail.com"}]
 maintainers = [{name = "Firq", email = "firelp42@gmail.com"}]
-description = "CLi for interacting with dockge"
+description = "CLI for interacting with dockge"
 classifiers = [
     "Development Status :: 3 - Alpha",
     "Programming Language :: Python :: 3",
@@ -31,8 +32,30 @@ include = ["dockge_cli*"]
 [tool.setuptools.package-data]
 "*" = ["py.typed"]
 
+[project.optional-dependencies]
+lint = [
+    "pylint~=3.2.5",
+]
+typing = [
+    "mypy~=1.10.1",
+    "types-PyYAML~=6.0.12.20240311",
+    "types-tabulate~=0.9.0.20240106",
+]
+
 [tool.pylint."MAIN"]
-disable = [ "line-too-long", "missing-module-docstring", "missing-function-docstring", "missing-class-docstring" ]
+disable = [
+    "line-too-long",
+    "missing-module-docstring",
+]
+
+[tool.mypy]
+python_version = "3.11"
+warn_return_any = true
+warn_unused_configs = true
+
+[[tool.mypy.overrides]]
+module = 'socketio.*'
+ignore_missing_imports = true
 
 [build-system]
 requires = ["setuptools >= 61.0"]