diff --git a/apps/tenant-manager/.env.example b/apps/tenant-manager/.env.example
new file mode 100644
index 0000000000000000000000000000000000000000..2a6ace4061199a053e53ecc5dd5582ffcad45c7b
--- /dev/null
+++ b/apps/tenant-manager/.env.example
@@ -0,0 +1,6 @@
+HTTP_HOSTNAME=0.0.0.0
+HTTP_PORT=4007
+NATS_URL=nats://localhost:4222
+NATS_USER=nats_user
+NATS_PASSWORD=nats_password
+NATS_MONITORING_URL=http://localhost:8222
diff --git a/apps/tenant-manager/README.md b/apps/tenant-manager/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..a57487e62ae6b9aab371c9fbcadd97860043e44a
--- /dev/null
+++ b/apps/tenant-manager/README.md
@@ -0,0 +1,32 @@
+# Gaia-X OCM Tenant Manager
+
+The Gaia-X OCM Tenant Manager is a component of the Gaia-X Open Common Metadata (OCM) project. It provides functionality for managing tenants within the Gaia-X ecosystem.
+
+## Table of Contents
+
+- [Introduction](#introduction)
+- [Features](#features)
+- [Installation](#installation)
+- [Usage](#usage)
+- [Contributing](#contributing)
+- [License](#license)
+
+## Introduction
+
+The Gaia-X OCM Tenant Manager is designed to facilitate the management of tenants in the Gaia-X ecosystem. It allows users to create, update, and delete tenants, as well as manage their associated resources.
+
+## Features
+
+- Create new tenants
+- Update existing tenants
+- Delete tenants
+- Manage tenant resources
+
+## Installation
+
+To install the Gaia-X OCM Tenant Manager, follow these steps:
+
+1. Clone the repository:
+
+   ```bash
+   git clone https://github.com/gaia-x/ocm-tenant-manager.git
diff --git a/apps/tenant-manager/deployment/helm/.helmignore b/apps/tenant-manager/deployment/helm/.helmignore
new file mode 100644
index 0000000000000000000000000000000000000000..0e8a0eb36f4ca2c939201c0d54b5d82a1ea34778
--- /dev/null
+++ b/apps/tenant-manager/deployment/helm/.helmignore
@@ -0,0 +1,23 @@
+# Patterns to ignore when building packages.
+# This supports shell glob matching, relative path matching, and
+# negation (prefixed with !). Only one pattern per line.
+.DS_Store
+# Common VCS dirs
+.git/
+.gitignore
+.bzr/
+.bzrignore
+.hg/
+.hgignore
+.svn/
+# Common backup files
+*.swp
+*.bak
+*.tmp
+*.orig
+*~
+# Various IDEs
+.project
+.idea/
+*.tmproj
+.vscode/
diff --git a/apps/tenant-manager/deployment/helm/Chart.yaml b/apps/tenant-manager/deployment/helm/Chart.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..ddcfb5d6390265482f38adb45e04c53792854b65
--- /dev/null
+++ b/apps/tenant-manager/deployment/helm/Chart.yaml
@@ -0,0 +1,24 @@
+apiVersion: v2
+name: tenant-manager
+description: OCM Tenant Manager Helm chart
+
+# A chart can be either an 'application' or a 'library' chart.
+#
+# Application charts are a collection of templates that can be packaged into versioned archives
+# to be deployed.
+#
+# Library charts provide useful utilities or functions for the chart developer. They're included as
+# a dependency of application charts to inject those utilities and functions into the rendering
+# pipeline. Library charts do not define any templates and therefore cannot be deployed.
+type: application
+
+# This is the chart version. This version number should be incremented each time you make changes
+# to the chart and its templates, including the app version.
+# Versions are expected to follow Semantic Versioning (https://semver.org/)
+version: 1.1.0
+
+# This is the version number of the application being deployed. This version number should be
+# incremented each time you make changes to the application. Versions are not expected to
+# follow Semantic Versioning. They should reflect the version the application is using.
+# It is recommended to use it with quotes.
+appVersion: "1.0.0"
diff --git a/apps/tenant-manager/deployment/helm/templates/NOTES.txt b/apps/tenant-manager/deployment/helm/templates/NOTES.txt
new file mode 100644
index 0000000000000000000000000000000000000000..49a9a7c47cf3b4a4366104fe6d55d43a84f39f9c
--- /dev/null
+++ b/apps/tenant-manager/deployment/helm/templates/NOTES.txt
@@ -0,0 +1,22 @@
+1. Get the application URL by running these commands:
+{{- if .Values.ingress.enabled }}
+{{- range $host := .Values.ingress.hosts }}
+  {{- range .paths }}
+  http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }}
+  {{- end }}
+{{- end }}
+{{- else if contains "NodePort" .Values.service.type }}
+  export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "tenant-manager.fullname" . }})
+  export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}")
+  echo http://$NODE_IP:$NODE_PORT
+{{- else if contains "LoadBalancer" .Values.service.type }}
+     NOTE: It may take a few minutes for the LoadBalancer IP to be available.
+           You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "tenant-manager.fullname" . }}'
+  export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "tenant-manager.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}")
+  echo http://$SERVICE_IP:{{ .Values.service.port }}
+{{- else if contains "ClusterIP" .Values.service.type }}
+  export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "tenant-manager.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}")
+  export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}")
+  echo "Visit http://127.0.0.1:8080 to use your application"
+  kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT
+{{- end }}
diff --git a/apps/tenant-manager/deployment/helm/templates/_helpers.tpl b/apps/tenant-manager/deployment/helm/templates/_helpers.tpl
new file mode 100644
index 0000000000000000000000000000000000000000..ce20e32a0a1e0341ea3b5dfe08da7dc9bffa330e
--- /dev/null
+++ b/apps/tenant-manager/deployment/helm/templates/_helpers.tpl
@@ -0,0 +1,62 @@
+{{/*
+Expand the name of the chart.
+*/}}
+{{- define "tenant-manager.name" -}}
+{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
+{{- end }}
+
+{{/*
+Create a default fully qualified app name.
+We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
+If release name contains chart name it will be used as a full name.
+*/}}
+{{- define "tenant-manager.fullname" -}}
+{{- if .Values.fullnameOverride }}
+{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
+{{- else }}
+{{- $name := default .Chart.Name .Values.nameOverride }}
+{{- if contains $name .Release.Name }}
+{{- .Release.Name | trunc 63 | trimSuffix "-" }}
+{{- else }}
+{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
+{{- end }}
+{{- end }}
+{{- end }}
+
+{{/*
+Create chart name and version as used by the chart label.
+*/}}
+{{- define "tenant-manager.chart" -}}
+{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
+{{- end }}
+
+{{/*
+Common labels
+*/}}
+{{- define "tenant-manager.labels" -}}
+helm.sh/chart: {{ include "tenant-manager.chart" . }}
+{{ include "tenant-manager.selectorLabels" . }}
+{{- if .Chart.AppVersion }}
+app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
+{{- end }}
+app.kubernetes.io/managed-by: {{ .Release.Service }}
+{{- end }}
+
+{{/*
+Selector labels
+*/}}
+{{- define "tenant-manager.selectorLabels" -}}
+app.kubernetes.io/name: {{ include "tenant-manager.name" . }}
+app.kubernetes.io/instance: {{ .Release.Name }}
+{{- end }}
+
+{{/*
+Create the name of the service account to use
+*/}}
+{{- define "tenant-manager.serviceAccountName" -}}
+{{- if .Values.serviceAccount.create }}
+{{- default (include "tenant-manager.fullname" .) .Values.serviceAccount.name }}
+{{- else }}
+{{- default "default" .Values.serviceAccount.name }}
+{{- end }}
+{{- end }}
diff --git a/apps/tenant-manager/deployment/helm/templates/deployment.yaml b/apps/tenant-manager/deployment/helm/templates/deployment.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..7567fc5e1ac6bab5f6fd76c0dd32befcbe991e9a
--- /dev/null
+++ b/apps/tenant-manager/deployment/helm/templates/deployment.yaml
@@ -0,0 +1,80 @@
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: {{ include "tenant-manager.fullname" . }}
+  labels:
+    {{- include "tenant-manager.labels" . | nindent 4 }}
+spec:
+  {{- if not .Values.autoscaling.enabled }}
+  replicas: {{ .Values.replicaCount }}
+  {{- end }}
+  selector:
+    matchLabels:
+      {{- include "tenant-manager.selectorLabels" . | nindent 6 }}
+  template:
+    metadata:
+      {{- with .Values.podAnnotations }}
+      annotations:
+        {{- toYaml . | nindent 8 }}
+      {{- end }}
+      labels:
+        {{- include "tenant-manager.labels" . | nindent 8 }}
+        {{- with .Values.podLabels }}
+        {{- toYaml . | nindent 8 }}
+        {{- end }}
+    spec:
+      {{- with .Values.imagePullSecrets }}
+      imagePullSecrets:
+        {{- toYaml . | nindent 8 }}
+      {{- end }}
+      serviceAccountName: {{ include "tenant-manager.serviceAccountName" . }}
+      securityContext:
+        {{- toYaml .Values.podSecurityContext | nindent 8 }}
+      containers:
+        - name: {{ .Chart.Name }}
+          securityContext:
+            {{- toYaml .Values.securityContext | nindent 12 }}
+          image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
+          imagePullPolicy: {{ .Values.image.pullPolicy }}
+          env:
+            - name: HTTP_HOSTNAME
+              value: {{ .Values.http.hostname }}
+            - name: HTTP_PORT
+              value: {{ .Values.http.port | quote }}
+          envFrom:
+            - configMapRef:
+                name: ocm-config-map
+            - secretRef:
+                name: ocm-secret
+          ports:
+            - name: http
+              containerPort: {{ .Values.http.port }}
+              protocol: TCP
+          livenessProbe:
+            {{- toYaml .Values.livenessProbe | nindent 12 }}
+          readinessProbe:
+            {{- toYaml .Values.readinessProbe | nindent 12 }}
+          startupProbe:
+            {{- toYaml .Values.startupProbe | nindent 12 }}
+          resources:
+            {{- toYaml .Values.resources | nindent 12 }}
+          {{- with .Values.volumeMounts }}
+          volumeMounts:
+            {{- toYaml . | nindent 12 }}
+          {{- end }}
+      {{- with .Values.volumes }}
+      volumes:
+        {{- toYaml . | nindent 8 }}
+      {{- end }}
+      {{- with .Values.nodeSelector }}
+      nodeSelector:
+        {{- toYaml . | nindent 8 }}
+      {{- end }}
+      {{- with .Values.affinity }}
+      affinity:
+        {{- toYaml . | nindent 8 }}
+      {{- end }}
+      {{- with .Values.tolerations }}
+      tolerations:
+        {{- toYaml . | nindent 8 }}
+      {{- end }}
diff --git a/apps/tenant-manager/deployment/helm/templates/hpa.yaml b/apps/tenant-manager/deployment/helm/templates/hpa.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..257f1f445fc97398bc7ecbbc9cc6cc074b6a1ad3
--- /dev/null
+++ b/apps/tenant-manager/deployment/helm/templates/hpa.yaml
@@ -0,0 +1,32 @@
+{{- if .Values.autoscaling.enabled }}
+apiVersion: autoscaling/v2
+kind: HorizontalPodAutoscaler
+metadata:
+  name: {{ include "tenant-manager.fullname" . }}
+  labels:
+    {{- include "tenant-manager.labels" . | nindent 4 }}
+spec:
+  scaleTargetRef:
+    apiVersion: apps/v1
+    kind: Deployment
+    name: {{ include "tenant-manager.fullname" . }}
+  minReplicas: {{ .Values.autoscaling.minReplicas }}
+  maxReplicas: {{ .Values.autoscaling.maxReplicas }}
+  metrics:
+    {{- if .Values.autoscaling.targetCPUUtilizationPercentage }}
+    - type: Resource
+      resource:
+        name: cpu
+        target:
+          type: Utilization
+          averageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }}
+    {{- end }}
+    {{- if .Values.autoscaling.targetMemoryUtilizationPercentage }}
+    - type: Resource
+      resource:
+        name: memory
+        target:
+          type: Utilization
+          averageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }}
+    {{- end }}
+{{- end }}
diff --git a/apps/tenant-manager/deployment/helm/templates/ingress.yaml b/apps/tenant-manager/deployment/helm/templates/ingress.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..d82cc3854d461a84e5c5f220c3f50fc63718e1cb
--- /dev/null
+++ b/apps/tenant-manager/deployment/helm/templates/ingress.yaml
@@ -0,0 +1,61 @@
+{{- if .Values.ingress.enabled -}}
+{{- $fullName := include "tenant-manager.fullname" . -}}
+{{- $svcPort := .Values.service.port -}}
+{{- if and .Values.ingress.className (not (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion)) }}
+  {{- if not (hasKey .Values.ingress.annotations "kubernetes.io/ingress.class") }}
+  {{- $_ := set .Values.ingress.annotations "kubernetes.io/ingress.class" .Values.ingress.className}}
+  {{- end }}
+{{- end }}
+{{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.GitVersion -}}
+apiVersion: networking.k8s.io/v1
+{{- else if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}}
+apiVersion: networking.k8s.io/v1beta1
+{{- else -}}
+apiVersion: extensions/v1beta1
+{{- end }}
+kind: Ingress
+metadata:
+  name: {{ $fullName }}
+  labels:
+    {{- include "tenant-manager.labels" . | nindent 4 }}
+  {{- with .Values.ingress.annotations }}
+  annotations:
+    {{- toYaml . | nindent 4 }}
+  {{- end }}
+spec:
+  {{- if and .Values.ingress.className (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion) }}
+  ingressClassName: {{ .Values.ingress.className }}
+  {{- end }}
+  {{- if .Values.ingress.tls }}
+  tls:
+    {{- range .Values.ingress.tls }}
+    - hosts:
+        {{- range .hosts }}
+        - {{ . | quote }}
+        {{- end }}
+      secretName: {{ .secretName }}
+    {{- end }}
+  {{- end }}
+  rules:
+    {{- range .Values.ingress.hosts }}
+    - host: {{ .host | quote }}
+      http:
+        paths:
+          {{- range .paths }}
+          - path: {{ .path }}
+            {{- if and .pathType (semverCompare ">=1.18-0" $.Capabilities.KubeVersion.GitVersion) }}
+            pathType: {{ .pathType }}
+            {{- end }}
+            backend:
+              {{- if semverCompare ">=1.19-0" $.Capabilities.KubeVersion.GitVersion }}
+              service:
+                name: {{ $fullName }}
+                port:
+                  number: {{ $svcPort }}
+              {{- else }}
+              serviceName: {{ $fullName }}
+              servicePort: {{ $svcPort }}
+              {{- end }}
+          {{- end }}
+    {{- end }}
+{{- end }}
diff --git a/apps/tenant-manager/deployment/helm/templates/service.yaml b/apps/tenant-manager/deployment/helm/templates/service.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..90cb898c4f25625f903a043659e6dae885d5bf00
--- /dev/null
+++ b/apps/tenant-manager/deployment/helm/templates/service.yaml
@@ -0,0 +1,15 @@
+apiVersion: v1
+kind: Service
+metadata:
+  name: {{ include "tenant-manager.fullname" . }}
+  labels:
+    {{- include "tenant-manager.labels" . | nindent 4 }}
+spec:
+  type: {{ .Values.service.type }}
+  ports:
+    - port: {{ .Values.service.port }}
+      targetPort: http
+      protocol: TCP
+      name: http
+  selector:
+    {{- include "tenant-manager.selectorLabels" . | nindent 4 }}
diff --git a/apps/tenant-manager/deployment/helm/templates/serviceaccount.yaml b/apps/tenant-manager/deployment/helm/templates/serviceaccount.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..9184c9094f31d4c564a99a722ab290a82ee7f338
--- /dev/null
+++ b/apps/tenant-manager/deployment/helm/templates/serviceaccount.yaml
@@ -0,0 +1,13 @@
+{{- if .Values.serviceAccount.create -}}
+apiVersion: v1
+kind: ServiceAccount
+metadata:
+  name: {{ include "tenant-manager.serviceAccountName" . }}
+  labels:
+    {{- include "tenant-manager.labels" . | nindent 4 }}
+  {{- with .Values.serviceAccount.annotations }}
+  annotations:
+    {{- toYaml . | nindent 4 }}
+  {{- end }}
+automountServiceAccountToken: {{ .Values.serviceAccount.automount }}
+{{- end }}
diff --git a/apps/tenant-manager/deployment/helm/values.yaml b/apps/tenant-manager/deployment/helm/values.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..bbb14419dc35ecdf92a958461213b49e33a29923
--- /dev/null
+++ b/apps/tenant-manager/deployment/helm/values.yaml
@@ -0,0 +1,121 @@
+# Default values for connection-manager.
+# This is a YAML-formatted file.
+# Declare variables to be passed into your templates.
+
+replicaCount: 1
+
+image:
+  repository: node-654e3bca7fbeeed18f81d7c7.ps-xaas.io/ocm/tenant-manager
+  name: tenant-manager
+  tag: main
+  sha: ""
+  pullPolicy: IfNotPresent
+  pullSecrets: key
+
+imagePullSecrets: []
+nameOverride: ""
+fullnameOverride: ""
+
+serviceAccount:
+  # Specifies whether a service account should be created
+  create: false
+  # Automatically mount a ServiceAccount's API credentials?
+  automount: true
+  # Annotations to add to the service account
+  annotations: {}
+  # The name of the service account to use.
+  # If not set and create is true, a name is generated using the fullname template
+  name: ocm-service-account
+
+podAnnotations: {}
+podLabels: {}
+
+podSecurityContext:
+  {}
+  # fsGroup: 2000
+
+securityContext:
+  {}
+  # capabilities:
+  #   drop:
+  #   - ALL
+  # readOnlyRootFilesystem: true
+  # runAsNonRoot: true
+  # runAsUser: 1000
+
+service:
+  type: ClusterIP
+  port: 80
+
+ingress:
+  enabled: false
+  className: ""
+  annotations:
+    {}
+    # kubernetes.io/ingress.class: nginx
+    # kubernetes.io/tls-acme: "true"
+  hosts:
+    - host: chart-example.local
+      paths:
+        - path: /
+          pathType: ImplementationSpecific
+  tls: []
+  #  - secretName: chart-example-tls
+  #    hosts:
+  #      - chart-example.local
+
+resources:
+  {}
+  # We usually recommend not to specify default resources and to leave this as a conscious
+  # choice for the user. This also increases chances charts run on environments with little
+  # resources, such as Minikube. If you do want to specify resources, uncomment the following
+  # lines, adjust them as necessary, and remove the curly braces after 'resources:'.
+  # limits:
+  #   cpu: 100m
+  #   memory: 128Mi
+  # requests:
+  #   cpu: 100m
+  #   memory: 128Mi
+
+livenessProbe:
+  httpGet:
+    path: /health
+    port: http
+readinessProbe:
+  httpGet:
+    path: /health
+    port: http
+startupProbe:
+  httpGet:
+    path: /health
+    port: http
+
+autoscaling:
+  enabled: false
+  minReplicas: 1
+  maxReplicas: 100
+  targetCPUUtilizationPercentage: 80
+  # targetMemoryUtilizationPercentage: 80
+
+# Additional volumes on the output Deployment definition.
+volumes: []
+# - name: foo
+#   secret:
+#     secretName: mysecret
+#     optional: false
+
+# Additional volumeMounts on the output Deployment definition.
+volumeMounts: []
+# - name: foo
+#   mountPath: "/etc/foo"
+#   readOnly: true
+
+nodeSelector: {}
+
+tolerations: []
+
+affinity: {}
+
+http:
+  hostname: "0.0.0.0"
+  port: 3000
diff --git a/apps/tenant-manager/jest.config.js b/apps/tenant-manager/jest.config.js
new file mode 100644
index 0000000000000000000000000000000000000000..ccdd468df2bf90570fb54087fa7dea267814c888
--- /dev/null
+++ b/apps/tenant-manager/jest.config.js
@@ -0,0 +1,48 @@
+import { readFileSync } from 'node:fs';
+
+const swcConfig = JSON.parse(readFileSync('../../.swcrc', 'utf8'));
+
+/** @type {import('jest').Config} */
+export default {
+  moduleFileExtensions: ['js', 'ts'],
+  testEnvironment: 'node',
+  transform: {
+    '^.+\\.(js|ts)$': [
+      '@swc/jest',
+      {
+        ...swcConfig,
+        sourceMaps: false,
+        exclude: [],
+        swcrc: false,
+      },
+    ],
+  },
+  extensionsToTreatAsEsm: ['.ts'],
+  moduleNameMapper: {
+    // ESM modules require `.js` extension to be specified, but Jest doesn't work with them
+    // Removing `.js` extension from module imports
+    '^uuid$': 'uuid',
+    '^(.*)/(.*)\\.js$': '$1/$2',
+  },
+  collectCoverageFrom: ['src/**/*.(t|j)s'],
+  coverageReporters:
+    process.env.CI === 'true'
+      ? ['text-summary', 'json-summary']
+      : ['text-summary', 'html'],
+  coveragePathIgnorePatterns: [
+    '<rootDir>/node_modules/',
+    '<rootDir>/coverage/',
+    '<rootDir>/dist/',
+    '__tests__',
+    '@types',
+    '.dto.(t|j)s',
+    '.enum.ts',
+    '.interface.ts',
+    '.type.ts',
+    '.spec.ts',
+  ],
+  coverageDirectory: './coverage',
+  // With v8 coverage provider it's much faster, but
+  // with this enabled it's not possible to ignore whole files' coverage
+  coverageProvider: 'v8',
+};
diff --git a/apps/tenant-manager/nest-cli.json b/apps/tenant-manager/nest-cli.json
new file mode 100644
index 0000000000000000000000000000000000000000..b9af737f405bfea055dcb58728c31d912fef06f3
--- /dev/null
+++ b/apps/tenant-manager/nest-cli.json
@@ -0,0 +1,14 @@
+{
+  "$schema": "https://json.schemastore.org/nest-cli",
+  "collection": "@nestjs/schematics",
+  "sourceRoot": "src",
+  "compilerOptions": {
+    "typeCheck": true,
+    "builder": {
+      "type": "swc",
+      "options": {
+        "swcrcPath": "../../.swcrc"
+      }
+    }
+  }
+}
diff --git a/apps/tenant-manager/package.json b/apps/tenant-manager/package.json
new file mode 100644
index 0000000000000000000000000000000000000000..3a5f8c05b25730a53af612568b1f567b2bd0155f
--- /dev/null
+++ b/apps/tenant-manager/package.json
@@ -0,0 +1,50 @@
+{
+  "name": "@ocm/tenant-manager",
+  "version": "1.0.0",
+  "description": "Gaia-X OCM Tenant Manager",
+  "author": "Gaia-X",
+  "contributors": [
+    "Konstantin Tsabolov <konstantin.tsabolov@spherity.com>"
+  ],
+  "private": true,
+  "license": "Apache-2.0",
+  "type": "module",
+  "scripts": {
+    "clean": "rimraf dist coverage *.tsbuildinfo",
+    "prebuild": "pnpm clean",
+    "build": "nest build -p tsconfig.production.json",
+    "start": "nest start --watch --preserveWatchOutput",
+    "test": "jest"
+  },
+  "dependencies": {
+    "@nestjs/common": "10.3.3",
+    "@nestjs/config": "3.2.0",
+    "@nestjs/core": "10.3.3",
+    "@nestjs/microservices": "10.3.3",
+    "@nestjs/platform-express": "10.3.3",
+    "@nestjs/swagger": "7.3.0",
+    "@ocm/shared": "workspace:*",
+    "class-transformer": "0.5.1",
+    "class-validator": "0.14.1",
+    "express": "4.18.2",
+    "helmet": "7.1.0",
+    "joi": "17.12.1",
+    "nats": "2.19.0",
+    "reflect-metadata": "0.2.1",
+    "rxjs": "7.8.1"
+  },
+  "devDependencies": {
+    "@nestjs/cli": "10.3.2",
+    "@nestjs/schematics": "10.1.1",
+    "@nestjs/testing": "10.3.3",
+    "@swc/cli": "0.3.9",
+    "@swc/core": "1.4.2",
+    "@swc/jest": "0.2.36",
+    "@types/express": "4.17.21",
+    "@types/jest": "29.5.12",
+    "@types/node": "20.11.19",
+    "jest": "29.7.0",
+    "rimraf": "5.0.5",
+    "typescript": "5.3.3"
+  }
+}
diff --git a/apps/tenant-manager/src/application.ts b/apps/tenant-manager/src/application.ts
new file mode 100644
index 0000000000000000000000000000000000000000..4931e8459d943117fb3922aa97d229c019c1dbce
--- /dev/null
+++ b/apps/tenant-manager/src/application.ts
@@ -0,0 +1,79 @@
+import type { OnApplicationBootstrap } from '@nestjs/common';
+import type { ConfigType } from '@nestjs/config';
+
+import { Inject, Module } from '@nestjs/common';
+import { ConfigModule } from '@nestjs/config';
+import { RouterModule } from '@nestjs/core';
+import { ClientProxy, ClientsModule, Transport } from '@nestjs/microservices';
+import { HealthModule } from '@ocm/shared';
+
+import { NATS_CLIENT } from './common/constants.js';
+import { httpConfig } from './config/http.config.js';
+import { natsConfig } from './config/nats.config.js';
+import { validationSchema } from './config/validation.js';
+import { TenantsModule } from './tenants/tenants.module.js';
+
+@Module({
+  imports: [
+    ConfigModule.forRoot({
+      isGlobal: true,
+      load: [httpConfig, natsConfig],
+      cache: true,
+      expandVariables: true,
+      validationSchema,
+      validationOptions: {
+        allowUnknown: true,
+        abortEarly: true,
+      },
+    }),
+
+    ClientsModule.registerAsync({
+      isGlobal: true,
+      clients: [
+        {
+          name: NATS_CLIENT,
+          inject: [natsConfig.KEY],
+          useFactory: (config: ConfigType<typeof natsConfig>) => ({
+            transport: Transport.NATS,
+            options: {
+              servers: [config.url],
+              user: config.user as string,
+              pass: config.password as string,
+            },
+          }),
+        },
+      ],
+    }),
+
+    HealthModule.registerAsync({
+      inject: [natsConfig.KEY],
+      useFactory: (config: ConfigType<typeof natsConfig>) => {
+        const options: Parameters<typeof HealthModule.register>[0] = {};
+
+        if (config.monitoringUrl) {
+          options.nats = {
+            monitoringUrl: config.monitoringUrl as string,
+          };
+        }
+
+        return options;
+      },
+    }),
+
+    TenantsModule,
+
+    RouterModule.register([
+      { module: HealthModule, path: '/health' },
+      { module: TenantsModule, path: '/tenants' },
+    ]),
+  ],
+})
+export class Application implements OnApplicationBootstrap {
+  public constructor(
+    @Inject(NATS_CLIENT) private readonly client: ClientProxy,
+  ) {}
+
+  public async onApplicationBootstrap(): Promise<void> {
+    await this.client.connect();
+  }
+}
diff --git a/apps/tenant-manager/src/common/constants.ts b/apps/tenant-manager/src/common/constants.ts
new file mode 100644
index 0000000000000000000000000000000000000000..6cc64117bcb8334cbfc31878fc490e0ebe9baef7
--- /dev/null
+++ b/apps/tenant-manager/src/common/constants.ts
@@ -0,0 +1,2 @@
+export const SERVICE_NAME = 'TENANT_MANAGER_SERVICE';
+export const NATS_CLIENT = Symbol('NATS_CLIENT');
diff --git a/apps/tenant-manager/src/config/http.config.ts b/apps/tenant-manager/src/config/http.config.ts
new file mode 100644
index 0000000000000000000000000000000000000000..c7b07c2dfdb215018bf33eb2083541e6809bdcd2
--- /dev/null
+++ b/apps/tenant-manager/src/config/http.config.ts
@@ -0,0 +1,6 @@
+import { registerAs } from '@nestjs/config';
+
+export const httpConfig = registerAs('http', () => ({
+  hostname: process.env.HTTP_HOSTNAME || '0.0.0.0',
+  port: Number(process.env.HTTP_PORT) || 3000,
+}));
diff --git a/apps/tenant-manager/src/config/nats.config.ts b/apps/tenant-manager/src/config/nats.config.ts
new file mode 100644
index 0000000000000000000000000000000000000000..194053c2e2e44070e34b8547b4a15819d02d9b75
--- /dev/null
+++ b/apps/tenant-manager/src/config/nats.config.ts
@@ -0,0 +1,8 @@
+import { registerAs } from '@nestjs/config';
+
+export const natsConfig = registerAs('nats', () => ({
+  url: process.env.NATS_URL || 'nats://localhost:4222',
+  user: process.env.NATS_USER,
+  password: process.env.NATS_PASSWORD,
+  monitoringUrl: process.env.NATS_MONITORING_URL || 'http://localhost:8222',
+}));
diff --git a/apps/tenant-manager/src/config/validation.ts b/apps/tenant-manager/src/config/validation.ts
new file mode 100644
index 0000000000000000000000000000000000000000..f963431550e25f1fe5f7db6fca536ffbb0c80144
--- /dev/null
+++ b/apps/tenant-manager/src/config/validation.ts
@@ -0,0 +1,11 @@
+import Joi from 'joi';
+
+export const validationSchema = Joi.object({
+  HTTP_HOSTNAME: Joi.string(),
+  HTTP_PORT: Joi.number(),
+
+  NATS_URL: Joi.string().uri(),
+  NATS_USER: Joi.string().optional(),
+  NATS_PASSWORD: Joi.string().optional(),
+  NATS_MONITORING_URL: Joi.string().uri(),
+});
diff --git a/apps/tenant-manager/src/main.ts b/apps/tenant-manager/src/main.ts
new file mode 100644
index 0000000000000000000000000000000000000000..e926eb71b8974e653e16cfc7f2f68901303e2825
--- /dev/null
+++ b/apps/tenant-manager/src/main.ts
@@ -0,0 +1,46 @@
+/* c8 ignore start */
+import type { ConfigType } from '@nestjs/config';
+
+import { Logger, VersioningType } from '@nestjs/common';
+import { NestFactory } from '@nestjs/core';
+import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
+import helmet from 'helmet';
+import { createRequire } from 'module';
+import { resolve } from 'node:path';
+
+import { Application } from './application.js';
+import { httpConfig } from './config/http.config.js';
+
+const pkgPath = resolve('package.json');
+const pkg = createRequire(import.meta.url)(pkgPath);
+
+const app = await NestFactory.create(Application);
+
+app.use(helmet());
+
+app.enableVersioning({
+  defaultVersion: ['1'],
+  type: VersioningType.URI,
+});
+
+const swaggerConfig = new DocumentBuilder()
+  .setTitle(pkg.description)
+  .setVersion(pkg.version)
+  .build();
+
+const document = SwaggerModule.createDocument(app, swaggerConfig);
+
+SwaggerModule.setup('/', app, document, {
+  swaggerOptions: {
+    docExpansion: 'none',
+    tryItOutEnabled: true,
+  },
+});
+
+const { hostname, port } = app.get(httpConfig.KEY) as ConfigType<
+  typeof httpConfig
+>;
+await app.listen(port, hostname);
+
+Logger.log(`Application is running on: ${await app.getUrl()}`);
+/* c8 ignore stop */
diff --git a/apps/tenant-manager/src/tenants/__tests__/tenants.controller.spec.ts b/apps/tenant-manager/src/tenants/__tests__/tenants.controller.spec.ts
new file mode 100644
index 0000000000000000000000000000000000000000..74fd88f1adf502e8c2140eabb7f83ad5bee372de
--- /dev/null
+++ b/apps/tenant-manager/src/tenants/__tests__/tenants.controller.spec.ts
@@ -0,0 +1,74 @@
+import type { TestingModule } from '@nestjs/testing';
+import type { EventTenantsCreate } from '@ocm/shared';
+
+import { Test } from '@nestjs/testing';
+import { Subject, of, takeUntil } from 'rxjs';
+
+import { NATS_CLIENT } from '../../common/constants.js';
+import { TenantsController } from '../tenants.controller.js';
+import { TenantsService } from '../tenants.service.js';
+
+describe('TenantsController', () => {
+  const natsClientMock = {};
+
+  let controller: TenantsController;
+  let service: TenantsService;
+
+  beforeEach(async () => {
+    const module: TestingModule = await Test.createTestingModule({
+      controllers: [TenantsController],
+      providers: [
+        { provide: NATS_CLIENT, useValue: natsClientMock },
+        TenantsService,
+      ],
+    }).compile();
+
+    controller = module.get<TenantsController>(TenantsController);
+    service = module.get<TenantsService>(TenantsService);
+  });
+
+  describe('create', () => {
+    it('should call service.create with the correct arguments', (done) => {
+      const unsubscribe$ = new Subject<void>();
+      const label = 'label';
+      const expectedResult = {} as EventTenantsCreate['data'];
+
+      jest.spyOn(service, 'create').mockReturnValueOnce(of(expectedResult));
+
+      controller
+        .create({ label })
+        .pipe(takeUntil(unsubscribe$))
+        .subscribe((result) => {
+          expect(service.create).toHaveBeenCalledWith(label);
+          expect(result).toStrictEqual(expectedResult);
+
+          unsubscribe$.next();
+          unsubscribe$.complete();
+
+          done();
+        });
+    });
+  });
+
+  describe('find', () => {
+    it('should call service.find with the correct arguments', (done) => {
+      const unsubscribe$ = new Subject<void>();
+      const expectedResult = [] as string[];
+
+      jest.spyOn(service, 'find').mockReturnValueOnce(of(expectedResult));
+
+      controller
+        .find()
+        .pipe(takeUntil(unsubscribe$))
+        .subscribe((result) => {
+          expect(service.find).toHaveBeenCalledWith();
+          expect(result).toStrictEqual(expectedResult);
+
+          unsubscribe$.next();
+          unsubscribe$.complete();
+
+          done();
+        });
+    });
+  });
+});
diff --git a/apps/tenant-manager/src/tenants/__tests__/tenants.module.spec.ts b/apps/tenant-manager/src/tenants/__tests__/tenants.module.spec.ts
new file mode 100644
index 0000000000000000000000000000000000000000..80c0a9090459fa27c3c819f0a00204bb7cd5ab60
--- /dev/null
+++ b/apps/tenant-manager/src/tenants/__tests__/tenants.module.spec.ts
@@ -0,0 +1,35 @@
+import { ClientsModule } from '@nestjs/microservices';
+import { Test } from '@nestjs/testing';
+
+import { NATS_CLIENT } from '../../common/constants.js';
+import { TenantsController } from '../tenants.controller.js';
+import { TenantsModule } from '../tenants.module.js';
+import { TenantsService } from '../tenants.service.js';
+
+describe('TenantsModule', () => {
+  let tenantsController: TenantsController;
+  let tenantsService: TenantsService;
+
+  beforeEach(async () => {
+    const moduleRef = await Test.createTestingModule({
+      imports: [
+        ClientsModule.registerAsync({
+          isGlobal: true,
+          clients: [{ name: NATS_CLIENT, useFactory: () => ({}) }],
+        }),
+        TenantsModule,
+      ],
+    }).compile();
+
+    tenantsController = moduleRef.get<TenantsController>(TenantsController);
+    tenantsService = moduleRef.get<TenantsService>(TenantsService);
+  });
+
+  it('should be defined', () => {
+    expect(tenantsController).toBeDefined();
+    expect(tenantsController).toBeInstanceOf(TenantsController);
+
+    expect(tenantsService).toBeDefined();
+    expect(tenantsService).toBeInstanceOf(TenantsService);
+  });
+});
diff --git a/apps/tenant-manager/src/tenants/__tests__/tenants.service.spec.ts b/apps/tenant-manager/src/tenants/__tests__/tenants.service.spec.ts
new file mode 100644
index 0000000000000000000000000000000000000000..9510cabbfa3d8c7e9169294ca0f15d6de06a0f6a
--- /dev/null
+++ b/apps/tenant-manager/src/tenants/__tests__/tenants.service.spec.ts
@@ -0,0 +1,85 @@
+import { Test } from '@nestjs/testing';
+import { EventTenantsCreate, EventTenantsGetAllTenantIds } from '@ocm/shared';
+import { Subject, of, takeUntil } from 'rxjs';
+
+import { NATS_CLIENT } from '../../common/constants.js';
+import { TenantsService } from '../tenants.service.js';
+
+describe('TenantsService', () => {
+  let service: TenantsService;
+  const natsClientMock = { send: jest.fn() };
+
+  beforeEach(async () => {
+    jest.resetAllMocks();
+
+    const module = await Test.createTestingModule({
+      providers: [
+        { provide: NATS_CLIENT, useValue: natsClientMock },
+        TenantsService,
+      ],
+    }).compile();
+
+    service = module.get<TenantsService>(TenantsService);
+  });
+
+  it('should be defined', () => {
+    expect(service).toBeDefined();
+  });
+
+  describe('create', () => {
+    it('should call natsClient.send with the correct arguments', (done) => {
+      const unsubscribe$ = new Subject<void>();
+      const label = 'label';
+      const expectedResult = {} as EventTenantsCreate['data'];
+
+      natsClientMock.send.mockReturnValueOnce(
+        of(new EventTenantsCreate(expectedResult, undefined)),
+      );
+
+      service
+        .create(label)
+        .pipe(takeUntil(unsubscribe$))
+        .subscribe((result) => {
+          expect(natsClientMock.send).toHaveBeenCalledWith(
+            EventTenantsCreate.token,
+            { label },
+          );
+
+          expect(result).toStrictEqual(expectedResult);
+
+          unsubscribe$.next();
+          unsubscribe$.complete();
+
+          done();
+        });
+    });
+  });
+
+  describe('find', () => {
+    it('should call natsClient.send with the correct arguments', (done) => {
+      const unsubscribe$ = new Subject<void>();
+      const expectedResult = [] as string[];
+
+      natsClientMock.send.mockReturnValueOnce(
+        of(new EventTenantsGetAllTenantIds(expectedResult, undefined)),
+      );
+
+      service
+        .find()
+        .pipe(takeUntil(unsubscribe$))
+        .subscribe((result) => {
+          expect(natsClientMock.send).toHaveBeenCalledWith(
+            EventTenantsGetAllTenantIds.token,
+            {},
+          );
+
+          expect(result).toStrictEqual(expectedResult);
+
+          unsubscribe$.next();
+          unsubscribe$.complete();
+
+          done();
+        });
+    });
+  });
+});
diff --git a/apps/tenant-manager/src/tenants/dto/create-tenant.dto.ts b/apps/tenant-manager/src/tenants/dto/create-tenant.dto.ts
new file mode 100644
index 0000000000000000000000000000000000000000..a8f3ee9873f9f0922bcba253310abd3a054b9406
--- /dev/null
+++ b/apps/tenant-manager/src/tenants/dto/create-tenant.dto.ts
@@ -0,0 +1,13 @@
+import { ApiProperty } from '@nestjs/swagger';
+import { IsNotEmpty, IsString } from 'class-validator';
+
+export class CreateTenantPayload {
+  @IsString()
+  @IsNotEmpty()
+  @ApiProperty({
+    type: String,
+    description: 'The label of the tenant',
+    example: 'Alice',
+  })
+  public label: string;
+}
diff --git a/apps/tenant-manager/src/tenants/tenants.controller.ts b/apps/tenant-manager/src/tenants/tenants.controller.ts
new file mode 100644
index 0000000000000000000000000000000000000000..dff8cb89860ba722189d754307b007484be6482d
--- /dev/null
+++ b/apps/tenant-manager/src/tenants/tenants.controller.ts
@@ -0,0 +1,32 @@
+import {
+  Body,
+  Controller,
+  Get,
+  Post,
+  UseInterceptors,
+  UsePipes,
+  ValidationPipe,
+} from '@nestjs/common';
+import { ApiTags } from '@nestjs/swagger';
+import { ResponseFormatInterceptor } from '@ocm/shared';
+
+import { CreateTenantPayload } from './dto/create-tenant.dto.js';
+import { TenantsService } from './tenants.service.js';
+
+@Controller()
+@ApiTags('Tenants')
+@UsePipes(new ValidationPipe({ transform: true, whitelist: true }))
+@UseInterceptors(new ResponseFormatInterceptor())
+export class TenantsController {
+  public constructor(private readonly service: TenantsService) {}
+
+  @Get()
+  public find() {
+    return this.service.find();
+  }
+
+  @Post()
+  public create(@Body() payload: CreateTenantPayload) {
+    return this.service.create(payload.label);
+  }
+}
diff --git a/apps/tenant-manager/src/tenants/tenants.module.ts b/apps/tenant-manager/src/tenants/tenants.module.ts
new file mode 100644
index 0000000000000000000000000000000000000000..6ee6f21baf498a5615bcadd9e87e75b636903041
--- /dev/null
+++ b/apps/tenant-manager/src/tenants/tenants.module.ts
@@ -0,0 +1,10 @@
+import { Module } from '@nestjs/common';
+
+import { TenantsController } from './tenants.controller.js';
+import { TenantsService } from './tenants.service.js';
+
+@Module({
+  providers: [TenantsService],
+  controllers: [TenantsController],
+})
+export class TenantsModule {}
diff --git a/apps/tenant-manager/src/tenants/tenants.service.ts b/apps/tenant-manager/src/tenants/tenants.service.ts
new file mode 100644
index 0000000000000000000000000000000000000000..22b308d2d87fef00f2ebae00c246085b5e7b6da9
--- /dev/null
+++ b/apps/tenant-manager/src/tenants/tenants.service.ts
@@ -0,0 +1,28 @@
+import { Inject, Injectable } from '@nestjs/common';
+import { ClientProxy } from '@nestjs/microservices';
+import {
+  EventTenantsCreate,
+  EventTenantsGetAllTenantIds,
+  handleSSIResponse,
+} from '@ocm/shared';
+
+import { NATS_CLIENT } from '../common/constants.js';
+
+@Injectable()
+export class TenantsService {
+  public constructor(
+    @Inject(NATS_CLIENT) private readonly natsClient: ClientProxy,
+  ) {}
+
+  public find() {
+    return this.natsClient
+      .send(EventTenantsGetAllTenantIds.token, {})
+      .pipe(handleSSIResponse);
+  }
+
+  public create(label: string) {
+    return this.natsClient
+      .send(EventTenantsCreate.token, { label })
+      .pipe(handleSSIResponse);
+  }
+}
diff --git a/apps/tenant-manager/tsconfig.build.json b/apps/tenant-manager/tsconfig.build.json
new file mode 100644
index 0000000000000000000000000000000000000000..3e5ab438230b6cbd30a5825fc562c485a89ff95d
--- /dev/null
+++ b/apps/tenant-manager/tsconfig.build.json
@@ -0,0 +1,9 @@
+{
+  "extends": "../../tsconfig.build.json",
+  "compilerOptions": {
+    "baseUrl": ".",
+    "outDir": "./dist",
+    "rootDir": "./src"
+  },
+  "exclude": ["node_modules", "**/test", "**/dist", "**/*spec.ts"]
+}
diff --git a/apps/tenant-manager/tsconfig.json b/apps/tenant-manager/tsconfig.json
new file mode 100644
index 0000000000000000000000000000000000000000..4082f16a5d91ce6f21a9092b14170eeecc8f1d75
--- /dev/null
+++ b/apps/tenant-manager/tsconfig.json
@@ -0,0 +1,3 @@
+{
+  "extends": "../../tsconfig.json"
+}
diff --git a/apps/tenant-manager/tsconfig.production.json b/apps/tenant-manager/tsconfig.production.json
new file mode 100644
index 0000000000000000000000000000000000000000..45f85dfe5daf11a59e2fac464fa15940a2f50200
--- /dev/null
+++ b/apps/tenant-manager/tsconfig.production.json
@@ -0,0 +1,9 @@
+{
+  "extends": "../../tsconfig.production.json",
+  "compilerOptions": {
+    "baseUrl": ".",
+    "outDir": "./dist",
+    "rootDir": "./src"
+  },
+  "exclude": ["node_modules", "**/test", "**/dist", "**/*spec.ts"]
+}
diff --git a/docker-compose.yml b/docker-compose.yml
index 4d3671b59f0be6412837e81794940878db5152f9..8f74339a9e05898c5ddb48dd4f6451befee7fb1e 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -197,3 +197,22 @@ services:
       - nats
     networks:
       - network1
+
+  tenant-manager:
+    build:
+      args:
+        - SERVICE=tenant-manager
+    init: true
+    environment:
+      HTTP_HOST: 0.0.0.0
+      HTTP_PORT: 3000
+      NATS_URL: nats://nats:4222
+      NATS_USER: nats_user
+      NATS_PASSWORD: nats_password
+      NATS_MONITORING_URL: http://nats:8222
+    ports:
+      - '4007:3000'
+    depends_on:
+      - nats
+    networks:
+      - network1