diff --git a/src/repdav/config.py b/src/repdav/config.py
new file mode 100644
index 0000000000000000000000000000000000000000..3971a4efe31e468d9d26c05153d5938d04d52057
--- /dev/null
+++ b/src/repdav/config.py
@@ -0,0 +1,33 @@
+import logging
+import os
+from .errors import EnvNotSetError
+
+
+_logger = logging.getLogger(__name__)
+
+
+def lookup_env_name(internal_name: str) -> str:
+    mapping = {
+        "_auth_wsdl": "tg_auth_wsdl",
+        "_auth_address": "tg_auth_address",
+    }
+    return mapping[internal_name]
+
+
+class TextgridConfig:
+    def __init__(self):
+        self._auth_wsdl = os.getenv(lookup_env_name("_auth_wsdl"))
+        self._auth_address = os.getenv(lookup_env_name("_auth_address"))
+
+    @property
+    def auth_wsdl(self):
+        if self._auth_wsdl:
+            _logger.debug(self._auth_wsdl)
+            return self._auth_wsdl
+        raise EnvNotSetError(lookup_env_name("_auth_wsdl"))
+
+    @property
+    def auth_address(self):
+        if self._auth_address:
+            return self._auth_address
+        raise EnvNotSetError(lookup_env_name("_auth_address"))
diff --git a/src/repdav/errors.py b/src/repdav/errors.py
new file mode 100644
index 0000000000000000000000000000000000000000..6f994638674a96b5f4323af818e7e6e56155d275
--- /dev/null
+++ b/src/repdav/errors.py
@@ -0,0 +1,23 @@
+"""Custom error classes
+"""
+import logging
+#from wsgidav import dav_error
+
+_logger = logging.getLogger(__name__)
+
+
+class _ConfigError(Exception):
+    """Base class for config errors. Do not import directly.
+    """
+    pass
+
+
+class EnvNotSetError(_ConfigError):
+    """Exception to raise if an environment variable is not set (correctly).
+    """
+
+    def __init__(self, env_name):
+        self.env_name = env_name
+        self.message = "Environment variable '%s' is not set."
+        _logger.error(self.message, self.env_name)
+        super().__init__(self.message)
diff --git a/src/repdav/tgapi.py b/src/repdav/tgapi.py
new file mode 100644
index 0000000000000000000000000000000000000000..447ee6eab1aa40a624542b461ee223ea3df2bf65
--- /dev/null
+++ b/src/repdav/tgapi.py
@@ -0,0 +1,32 @@
+#from typing import List
+from zeep import Client
+from zeep.exceptions import TransportError
+
+from .config import TextgridConfig
+
+
+class TextgridAuth:
+    def __init__(self):
+        self._config = TextgridConfig()
+
+    def _connect(self) -> Client:
+        """Internal helper that provides a SOAP client that is configured for
+        the use with the Textgrid Auth service.
+
+        Returns:
+            Client: A SOAP client.
+        """
+        client = Client(self._config.auth_wsdl)
+        # this is dirty hack; should be remediated
+        client.service._binding_options["address"] = self._config.auth_address
+        return client
+
+    # replace ":" with " -> List | None:" when switching to python3.10
+    def assigned_projects(self, sid: str):
+        """Return an array of project id strings
+        """
+        client = self._connect()
+        try:
+            return client.service.tgAssignedProjects(sid)
+        except TransportError:
+            return None