diff --git a/manifests/nginx_ingress_http.yaml b/manifests/nginx_ingress_http.yaml
index 955d5726a9f8f79560327a8f595c1865f6d37d22..c0a0875e2b0afdc0f18e1578370dbbcce5b88cde 100644
--- a/manifests/nginx_ingress_http.yaml
+++ b/manifests/nginx_ingress_http.yaml
@@ -62,3 +62,10 @@ spec:
                 name: nbiservice
                 port:
                   number: 8080
+                    - path: /()(qkd_app/.*)
+          - pathType: Prefix
+            backend:
+              service:
+                name: qkdappservice
+                port:
+                  number: 8005
diff --git a/manifests/qkdappservice.yaml b/manifests/qkdappservice.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..13f16f4e501b20208b93e3edcff9c161744632d2
--- /dev/null
+++ b/manifests/qkdappservice.yaml
@@ -0,0 +1,85 @@
+# Copyright 2022-2024 ETSI OSG/SDG TeraFlowSDN (TFS) (https://tfs.etsi.org/)
+#
+# 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.
+
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: qkdappservice
+spec:
+  selector:
+    matchLabels:
+      app: qkdappservice
+  #replicas: 1
+  template:
+    metadata:
+      labels:
+        app: qkdappservice
+    spec:
+      terminationGracePeriodSeconds: 5
+      containers:
+      - name: server
+        image: labs.etsi.org:5050/tfs/controller/qkd_app:latest
+        imagePullPolicy: Always
+        ports:
+        - containerPort: 10060
+        - containerPort: 9192
+        - containerPort: 8005
+        env:
+        - name: MB_BACKEND
+          value: "nats"
+        - name: LOG_LEVEL
+          value: "INFO"
+        - name: CRDB_DATABASE_APP
+          value: "qkd_app"
+        envFrom:
+        - secretRef:
+            name: crdb-data
+        - secretRef:
+            name: nats-data
+        readinessProbe:
+          exec:
+            command: ["/bin/grpc_health_probe", "-addr=:10060"]
+        livenessProbe:
+          exec:
+            command: ["/bin/grpc_health_probe", "-addr=:10060"]
+        resources:
+          requests:
+            cpu: 150m
+            memory: 128Mi
+          limits:
+            cpu: 500m
+            memory: 512Mi
+---
+apiVersion: v1
+kind: Service
+metadata:
+  name: qkdappservice
+  labels:
+    app: qkdappservice
+spec:
+  type: ClusterIP
+  selector:
+    app: qkdappservice
+  ports:
+  - name: grpc
+    protocol: TCP
+    port: 10060
+    targetPort: 10060
+  - name: metrics
+    protocol: TCP
+    port: 9192
+    targetPort: 9192
+  - name: http
+    port: 8005
+    targetPort: 8005
diff --git a/my_deploy.sh b/my_deploy.sh
index 88be82b63e9e79a97ee79702de886f69a6152f94..ee17cb3679f6390115917a005cfaf670585c28c7 100755
--- a/my_deploy.sh
+++ b/my_deploy.sh
@@ -71,7 +71,14 @@ export TFS_COMPONENTS="context device pathcomp service slice nbi webui load_gene
 #fi
 
 # Uncomment to activate QKD App
-#export TFS_COMPONENTS="${TFS_COMPONENTS} app"
+#   To manage QKD Apps, "service" requires "qkd_app" to be deployed
+#   before "service", thus we "hack" the TFS_COMPONENTS environment variable prepending the
+#   "qkd_app" only if "service" is already in TFS_COMPONENTS, and re-export it.
+#if [[ "$TFS_COMPONENTS" == *"service"* ]]; then
+#    BEFORE="${TFS_COMPONENTS% service*}"
+#    AFTER="${TFS_COMPONENTS#* service}"
+#    export TFS_COMPONENTS="${BEFORE} qkd_app service ${AFTER}"
+#fi
 
 
 # Set the tag you want to use for your images.
diff --git a/proto/qkd_app.proto b/proto/qkd_app.proto
new file mode 100644
index 0000000000000000000000000000000000000000..7b6c47330833849b889e770aac43844ec6e6072c
--- /dev/null
+++ b/proto/qkd_app.proto
@@ -0,0 +1,53 @@
+syntax = "proto3";
+package qkd_app;
+
+import "context.proto";
+
+// Optare: Change this if you want to change App's structure or enums. 
+// Optare: If a message (structure) is changed it must be changed in src/app/service/database
+
+enum QKDAppStatusEnum {
+  QKDAPPSTATUS_ON = 0;
+  QKDAPPSTATUS_DISCONNECTED = 1;
+  QKDAPPSTATUS_OUT_OF_TIME = 2;
+  QKDAPPSTATUS_ZOMBIE = 3;
+}
+
+enum QKDAppTypesEnum {
+  QKDAPPTYPES_INTERNAL = 0;
+  QKDAPPTYPES_CLIENT = 1;
+}
+
+message QKDLId {
+  context.Uuid qkdl_uuid = 1;
+}
+
+
+message App {
+  AppId app_id = 1;
+  QKDAppStatusEnum app_status = 2;
+  QKDAppTypesEnum app_type = 3;
+  string server_app_id = 4;
+  repeated string client_app_id = 5;
+  repeated QKDLId backing_qkdl_id = 6;
+  context.DeviceId local_device_id = 7;
+  context.DeviceId remote_device_id = 8;
+}
+
+
+message AppId {
+  context.ContextId context_id = 1;
+  context.Uuid app_uuid = 2;
+}
+
+
+service AppService {
+  rpc RegisterApp(App) returns (context.Empty) {}
+  rpc ListApps       (context.ContextId     ) returns (       AppList     ) {}
+ }
+ 
+
+
+ message AppList {
+  repeated App apps = 1;
+}
diff --git a/src/common/Constants.py b/src/common/Constants.py
index 74490321f9c8ec016fa4b48b583e2217c61710ec..8b2e215a0ee669726430d12ea4ebac334f69c1ce 100644
--- a/src/common/Constants.py
+++ b/src/common/Constants.py
@@ -61,6 +61,7 @@ class ServiceNameEnum(Enum):
     E2EORCHESTRATOR        = 'e2e-orchestrator'
     OPTICALCONTROLLER      = 'opticalcontroller'
     BGPLS                  = 'bgpls-speaker'
+    QKD_APP                = 'qkd_app'
     KPIMANAGER             = 'kpi-manager'
     KPIVALUEAPI            = 'kpi-value-api'
     KPIVALUEWRITER         = 'kpi-value-writer'
@@ -96,6 +97,7 @@ DEFAULT_SERVICE_GRPC_PORTS = {
     ServiceNameEnum.FORECASTER             .value : 10040,
     ServiceNameEnum.E2EORCHESTRATOR        .value : 10050,
     ServiceNameEnum.OPTICALCONTROLLER      .value : 10060,
+    ServiceNameEnum.QKD_APP                .value : 10070,
     ServiceNameEnum.BGPLS                  .value : 20030,
     ServiceNameEnum.KPIMANAGER             .value : 30010,
     ServiceNameEnum.KPIVALUEAPI            .value : 30020,
@@ -115,10 +117,12 @@ DEFAULT_SERVICE_HTTP_PORTS = {
     ServiceNameEnum.CONTEXT   .value : 8080,
     ServiceNameEnum.NBI       .value : 8080,
     ServiceNameEnum.WEBUI     .value : 8004,
+    ServiceNameEnum.QKD_APP   .value : 8005,
 }
 
 # Default HTTP/REST-API service base URLs
 DEFAULT_SERVICE_HTTP_BASEURLS = {
     ServiceNameEnum.NBI       .value : None,
     ServiceNameEnum.WEBUI     .value : None,
+    ServiceNameEnum.QKD_APP   .value : None,
 }
diff --git a/src/common/tools/object_factory/QKDApp.py b/src/common/tools/object_factory/QKDApp.py
new file mode 100644
index 0000000000000000000000000000000000000000..936a0f228302469ec51321623f8d8050f3daced0
--- /dev/null
+++ b/src/common/tools/object_factory/QKDApp.py
@@ -0,0 +1,24 @@
+# Copyright 2022-2024 ETSI OSG/SDG TeraFlowSDN (TFS) (https://tfs.etsi.org/)
+#
+# 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.
+
+import copy
+from typing import Dict, List, Optional
+from common.Constants import DEFAULT_CONTEXT_NAME
+from common.tools.object_factory.Context import json_context_id
+
+
+def json_app_id(app_uuid : str, context_id : Optional[Dict] = None) -> Dict:
+    result = {'app_uuid': {'uuid': app_uuid}}
+    if context_id is not None: result['context_id'] = copy.deepcopy(context_id)
+    return result
diff --git a/src/common/tools/object_factory/Service.py b/src/common/tools/object_factory/Service.py
index 32b99a31f22072874ab894de2a87ce2b7d56ba85..b05821c7814ce250abca1819b111376af7c0430f 100644
--- a/src/common/tools/object_factory/Service.py
+++ b/src/common/tools/object_factory/Service.py
@@ -42,6 +42,16 @@ def json_service(
         'service_config'      : {'config_rules': copy.deepcopy(config_rules)},
     }
 
+def json_service_qkd_planned(
+        service_uuid : str, endpoint_ids : List[Dict] = [], constraints : List[Dict] = [],
+        config_rules : List[Dict] = [], context_uuid : str = DEFAULT_CONTEXT_NAME
+    ):
+
+    return json_service(
+        service_uuid, ServiceTypeEnum.SERVICETYPE_QKD, context_id=json_context_id(context_uuid),
+        status=ServiceStatusEnum.SERVICESTATUS_PLANNED, endpoint_ids=endpoint_ids, constraints=constraints,
+        config_rules=config_rules)
+
 def json_service_l2nm_planned(
         service_uuid : str, endpoint_ids : List[Dict] = [], constraints : List[Dict] = [],
         config_rules : List[Dict] = [], context_uuid : str = DEFAULT_CONTEXT_NAME
diff --git a/src/device/tests/qkd/unit/test_qkd_mock_connectivity.py b/src/device/tests/qkd/unit/test_qkd_mock_connectivity.py
index 150d00fd079b0a036f383653c833562279bb4d72..be9427d9b619423a61a3e6f5270d8aab76dc8955 100644
--- a/src/device/tests/qkd/unit/test_qkd_mock_connectivity.py
+++ b/src/device/tests/qkd/unit/test_qkd_mock_connectivity.py
@@ -38,3 +38,4 @@ def test_qkd_driver_timeout_connection(mock_get, qkd_driver):
     mock_get.side_effect = requests.exceptions.Timeout
     qkd_driver.timeout = 0.001  # Simulate very short timeout
     assert qkd_driver.Connect() is False
+
diff --git a/src/qkd_app/.gitlab-ci.yml b/src/qkd_app/.gitlab-ci.yml
new file mode 100644
index 0000000000000000000000000000000000000000..85ee2b5e04bce3077086d0cc831043483a995aef
--- /dev/null
+++ b/src/qkd_app/.gitlab-ci.yml
@@ -0,0 +1,80 @@
+# Copyright 2022-2024 ETSI OSG/SDG TeraFlowSDN (TFS) (https://tfs.etsi.org/)
+#
+# 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.
+
+build app:
+  variables:
+    IMAGE_NAME: 'qkd_app' # name of the microservice
+    IMAGE_TAG: 'latest' # tag of the container image (production, development, etc)
+  stage: build
+  before_script:
+    - docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" $CI_REGISTRY
+  script:
+    - docker build -t "$IMAGE_NAME:$IMAGE_TAG" -f ./src/$IMAGE_NAME/Dockerfile .
+    - docker tag "$IMAGE_NAME:$IMAGE_TAG" "$CI_REGISTRY_IMAGE/$IMAGE_NAME:$IMAGE_TAG"
+    - docker push "$CI_REGISTRY_IMAGE/$IMAGE_NAME:$IMAGE_TAG"
+  after_script:
+    - docker images --filter="dangling=true" --quiet | xargs -r docker rmi
+  rules:
+    - if: '$CI_PIPELINE_SOURCE == "merge_request_event" && ($CI_MERGE_REQUEST_TARGET_BRANCH_NAME == "develop" || $CI_MERGE_REQUEST_TARGET_BRANCH_NAME == $CI_DEFAULT_BRANCH)'
+    - if: '$CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == "develop"'
+    - changes:
+      - src/common/**/*.py
+      - proto/*.proto
+      - src/$IMAGE_NAME/**/*.{py,in,yml}
+      - src/$IMAGE_NAME/Dockerfile
+      - src/$IMAGE_NAME/tests/*.py
+      - manifests/${IMAGE_NAME}service.yaml
+      - .gitlab-ci.yml
+
+# Apply unit test to the component
+unit_test app:
+  variables:
+    IMAGE_NAME: 'qkd_app' # name of the microservice
+    IMAGE_TAG: 'latest' # tag of the container image (production, development, etc)
+  stage: unit_test
+  needs:
+    - build app
+    - unit_test service
+  before_script:
+    - docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" $CI_REGISTRY
+    - if docker network list | grep teraflowbridge; then echo "teraflowbridge is already created"; else docker network create -d bridge teraflowbridge; fi
+    - if docker container ls | grep $IMAGE_NAME; then docker rm -f $IMAGE_NAME; else echo "$IMAGE_NAME image is not in the system"; fi
+  script:
+    - docker pull "$CI_REGISTRY_IMAGE/$IMAGE_NAME:$IMAGE_TAG"
+    - docker run --name $IMAGE_NAME -d -p 10070:10070 -p 8005:8005 -v "$PWD/src/$IMAGE_NAME/tests:/opt/results" --network=teraflowbridge $CI_REGISTRY_IMAGE/$IMAGE_NAME:$IMAGE_TAG
+    - sleep 5
+    - docker ps -a
+    - docker logs $IMAGE_NAME
+    - docker exec -i $IMAGE_NAME bash -c "coverage run -m pytest --log-level=INFO --verbose $IMAGE_NAME/tests/test_unitary.py --junitxml=/opt/results/${IMAGE_NAME}_report.xml"
+    - docker exec -i $IMAGE_NAME bash -c "coverage report --include='${IMAGE_NAME}/*' --show-missing"
+  coverage: '/TOTAL\s+\d+\s+\d+\s+(\d+%)/'
+  after_script:
+    - docker rm -f $IMAGE_NAME
+    - docker network rm teraflowbridge
+  rules:
+    - if: '$CI_PIPELINE_SOURCE == "merge_request_event" && ($CI_MERGE_REQUEST_TARGET_BRANCH_NAME == "develop" || $CI_MERGE_REQUEST_TARGET_BRANCH_NAME == $CI_DEFAULT_BRANCH)'
+    - if: '$CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == "develop"'
+    - changes:
+      - src/common/**/*.py
+      - proto/*.proto
+      - src/$IMAGE_NAME/**/*.{py,in,yml}
+      - src/$IMAGE_NAME/Dockerfile
+      - src/$IMAGE_NAME/tests/*.py
+      - src/$IMAGE_NAME/tests/Dockerfile
+      - manifests/${IMAGE_NAME}service.yaml
+      - .gitlab-ci.yml
+  artifacts:
+      when: always
+      reports:
+        junit: src/$IMAGE_NAME/tests/${IMAGE_NAME}_report.xml
diff --git a/src/qkd_app/Config.py b/src/qkd_app/Config.py
new file mode 100644
index 0000000000000000000000000000000000000000..07d08814021ef82220611ee21c01ba01806682e9
--- /dev/null
+++ b/src/qkd_app/Config.py
@@ -0,0 +1,13 @@
+# Copyright 2022-2024 ETSI OSG/SDG TeraFlowSDN (TFS) (https://tfs.etsi.org/)
+#
+# 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.
diff --git a/src/qkd_app/Dockerfile b/src/qkd_app/Dockerfile
new file mode 100644
index 0000000000000000000000000000000000000000..4d2b8d61bd61ca98d62316b021d2486de3777977
--- /dev/null
+++ b/src/qkd_app/Dockerfile
@@ -0,0 +1,70 @@
+# Copyright 2022-2024 ETSI OSG/SDG TeraFlowSDN (TFS) (https://tfs.etsi.org/)
+#
+# 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 python:3.9-slim
+
+# Install dependencies
+RUN apt-get --yes --quiet --quiet update && \
+    apt-get --yes --quiet --quiet install wget g++ git && \
+    rm -rf /var/lib/apt/lists/*
+
+# Set Python to show logs as they occur
+ENV PYTHONUNBUFFERED=0
+
+# Download the gRPC health probe
+RUN GRPC_HEALTH_PROBE_VERSION=v0.2.0 && \
+    wget -qO/bin/grpc_health_probe https://github.com/grpc-ecosystem/grpc-health-probe/releases/download/${GRPC_HEALTH_PROBE_VERSION}/grpc_health_probe-linux-amd64 && \
+    chmod +x /bin/grpc_health_probe
+
+# Get generic Python packages
+RUN python3 -m pip install --upgrade pip
+RUN python3 -m pip install --upgrade setuptools wheel
+RUN python3 -m pip install --upgrade pip-tools
+
+# Get common Python packages
+# Note: this step enables sharing the previous Docker build steps among all the Python components
+WORKDIR /var/teraflow
+COPY common_requirements.in common_requirements.in
+RUN pip-compile --quiet --output-file=common_requirements.txt common_requirements.in
+RUN python3 -m pip install -r common_requirements.txt
+
+# Add common files into working directory
+WORKDIR /var/teraflow/common
+COPY src/common/. ./
+RUN rm -rf proto
+
+# Create proto sub-folder, copy .proto files, and generate Python code
+RUN mkdir -p /var/teraflow/common/proto
+WORKDIR /var/teraflow/common/proto
+RUN touch __init__.py
+COPY proto/*.proto ./
+RUN python3 -m grpc_tools.protoc -I=. --python_out=. --grpc_python_out=. *.proto
+RUN rm *.proto
+RUN find . -type f -exec sed -i -E 's/(import\ .*)_pb2/from . \1_pb2/g' {} \;
+
+# Create component sub-folders, get specific Python packages
+RUN mkdir -p /var/teraflow/qkd_app
+WORKDIR /var/teraflow/qkd_app
+COPY src/qkd_app/requirements.in requirements.in
+RUN pip-compile --quiet --output-file=requirements.txt requirements.in
+RUN python3 -m pip install -r requirements.txt
+
+# Add component files into working directory
+WORKDIR /var/teraflow
+COPY src/context/. context/
+COPY src/service/. service/
+COPY src/qkd_app/. qkd_app/
+
+# Start the service
+ENTRYPOINT ["python", "-m", "qkd_app.service"]
diff --git a/src/qkd_app/__init__.py b/src/qkd_app/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..07d08814021ef82220611ee21c01ba01806682e9
--- /dev/null
+++ b/src/qkd_app/__init__.py
@@ -0,0 +1,13 @@
+# Copyright 2022-2024 ETSI OSG/SDG TeraFlowSDN (TFS) (https://tfs.etsi.org/)
+#
+# 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.
diff --git a/src/qkd_app/client/QKDAppClient.py b/src/qkd_app/client/QKDAppClient.py
new file mode 100644
index 0000000000000000000000000000000000000000..b8c09329f9511868a1013ea41a0d8890da499218
--- /dev/null
+++ b/src/qkd_app/client/QKDAppClient.py
@@ -0,0 +1,64 @@
+# Copyright 2022-2024 ETSI OSG/SDG TeraFlowSDN (TFS) (https://tfs.etsi.org/)
+#
+# 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.
+
+import grpc, logging
+from common.Constants import ServiceNameEnum
+from common.Settings import get_service_host, get_service_port_grpc
+from common.proto.context_pb2 import Empty, ContextId
+from common.proto.qkd_app_pb2 import App, AppId, AppList
+from common.proto.qkd_app_pb2_grpc import AppServiceStub
+from common.tools.client.RetryDecorator import retry, delay_exponential
+from common.tools.grpc.Tools import grpc_message_to_json_string
+
+LOGGER = logging.getLogger(__name__)
+MAX_RETRIES = 15
+DELAY_FUNCTION = delay_exponential(initial=0.01, increment=2.0, maximum=5.0)
+RETRY_DECORATOR = retry(max_retries=MAX_RETRIES, delay_function=DELAY_FUNCTION, prepare_method_name='connect')
+
+class QKDAppClient:
+    def __init__(self, host=None, port=None):
+        if not host: host = get_service_host(ServiceNameEnum.APP)
+        if not port: port = get_service_port_grpc(ServiceNameEnum.APP)
+        self.endpoint = '{:s}:{:s}'.format(str(host), str(port))
+        LOGGER.debug('Creating channel to {:s}...'.format(self.endpoint))
+        self.channel = None
+        self.stub = None
+        self.connect()
+        LOGGER.debug('Channel created')
+
+    def connect(self):
+        self.channel = grpc.insecure_channel(self.endpoint)
+        self.stub = AppServiceStub(self.channel)
+
+    def close(self):
+        if self.channel is not None: self.channel.close()
+        self.channel = None
+        self.stub = None
+
+
+
+    @RETRY_DECORATOR
+    def RegisterApp(self, request : App) -> Empty:
+        LOGGER.debug('RegisterApp request: {:s}'.format(grpc_message_to_json_string(request)))
+        response = self.stub.RegisterApp(request)
+        LOGGER.debug('RegisterApp result: {:s}'.format(grpc_message_to_json_string(response)))
+        return response
+
+    
+    @RETRY_DECORATOR
+    def ListApps(self, request: ContextId) -> AppList:
+        LOGGER.debug('ListApps request: {:s}'.format(grpc_message_to_json_string(request)))
+        response = self.stub.ListApps(request)
+        LOGGER.debug('ListApps result: {:s}'.format(grpc_message_to_json_string(response)))
+        return response
diff --git a/src/qkd_app/client/__init__.py b/src/qkd_app/client/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..07d08814021ef82220611ee21c01ba01806682e9
--- /dev/null
+++ b/src/qkd_app/client/__init__.py
@@ -0,0 +1,13 @@
+# Copyright 2022-2024 ETSI OSG/SDG TeraFlowSDN (TFS) (https://tfs.etsi.org/)
+#
+# 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.
diff --git a/src/qkd_app/requirements.in b/src/qkd_app/requirements.in
new file mode 100644
index 0000000000000000000000000000000000000000..a9bce93b569792a75687811f08e03d398ae4aeb5
--- /dev/null
+++ b/src/qkd_app/requirements.in
@@ -0,0 +1,25 @@
+# Copyright 2022-2024 ETSI OSG/SDG TeraFlowSDN (TFS) (https://tfs.etsi.org/)
+#
+# 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.
+
+Flask==2.1.3
+Flask-HTTPAuth==4.5.0
+Flask-RESTful==0.3.9
+jsonschema==4.4.0
+requests==2.27.1
+werkzeug==2.3.7
+nats-py==2.6.*
+psycopg2-binary==2.9.*
+SQLAlchemy==1.4.*
+sqlalchemy-cockroachdb==1.4.*
+SQLAlchemy-Utils==0.38.*
diff --git a/src/qkd_app/service/QKDAppService.py b/src/qkd_app/service/QKDAppService.py
new file mode 100644
index 0000000000000000000000000000000000000000..fcf63efba3b390e0cb58e82430b2a2ce9feeef8d
--- /dev/null
+++ b/src/qkd_app/service/QKDAppService.py
@@ -0,0 +1,37 @@
+# Copyright 2022-2024 ETSI OSG/SDG TeraFlowSDN (TFS) (https://tfs.etsi.org/)
+#
+# 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.
+
+import logging, sqlalchemy
+from common.Constants import ServiceNameEnum
+from common.Settings import get_service_port_grpc
+from common.message_broker.MessageBroker import MessageBroker
+from common.proto.qkd_app_pb2_grpc import add_AppServiceServicer_to_server
+from common.tools.service.GenericGrpcService import GenericGrpcService
+from qkd_app.service.QKDAppServiceServicerImpl import AppServiceServicerImpl
+
+# Custom gRPC settings
+GRPC_MAX_WORKERS = 200 # multiple clients might keep connections alive for Get*Events() RPC methods
+LOGGER = logging.getLogger(__name__)
+
+
+class AppService(GenericGrpcService):
+    def __init__(
+        self, db_engine : sqlalchemy.engine.Engine, messagebroker : MessageBroker, cls_name: str = __name__
+    ) -> None:
+        port = get_service_port_grpc(ServiceNameEnum.APP)
+        super().__init__(port, max_workers=GRPC_MAX_WORKERS, cls_name=cls_name)
+        self.app_servicer = AppServiceServicerImpl(db_engine, messagebroker)
+
+    def install_servicers(self):
+        add_AppServiceServicer_to_server(self.app_servicer, self.server)
diff --git a/src/qkd_app/service/QKDAppServiceServicerImpl.py b/src/qkd_app/service/QKDAppServiceServicerImpl.py
new file mode 100644
index 0000000000000000000000000000000000000000..df7a885c47eda9d7a6137c9905388da49c698e7e
--- /dev/null
+++ b/src/qkd_app/service/QKDAppServiceServicerImpl.py
@@ -0,0 +1,73 @@
+# Copyright 2022-2024 ETSI OSG/SDG TeraFlowSDN (TFS) (https://tfs.etsi.org/)
+#
+# 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.
+
+import grpc, logging, sqlalchemy
+#from typing import Iterator, Optional
+from common.message_broker.MessageBroker import MessageBroker
+import grpc, json, logging #, deepdiff
+from common.proto.context_pb2 import (
+    Empty, Service, ServiceId, ServiceStatusEnum, ServiceTypeEnum, ContextId)
+from common.proto.qkd_app_pb2 import (App, AppId, AppList, QKDAppTypesEnum)
+from common.proto.qkd_app_pb2_grpc import AppServiceServicer
+from common.method_wrappers.Decorator import MetricsPool, safe_and_metered_rpc_method
+#from common.tools.context_queries.InterDomain import is_inter_domain #, is_multi_domain
+#from common.tools.grpc.ConfigRules import copy_config_rules
+#from common.tools.grpc.Constraints import copy_constraints
+#from common.tools.grpc.EndPointIds import copy_endpoint_ids
+#from common.tools.grpc.ServiceIds import update_service_ids
+#from common.tools.grpc.Tools import grpc_message_to_json_string
+#from context.client.ContextClient import ContextClient
+#from qkd_app.client.QKDAppClient import QKDAppClient
+from .database.QKDApp import app_set, app_list_objs, app_get, app_get_by_server
+from common.method_wrappers.ServiceExceptions import NotFoundException
+
+LOGGER = logging.getLogger(__name__)
+
+METRICS_POOL = MetricsPool('QkdApp', 'RPC')
+
+# Optare: This file must be edited based on app's logic
+
+class AppServiceServicerImpl(AppServiceServicer):
+    def __init__(self, db_engine : sqlalchemy.engine.Engine, messagebroker : MessageBroker):
+        LOGGER.debug('Creating Servicer...')
+        self.db_engine = db_engine
+        self.messagebroker = messagebroker
+        LOGGER.debug('Servicer Created')
+
+    @safe_and_metered_rpc_method(METRICS_POOL, LOGGER)
+    def RegisterApp(self, request : App, context : grpc.ServicerContext) -> Empty:
+        # Optare: This is the main function required for the project.
+        # Optare: If it's an internal it will save it directly. If it's an external one it will save it as pending by not providing the remote until the other party requests it too
+        # Optare: Ideally, the only thing needed to change is the code inside the try block. Currently it just searches by a pending app with the same server_id but you can put more restrictions or different search and raise the NotFoundException
+
+        if request.app_type == QKDAppTypesEnum.QKDAPPTYPES_INTERNAL:
+            app_set(self.db_engine, self.messagebroker, request)
+
+        else:
+            try:
+                app = app_get_by_server(self.db_engine, request.server_app_id)
+            except NotFoundException:
+                app = request
+                app_set(self.db_engine, self.messagebroker, app)
+            else:
+                app.remote_device_id.device_uuid.uuid = request.local_device_id.device_uuid.uuid
+                app_set(self.db_engine, self.messagebroker, app)
+                
+        
+        return Empty()
+    
+
+    @safe_and_metered_rpc_method(METRICS_POOL, LOGGER)
+    def ListApps(self, request: ContextId, context : grpc.ServicerContext) -> AppList:
+        return app_list_objs(self.db_engine)
diff --git a/src/qkd_app/service/__init__.py b/src/qkd_app/service/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..07d08814021ef82220611ee21c01ba01806682e9
--- /dev/null
+++ b/src/qkd_app/service/__init__.py
@@ -0,0 +1,13 @@
+# Copyright 2022-2024 ETSI OSG/SDG TeraFlowSDN (TFS) (https://tfs.etsi.org/)
+#
+# 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.
diff --git a/src/qkd_app/service/__main__.py b/src/qkd_app/service/__main__.py
new file mode 100644
index 0000000000000000000000000000000000000000..ed7e554728eb2de6240dd4facb7f084337a026a4
--- /dev/null
+++ b/src/qkd_app/service/__main__.py
@@ -0,0 +1,94 @@
+# Copyright 2022-2024 ETSI OSG/SDG TeraFlowSDN (TFS) (https://tfs.etsi.org/)
+#
+# 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.
+
+import logging, signal, sys, threading
+from prometheus_client import start_http_server
+#from common.Constants import ServiceNameEnum
+from common.Settings import (
+    #ENVVAR_SUFIX_SERVICE_HOST, ENVVAR_SUFIX_SERVICE_PORT_GRPC, get_env_var_name,
+    get_log_level, get_metrics_port, wait_for_environment_variables)
+from qkd_app.service.QKDAppService import AppService
+from qkd_app.service.rest_server.RestServer import RestServer
+from qkd_app.service.rest_server.qkd_app import register_qkd_app
+#from common.message_broker.Factory import get_messagebroker_backend
+#from common.message_broker.MessageBroker import MessageBroker
+from qkd_app.service.database.Engine import Engine
+from qkd_app.service.database.models._Base import rebuild_database
+
+terminate = threading.Event()
+LOGGER : logging.Logger = None
+
+def signal_handler(signal, frame): # pylint: disable=redefined-outer-name
+    LOGGER.warning('Terminate signal received')
+    terminate.set()
+
+def main():
+    global LOGGER # pylint: disable=global-statement
+
+    log_level = get_log_level()
+    logging.basicConfig(level=log_level, format="[%(asctime)s] %(levelname)s:%(name)s:%(message)s")
+    LOGGER = logging.getLogger(__name__)
+
+    wait_for_environment_variables([
+        #get_env_var_name(ServiceNameEnum.CONTEXT, ENVVAR_SUFIX_SERVICE_HOST     ),
+        #get_env_var_name(ServiceNameEnum.CONTEXT, ENVVAR_SUFIX_SERVICE_PORT_GRPC),
+    ])
+
+    signal.signal(signal.SIGINT,  signal_handler)
+    signal.signal(signal.SIGTERM, signal_handler)
+
+    LOGGER.info('Starting...')
+
+    # Start metrics server
+    metrics_port = get_metrics_port()
+    start_http_server(metrics_port)
+
+    # Get Database Engine instance and initialize database, if needed
+    LOGGER.info('Getting SQLAlchemy DB Engine...')
+    db_engine = Engine.get_engine()
+    if db_engine is None:
+        LOGGER.error('Unable to get SQLAlchemy DB Engine...')
+        return -1
+
+    try:
+        Engine.create_database(db_engine)
+    except: # pylint: disable=bare-except # pragma: no cover
+        LOGGER.exception('Failed to check/create the database: {:s}'.format(str(db_engine.url)))
+
+    rebuild_database(db_engine)
+
+    # Get message broker instance
+    messagebroker = None #MessageBroker(get_messagebroker_backend())
+
+    # Starting context service
+    grpc_service = AppService(db_engine, messagebroker)
+    grpc_service.start()
+
+    rest_server = RestServer()
+    register_qkd_app(rest_server)
+    rest_server.start()
+
+    # Wait for Ctrl+C or termination signal
+    while not terminate.wait(timeout=1.0): pass
+
+    LOGGER.info('Terminating...')
+    grpc_service.stop()
+    rest_server.shutdown()
+    rest_server.join()
+
+    LOGGER.info('Bye')
+    return 0
+
+if __name__ == '__main__':
+    sys.exit(main())
diff --git a/src/qkd_app/service/database/Engine.py b/src/qkd_app/service/database/Engine.py
new file mode 100644
index 0000000000000000000000000000000000000000..8f528f9a1b3cacca2ea260901ab808461dd3183d
--- /dev/null
+++ b/src/qkd_app/service/database/Engine.py
@@ -0,0 +1,55 @@
+# Copyright 2022-2024 ETSI OSG/SDG TeraFlowSDN (TFS) (https://tfs.etsi.org/)
+#
+# 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.
+
+import logging, sqlalchemy, sqlalchemy_utils
+from common.Settings import get_setting
+
+LOGGER = logging.getLogger(__name__)
+
+APP_NAME = 'tfs'
+ECHO = False # true: dump SQL commands and transactions executed
+CRDB_URI_TEMPLATE = 'cockroachdb://{:s}:{:s}@cockroachdb-public.{:s}.svc.cluster.local:{:s}/{:s}?sslmode={:s}'
+
+class Engine:
+    @staticmethod
+    def get_engine() -> sqlalchemy.engine.Engine:
+        crdb_uri = get_setting('CRDB_URI', default=None)
+        if crdb_uri is None:
+            CRDB_NAMESPACE = get_setting('CRDB_NAMESPACE')
+            CRDB_SQL_PORT  = get_setting('CRDB_SQL_PORT')
+            CRDB_DATABASE  = get_setting('CRDB_DATABASE_APP')
+            CRDB_USERNAME  = get_setting('CRDB_USERNAME')
+            CRDB_PASSWORD  = get_setting('CRDB_PASSWORD')
+            CRDB_SSLMODE   = get_setting('CRDB_SSLMODE')
+            crdb_uri = CRDB_URI_TEMPLATE.format(
+                CRDB_USERNAME, CRDB_PASSWORD, CRDB_NAMESPACE, CRDB_SQL_PORT, CRDB_DATABASE, CRDB_SSLMODE)
+
+        try:
+            engine = sqlalchemy.create_engine(
+                crdb_uri, connect_args={'application_name': APP_NAME}, echo=ECHO, future=True)
+        except: # pylint: disable=bare-except # pragma: no cover
+            LOGGER.exception('Failed to connect to database: {:s}'.format(str(crdb_uri)))
+            return None
+
+        return engine
+
+    @staticmethod
+    def create_database(engine : sqlalchemy.engine.Engine) -> None:
+        if not sqlalchemy_utils.database_exists(engine.url):
+            sqlalchemy_utils.create_database(engine.url)
+
+    @staticmethod
+    def drop_database(engine : sqlalchemy.engine.Engine) -> None:
+        if sqlalchemy_utils.database_exists(engine.url):
+            sqlalchemy_utils.drop_database(engine.url)
diff --git a/src/qkd_app/service/database/QKDApp.py b/src/qkd_app/service/database/QKDApp.py
new file mode 100644
index 0000000000000000000000000000000000000000..b1fb90d4efcd0770bcc4c48c1f00deb0e95687ad
--- /dev/null
+++ b/src/qkd_app/service/database/QKDApp.py
@@ -0,0 +1,185 @@
+# Copyright 2022-2024 ETSI OSG/SDG TeraFlowSDN (TFS) (https://tfs.etsi.org/)
+#
+# 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.
+
+import datetime, logging, uuid
+from sqlalchemy.dialects.postgresql import insert
+from sqlalchemy.engine import Engine
+from sqlalchemy.orm import Session, selectinload, sessionmaker
+from sqlalchemy_cockroachdb import run_transaction
+from typing import Dict, List, Optional, Set, Tuple
+from common.method_wrappers.ServiceExceptions import InvalidArgumentException, NotFoundException
+from common.message_broker.MessageBroker import MessageBroker
+from common.proto.context_pb2 import Empty
+from common.proto.qkd_app_pb2 import (
+    AppList, App, AppId)
+from common.tools.grpc.Tools import grpc_message_to_json_string
+from .models.QKDAppModel import AppModel
+from .models.enums.QKDAppStatus import grpc_to_enum__qkd_app_status
+from .models.enums.QKDAppTypes import grpc_to_enum__qkd_app_types
+from .uuids.QKDApp import app_get_uuid
+from common.tools.object_factory.Context import json_context_id
+from common.tools.object_factory.QKDApp import json_app_id
+from context.service.database.uuids.Context import context_get_uuid
+
+
+
+#from .Events import notify_event_context, notify_event_device, notify_event_topology
+
+LOGGER = logging.getLogger(__name__)
+
+
+def app_list_objs(db_engine : Engine) -> AppList:
+    def callback(session : Session) -> List[Dict]:
+        obj_list : List[AppModel] = session.query(AppModel)\
+            .all()
+        return [obj.dump() for obj in obj_list]
+    apps = run_transaction(sessionmaker(bind=db_engine), callback)
+    return AppList(apps=apps)
+
+def app_get(db_engine : Engine, request : AppId) -> App:
+    app_uuid = app_get_uuid(request, allow_random=False)
+    def callback(session : Session) -> Optional[Dict]:
+        obj : Optional[AppModel] = session.query(AppModel)\
+            .filter_by(app_uuid=app_uuid).one_or_none()
+        return None if obj is None else obj.dump()
+    obj = run_transaction(sessionmaker(bind=db_engine), callback)
+    if obj is None:
+        raw_app_uuid = request.app_uuid.uuid
+        raise NotFoundException('App', raw_app_uuid, extra_details=[
+            'app_uuid generated was: {:s}'.format(app_uuid)
+        ])
+    return App(**obj)
+
+def app_set(db_engine : Engine, messagebroker : MessageBroker, request : App) -> AppId:
+    context_uuid = context_get_uuid(request.app_id.context_id, allow_random=False)
+    raw_app_uuid = request.app_id.app_uuid.uuid
+    app_uuid = app_get_uuid(request.app_id, allow_random=True)
+
+    app_type = request.app_type
+    app_status = grpc_to_enum__qkd_app_status(request.app_status)
+    app_type = grpc_to_enum__qkd_app_types(request.app_type)
+
+    now = datetime.datetime.utcnow()
+
+    
+    app_data = [{
+        'context_uuid'       : context_uuid,
+        'app_uuid'           : app_uuid,
+        'app_status'         : app_status,
+        'app_type'           : app_type,
+        'server_app_id'      : request.server_app_id,
+        'client_app_id'      : request.client_app_id,
+        'backing_qkdl_uuid'  : [qkdl_id.qkdl_uuid.uuid for qkdl_id in request.backing_qkdl_id],
+        'local_device_uuid'  : request.local_device_id.device_uuid.uuid,
+        'remote_device_uuid' : request.remote_device_id.device_uuid.uuid or None,
+        'created_at'         : now,
+        'updated_at'         : now,
+    }]
+
+
+    def callback(session : Session) -> Tuple[bool, List[Dict]]:
+        stmt = insert(AppModel).values(app_data)
+        stmt = stmt.on_conflict_do_update(
+            index_elements=[AppModel.app_uuid],
+            set_=dict(
+                app_status         = stmt.excluded.app_status,
+                app_type           = stmt.excluded.app_type,
+                server_app_id      = stmt.excluded.server_app_id,
+                client_app_id      = stmt.excluded.client_app_id,
+                backing_qkdl_uuid  = stmt.excluded.backing_qkdl_uuid,
+                local_device_uuid  = stmt.excluded.local_device_uuid,
+                remote_device_uuid = stmt.excluded.remote_device_uuid,
+                updated_at         = stmt.excluded.updated_at,
+            )
+        )
+        stmt = stmt.returning(AppModel.created_at, AppModel.updated_at)
+        created_at,updated_at = session.execute(stmt).fetchone()
+        updated = updated_at > created_at
+
+        return updated
+
+    updated = run_transaction(sessionmaker(bind=db_engine), callback)
+    context_id = json_context_id(context_uuid)
+    app_id = json_app_id(app_uuid, context_id=context_id)
+    #event_type = EventTypeEnum.EVENTTYPE_UPDATE if updated else EventTypeEnum.EVENTTYPE_CREATE
+    #notify_event_app(messagebroker, event_type, app_id)
+    #notify_event_context(messagebroker, EventTypeEnum.EVENTTYPE_UPDATE, context_id)
+    return AppId(**app_id)
+
+
+
+def app_get_by_server(db_engine : Engine, request : str) -> App:
+    def callback(session : Session) -> Optional[Dict]:
+        obj : Optional[AppModel] = session.query(AppModel)\
+            .filter_by(server_app_id=request).one_or_none()
+        return None if obj is None else obj.dump()
+    obj = run_transaction(sessionmaker(bind=db_engine), callback)
+    if obj is None:
+        raise NotFoundException('No app match found for', request)
+    return App(**obj)
+
+
+
+"""
+def device_delete(db_engine : Engine, messagebroker : MessageBroker, request : DeviceId) -> Empty:
+    device_uuid = device_get_uuid(request, allow_random=False)
+    def callback(session : Session) -> Tuple[bool, List[Dict]]:
+        query = session.query(TopologyDeviceModel)
+        query = query.filter_by(device_uuid=device_uuid)
+        topology_device_list : List[TopologyDeviceModel] = query.all()
+        topology_ids = [obj.topology.dump_id() for obj in topology_device_list]
+        num_deleted = session.query(DeviceModel).filter_by(device_uuid=device_uuid).delete()
+        return num_deleted > 0, topology_ids
+    deleted, updated_topology_ids = run_transaction(sessionmaker(bind=db_engine), callback)
+    device_id = json_device_id(device_uuid)
+    if deleted:
+        notify_event_device(messagebroker, EventTypeEnum.EVENTTYPE_REMOVE, device_id)
+
+        context_ids  : Dict[str, Dict] = dict()
+        topology_ids : Dict[str, Dict] = dict()
+        for topology_id in updated_topology_ids:
+            topology_uuid = topology_id['topology_uuid']['uuid']
+            topology_ids[topology_uuid] = topology_id
+            context_id = topology_id['context_id']
+            context_uuid = context_id['context_uuid']['uuid']
+            context_ids[context_uuid] = context_id
+
+        for topology_id in topology_ids.values():
+            notify_event_topology(messagebroker, EventTypeEnum.EVENTTYPE_UPDATE, topology_id)
+
+        for context_id in context_ids.values():
+            notify_event_context(messagebroker, EventTypeEnum.EVENTTYPE_UPDATE, context_id)
+
+    return Empty()
+
+def device_select(db_engine : Engine, request : DeviceFilter) -> DeviceList:
+    device_uuids = [
+        device_get_uuid(device_id, allow_random=False)
+        for device_id in request.device_ids.device_ids
+    ]
+    dump_params = dict(
+        include_endpoints   =request.include_endpoints,
+        include_config_rules=request.include_config_rules,
+        include_components  =request.include_components,
+    )
+    def callback(session : Session) -> List[Dict]:
+        query = session.query(DeviceModel)
+        if request.include_endpoints   : query = query.options(selectinload(DeviceModel.endpoints))
+        if request.include_config_rules: query = query.options(selectinload(DeviceModel.config_rules))
+        #if request.include_components  : query = query.options(selectinload(DeviceModel.components))
+        obj_list : List[DeviceModel] = query.filter(DeviceModel.device_uuid.in_(device_uuids)).all()
+        return [obj.dump(**dump_params) for obj in obj_list]
+    devices = run_transaction(sessionmaker(bind=db_engine), callback)
+    return DeviceList(devices=devices)
+"""
diff --git a/src/qkd_app/service/database/__init__.py b/src/qkd_app/service/database/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..07d08814021ef82220611ee21c01ba01806682e9
--- /dev/null
+++ b/src/qkd_app/service/database/__init__.py
@@ -0,0 +1,13 @@
+# Copyright 2022-2024 ETSI OSG/SDG TeraFlowSDN (TFS) (https://tfs.etsi.org/)
+#
+# 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.
diff --git a/src/qkd_app/service/database/models/QKDAppModel.py b/src/qkd_app/service/database/models/QKDAppModel.py
new file mode 100644
index 0000000000000000000000000000000000000000..c32b4e28c95105d8659cb52790f51b330764c2cf
--- /dev/null
+++ b/src/qkd_app/service/database/models/QKDAppModel.py
@@ -0,0 +1,63 @@
+# Copyright 2022-2024 ETSI OSG/SDG TeraFlowSDN (TFS) (https://tfs.etsi.org/)
+#
+# 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.
+
+import operator
+from sqlalchemy import CheckConstraint, Column, DateTime, Float, Enum, ForeignKey, Integer, String
+from sqlalchemy.dialects.postgresql import UUID, ARRAY
+from sqlalchemy.orm import relationship
+from typing import Dict
+from ._Base import _Base
+from .enums.QKDAppStatus import ORM_QKDAppStatusEnum
+from .enums.QKDAppTypes import ORM_QKDAppTypesEnum
+
+class AppModel(_Base):
+    __tablename__ = 'qkd_app'
+
+    app_uuid            = Column(UUID(as_uuid=False), primary_key=True)
+    context_uuid        = Column(UUID(as_uuid=False), nullable=False) # Supposed to be Foreign Key
+    app_status          = Column(Enum(ORM_QKDAppStatusEnum), nullable=False)
+    app_type            = Column(Enum(ORM_QKDAppTypesEnum), nullable=False)
+    server_app_id       = Column(String, nullable=False)
+    client_app_id       = Column(ARRAY(String), nullable=False)
+    backing_qkdl_uuid   = Column(ARRAY(UUID(as_uuid=False)), nullable=False)
+    local_device_uuid   = Column(UUID(as_uuid=False), nullable=False)
+    remote_device_uuid  = Column(UUID(as_uuid=False), nullable=True)
+
+    # Optare: Created_at and Updated_at are only used to know if an app was updated later on the code. Don't change it
+
+    created_at          = Column(DateTime, nullable=False)
+    updated_at          = Column(DateTime, nullable=False)
+
+    #__table_args__ = (
+    #    CheckConstraint(... >= 0, name='name_value_...'),
+    #)
+
+    def dump_id(self) -> Dict:
+        return {
+            'context_id': {'context_uuid': {'uuid': self.context_uuid}},
+            'app_uuid': {'uuid': self.app_uuid}
+        }
+
+    def dump(self) -> Dict:
+        result = {
+            'app_id'           : self.dump_id(),
+            'app_status'       : self.app_status.value,
+            'app_type'         : self.app_type.value,
+            'server_app_id'    : self.server_app_id,
+            'client_app_id'    : self.client_app_id,
+            'backing_qkdl_id'  : [{'qkdl_uuid': {'uuid': qkdl_id}} for qkdl_id in self.backing_qkdl_uuid],
+            'local_device_id'  : {'device_uuid': {'uuid': self.local_device_uuid}},
+            'remote_device_id' : {'device_uuid': {'uuid': self.remote_device_uuid}},
+        }
+        return result
diff --git a/src/qkd_app/service/database/models/_Base.py b/src/qkd_app/service/database/models/_Base.py
new file mode 100644
index 0000000000000000000000000000000000000000..51863e1d5c06a875c298eab726cfdc3b7fcb75ca
--- /dev/null
+++ b/src/qkd_app/service/database/models/_Base.py
@@ -0,0 +1,44 @@
+# Copyright 2022-2024 ETSI OSG/SDG TeraFlowSDN (TFS) (https://tfs.etsi.org/)
+#
+# 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.
+
+import sqlalchemy
+from typing import Any, List
+from sqlalchemy.orm import Session, sessionmaker, declarative_base
+from sqlalchemy.sql import text
+from sqlalchemy_cockroachdb import run_transaction
+
+_Base = declarative_base()
+
+'''
+def create_performance_enhancers(db_engine : sqlalchemy.engine.Engine) -> None:
+    def index_storing(
+        index_name : str, table_name : str, index_fields : List[str], storing_fields : List[str]
+    ) -> Any:
+        str_index_fields = ','.join(['"{:s}"'.format(index_field) for index_field in index_fields])
+        str_storing_fields = ','.join(['"{:s}"'.format(storing_field) for storing_field in storing_fields])
+        INDEX_STORING = 'CREATE INDEX IF NOT EXISTS {:s} ON "{:s}" ({:s}) STORING ({:s});'
+        return text(INDEX_STORING.format(index_name, table_name, str_index_fields, str_storing_fields))
+
+    statements = [
+        # In case of relations
+    ]
+    def callback(session : Session) -> bool:
+        for stmt in statements: session.execute(stmt)
+    run_transaction(sessionmaker(bind=db_engine), callback)
+'''
+
+def rebuild_database(db_engine : sqlalchemy.engine.Engine, drop_if_exists : bool = False):
+    if drop_if_exists: _Base.metadata.drop_all(db_engine)
+    _Base.metadata.create_all(db_engine)
+    #create_performance_enhancers(db_engine)
diff --git a/src/qkd_app/service/database/models/__init__.py b/src/qkd_app/service/database/models/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..07d08814021ef82220611ee21c01ba01806682e9
--- /dev/null
+++ b/src/qkd_app/service/database/models/__init__.py
@@ -0,0 +1,13 @@
+# Copyright 2022-2024 ETSI OSG/SDG TeraFlowSDN (TFS) (https://tfs.etsi.org/)
+#
+# 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.
diff --git a/src/qkd_app/service/database/models/enums/QKDAppStatus.py b/src/qkd_app/service/database/models/enums/QKDAppStatus.py
new file mode 100644
index 0000000000000000000000000000000000000000..d3063ef56704ce1bdd48d15ea8c6486ed7c8cfae
--- /dev/null
+++ b/src/qkd_app/service/database/models/enums/QKDAppStatus.py
@@ -0,0 +1,27 @@
+# Copyright 2022-2024 ETSI OSG/SDG TeraFlowSDN (TFS) (https://tfs.etsi.org/)
+#
+# 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.
+
+import enum, functools
+from common.proto.qkd_app_pb2 import QKDAppStatusEnum
+from ._GrpcToEnum import grpc_to_enum
+
+class ORM_QKDAppStatusEnum(enum.Enum):
+    ON           = QKDAppStatusEnum.QKDAPPSTATUS_ON
+    DISCONNECTED = QKDAppStatusEnum.QKDAPPSTATUS_DISCONNECTED
+    OUT_OF_TIME  = QKDAppStatusEnum.QKDAPPSTATUS_OUT_OF_TIME
+    ZOMBIE       = QKDAppStatusEnum.QKDAPPSTATUS_ZOMBIE
+
+
+grpc_to_enum__qkd_app_status = functools.partial(
+    grpc_to_enum, QKDAppStatusEnum, ORM_QKDAppStatusEnum)
diff --git a/src/qkd_app/service/database/models/enums/QKDAppTypes.py b/src/qkd_app/service/database/models/enums/QKDAppTypes.py
new file mode 100644
index 0000000000000000000000000000000000000000..f50b8982d80c0af97c2cbd96d336f450afc50f9b
--- /dev/null
+++ b/src/qkd_app/service/database/models/enums/QKDAppTypes.py
@@ -0,0 +1,25 @@
+# Copyright 2022-2024 ETSI OSG/SDG TeraFlowSDN (TFS) (https://tfs.etsi.org/)
+#
+# 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.
+
+import enum, functools
+from common.proto.qkd_app_pb2 import QKDAppTypesEnum
+from ._GrpcToEnum import grpc_to_enum
+
+class ORM_QKDAppTypesEnum(enum.Enum):
+    INTERNAL = QKDAppTypesEnum.QKDAPPTYPES_INTERNAL
+    CLIENT   = QKDAppTypesEnum.QKDAPPTYPES_CLIENT
+
+
+grpc_to_enum__qkd_app_types = functools.partial(
+    grpc_to_enum, QKDAppTypesEnum, ORM_QKDAppTypesEnum)
diff --git a/src/qkd_app/service/database/models/enums/_GrpcToEnum.py b/src/qkd_app/service/database/models/enums/_GrpcToEnum.py
new file mode 100644
index 0000000000000000000000000000000000000000..0dcad039f7be240acbbec418d12475557e4c42c1
--- /dev/null
+++ b/src/qkd_app/service/database/models/enums/_GrpcToEnum.py
@@ -0,0 +1,38 @@
+# Copyright 2022-2024 ETSI OSG/SDG TeraFlowSDN (TFS) (https://tfs.etsi.org/)
+#
+# 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.
+
+import re
+from enum import Enum
+from typing import Optional
+
+# Enumeration classes are redundant with gRPC classes, but gRPC does not provide a programmatical method to retrieve
+# the values it expects from strings containing the desired value symbol or its integer value, so a kind of mapping is
+# required. Besides, ORM Models expect Enum classes in EnumeratedFields; we create specific and conveniently defined
+# Enum classes to serve both purposes.
+
+def grpc_to_enum(grpc_enum_class, orm_enum_class : Enum, grpc_enum_value, grpc_enum_prefix : Optional[str] = None):
+    enum_name = grpc_enum_class.Name(grpc_enum_value)
+
+    if grpc_enum_prefix is None:
+        grpc_enum_prefix = orm_enum_class.__name__.upper()
+        #grpc_enum_prefix = re.sub(r'^ORM_(.+)$', r'\1', grpc_enum_prefix)
+        #grpc_enum_prefix = re.sub(r'^(.+)ENUM$', r'\1', grpc_enum_prefix)
+        #grpc_enum_prefix = grpc_enum_prefix + '_'
+        grpc_enum_prefix = re.sub(r'^ORM_(.+)ENUM$', r'\1_', grpc_enum_prefix)
+
+    if len(grpc_enum_prefix) > 0:
+        enum_name = enum_name.replace(grpc_enum_prefix, '')
+
+    orm_enum_value = orm_enum_class._member_map_.get(enum_name)
+    return orm_enum_value
diff --git a/src/qkd_app/service/database/models/enums/__init__.py b/src/qkd_app/service/database/models/enums/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..07d08814021ef82220611ee21c01ba01806682e9
--- /dev/null
+++ b/src/qkd_app/service/database/models/enums/__init__.py
@@ -0,0 +1,13 @@
+# Copyright 2022-2024 ETSI OSG/SDG TeraFlowSDN (TFS) (https://tfs.etsi.org/)
+#
+# 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.
diff --git a/src/qkd_app/service/database/uuids/QKDApp.py b/src/qkd_app/service/database/uuids/QKDApp.py
new file mode 100644
index 0000000000000000000000000000000000000000..175f1d5f3cf4ceda12a022b4afadb376e11ae5a5
--- /dev/null
+++ b/src/qkd_app/service/database/uuids/QKDApp.py
@@ -0,0 +1,30 @@
+# Copyright 2022-2024 ETSI OSG/SDG TeraFlowSDN (TFS) (https://tfs.etsi.org/)
+#
+# 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 common.proto.qkd_app_pb2 import AppId
+from common.method_wrappers.ServiceExceptions import InvalidArgumentsException
+from ._Builder import get_uuid_from_string, get_uuid_random
+
+def app_get_uuid(
+    app_id : AppId, allow_random : bool = False
+) -> str:
+    app_uuid = app_id.app_uuid.uuid
+
+    if len(app_uuid) > 0:
+        return get_uuid_from_string(app_uuid)
+    if allow_random: return get_uuid_random()
+
+    raise InvalidArgumentsException([
+        ('app_id.app_uuid.uuid', app_uuid),
+    ], extra_details=['At least one is required to produce a App UUID'])
diff --git a/src/qkd_app/service/database/uuids/_Builder.py b/src/qkd_app/service/database/uuids/_Builder.py
new file mode 100644
index 0000000000000000000000000000000000000000..39c98de69d577ce2722693e57c4ee678124f9e30
--- /dev/null
+++ b/src/qkd_app/service/database/uuids/_Builder.py
@@ -0,0 +1,44 @@
+# Copyright 2022-2024 ETSI OSG/SDG TeraFlowSDN (TFS) (https://tfs.etsi.org/)
+#
+# 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 typing import Optional, Union
+from uuid import UUID, uuid4, uuid5
+
+# Generate a UUIDv5-like from the SHA-1 of "TFS" and no namespace to be used as the NAMESPACE for all
+# the context UUIDs generated. For efficiency purposes, the UUID is hardcoded; however, it is produced
+# using the following code:
+#    from hashlib import sha1
+#    from uuid import UUID
+#    hash = sha1(bytes('TFS', 'utf-8')).digest()
+#    NAMESPACE_TFS = UUID(bytes=hash[:16], version=5)
+NAMESPACE_TFS = UUID('200e3a1f-2223-534f-a100-758e29c37f40')
+
+def get_uuid_from_string(str_uuid_or_name : Union[str, UUID], prefix_for_name : Optional[str] = None) -> str:
+    # if UUID given, assume it is already a valid UUID
+    if isinstance(str_uuid_or_name, UUID): return str_uuid_or_name
+    if not isinstance(str_uuid_or_name, str):
+        MSG = 'Parameter({:s}) cannot be used to produce a UUID'
+        raise Exception(MSG.format(str(repr(str_uuid_or_name))))
+    try:
+        # try to parse as UUID
+        return str(UUID(str_uuid_or_name))
+    except: # pylint: disable=bare-except
+        # produce a UUID within TFS namespace from parameter
+        if prefix_for_name is not None:
+            str_uuid_or_name = '{:s}/{:s}'.format(prefix_for_name, str_uuid_or_name)
+        return str(uuid5(NAMESPACE_TFS, str_uuid_or_name))
+
+def get_uuid_random() -> str:
+    # Generate random UUID. No need to use namespace since "namespace + random = random".
+    return str(uuid4())
diff --git a/src/qkd_app/service/rest_server/RestServer.py b/src/qkd_app/service/rest_server/RestServer.py
new file mode 100644
index 0000000000000000000000000000000000000000..4302a0562337ad9af8c28ef037d4fb64ec1e53bf
--- /dev/null
+++ b/src/qkd_app/service/rest_server/RestServer.py
@@ -0,0 +1,23 @@
+# Copyright 2022-2024 ETSI OSG/SDG TeraFlowSDN (TFS) (https://tfs.etsi.org/)
+#
+# 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 common.Constants import ServiceNameEnum
+from common.Settings import get_service_baseurl_http, get_service_port_http
+from common.tools.service.GenericRestServer import GenericRestServer
+
+class RestServer(GenericRestServer):
+    def __init__(self, cls_name: str = __name__) -> None:
+        bind_port = get_service_port_http(ServiceNameEnum.APP)
+        base_url = get_service_baseurl_http(ServiceNameEnum.APP)
+        super().__init__(bind_port, base_url, cls_name=cls_name)
diff --git a/src/qkd_app/service/rest_server/__init__.py b/src/qkd_app/service/rest_server/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..07d08814021ef82220611ee21c01ba01806682e9
--- /dev/null
+++ b/src/qkd_app/service/rest_server/__init__.py
@@ -0,0 +1,13 @@
+# Copyright 2022-2024 ETSI OSG/SDG TeraFlowSDN (TFS) (https://tfs.etsi.org/)
+#
+# 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.
diff --git a/src/qkd_app/service/rest_server/qkd_app/Resources.py b/src/qkd_app/service/rest_server/qkd_app/Resources.py
new file mode 100644
index 0000000000000000000000000000000000000000..6ba79d3940da91dfebc1a1c666893548caccbe6c
--- /dev/null
+++ b/src/qkd_app/service/rest_server/qkd_app/Resources.py
@@ -0,0 +1,86 @@
+# Copyright 2022-2024 ETSI OSG/SDG TeraFlowSDN (TFS) (https://tfs.etsi.org/)
+#
+# 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.
+
+import uuid, json
+from flask import request
+from flask_restful import Resource
+from common.proto.context_pb2 import Empty
+from common.proto.qkd_app_pb2 import App, QKDAppTypesEnum
+from common.Constants import DEFAULT_CONTEXT_NAME
+from context.client.ContextClient import ContextClient
+from qkd_app.client.QKDAppClient import QKDAppClient
+
+
+class _Resource(Resource):
+    def __init__(self) -> None:
+        super().__init__()
+        self.context_client = ContextClient()
+        self.qkd_app_client = QKDAppClient()
+
+class Index(_Resource):
+    def get(self):
+        return {'hello': 'world'}
+
+class CreateQKDApp(_Resource):
+    # Optare: Post request for the QKD Node to call the TeraflowSDN. Example of requests below
+    def post(self):
+        app = request.get_json()['app']
+
+        devices = self.context_client.ListDevices(Empty())
+        devices = devices.devices
+
+        local_device = None
+
+
+        # This for-loop won't be necessary if we can garantee Device ID is the same as QKDN Id
+        for device in devices:
+            for config_rule in device.device_config.config_rules:
+                if config_rule.custom.resource_key == '__node__':
+                    value = json.loads(config_rule.custom.resource_value)
+                    qkdn_id = value['qkdn_id']
+                    if app['local_qkdn_id'] == qkdn_id:
+                        local_device = device
+                    break
+
+        # Optare: Todo:  Verify that a service is present for this app
+        '''
+        requests.post('http://10.211.36.220/app/create_qkd_app', json={'app': {'server_app_id':'1', 'client_app_id':[], 'app_status':'ON', 'local_qkdn_id':'00000001-0000-0000-0000-000000000000', 'backing_qkdl_id':['00000003-0002-0000-0000-000000000000']}})
+
+
+        requests.post('http://10.211.36.220/app/create_qkd_app', json={'app': {'server_app_id':'1', 'client_app_id':[], 'app_status':'ON', 'local_qkdn_id':'00000003-0000-0000-0000-000000000000', 'backing_qkdl_id':['00000003-0002-0000-0000-000000000000']}})
+        '''
+        
+
+        if local_device is None:
+            return {"status": "fail"}
+
+        external_app_src_dst = {
+            'app_id': {'context_id': {'context_uuid': {'uuid': DEFAULT_CONTEXT_NAME}}, 'app_uuid': {'uuid': ''}},
+            'app_status': 'QKDAPPSTATUS_' + app['app_status'],
+            'app_type': QKDAppTypesEnum.QKDAPPTYPES_CLIENT,
+            'server_app_id': app['server_app_id'],
+            'client_app_id': app['client_app_id'],
+            'backing_qkdl_id': [{'qkdl_uuid': {'uuid': qkdl_id}} for qkdl_id in app['backing_qkdl_id']],
+            'local_device_id': local_device.device_id,
+            'remote_device_id': {'device_uuid': {'uuid': ''}},
+        }
+
+
+        # Optare: This will call our internal RegisterApp which supports the creation of both internal and external app.
+        # Optare the verification for knowing if two parties are requesting the same app is done inside RegisterApp's function
+        self.qkd_app_client.RegisterApp(App(**external_app_src_dst))
+
+        # Optare: Todo: Communicate by SBI with both Nodes of the new App
+
+        return {"status": "success"}
diff --git a/src/qkd_app/service/rest_server/qkd_app/__init__.py b/src/qkd_app/service/rest_server/qkd_app/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..6fc23b371414dcb2bac4afde63524febf71e5337
--- /dev/null
+++ b/src/qkd_app/service/rest_server/qkd_app/__init__.py
@@ -0,0 +1,30 @@
+# Copyright 2022-2024 ETSI OSG/SDG TeraFlowSDN (TFS) (https://tfs.etsi.org/)
+#
+# 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 qkd_app.service.rest_server.RestServer import RestServer
+from .Resources import (
+    CreateQKDApp, Index)
+
+URL_PREFIX = '/qkd_app'
+
+# Use 'path' type since some identifiers might contain char '/' and Flask is unable to recognize them in 'string' type.
+RESOURCES = [
+    # (endpoint_name, resource_class, resource_url)
+    ('api.index',    Index,    '/'),
+    ('api.register_qkd_app',    CreateQKDApp,    '/create_qkd_app'),
+]
+
+def register_qkd_app(app_server : RestServer):
+    for endpoint_name, resource_class, resource_url in RESOURCES:
+        app_server.add_resource(resource_class, URL_PREFIX + resource_url, endpoint=endpoint_name)
diff --git a/src/service/Dockerfile b/src/service/Dockerfile
index a847ae762d1303dda852d7f3d8200d3db3ef53f7..6f23f0a89387e2db3802ee20e003ccb6482844ff 100644
--- a/src/service/Dockerfile
+++ b/src/service/Dockerfile
@@ -70,6 +70,8 @@ COPY src/pathcomp/frontend/__init__.py pathcomp/frontend/__init__.py
 COPY src/pathcomp/frontend/client/. pathcomp/frontend/client/
 COPY src/e2e_orchestrator/__init__.py e2e_orchestrator/__init__.py
 COPY src/e2e_orchestrator/client/. e2e_orchestrator/client/
+COPY src/qkd_app/__init__.py qkd_app/__init__.py
+COPY src/qkd_app/client/. qkd_app/client/
 COPY src/service/. service/
 
 # Start the service
diff --git a/src/service/service/__main__.py b/src/service/service/__main__.py
index ae8a9e960cf6a0d720b508fe3ea5592632420c18..5f9f2fa3afb2b708068423f70f764ffc61d29d9c 100644
--- a/src/service/service/__main__.py
+++ b/src/service/service/__main__.py
@@ -44,6 +44,8 @@ def main():
         get_env_var_name(ServiceNameEnum.DEVICE,   ENVVAR_SUFIX_SERVICE_PORT_GRPC),
         get_env_var_name(ServiceNameEnum.PATHCOMP, ENVVAR_SUFIX_SERVICE_HOST     ),
         get_env_var_name(ServiceNameEnum.PATHCOMP, ENVVAR_SUFIX_SERVICE_PORT_GRPC),
+        get_env_var_name(ServiceNameEnum.QKD_APP,  ENVVAR_SUFIX_SERVICE_HOST     ),
+        get_env_var_name(ServiceNameEnum.QKD_APP,  ENVVAR_SUFIX_SERVICE_PORT_GRPC),
     ])
 
     signal.signal(signal.SIGINT,  signal_handler)
diff --git a/src/service/service/service_handlers/qkd/qkd_service_handler.py b/src/service/service/service_handlers/qkd/qkd_service_handler.py
index 76c67867ee2f4bae60b8dd6e187f221f2efc1eb0..0977388005ef72fe036de93de2dc73438f0c6163 100644
--- a/src/service/service/service_handlers/qkd/qkd_service_handler.py
+++ b/src/service/service/service_handlers/qkd/qkd_service_handler.py
@@ -17,7 +17,7 @@ import json, logging, uuid
 from typing import Any, Dict, List, Optional, Tuple, Union
 from common.method_wrappers.Decorator import MetricsPool, metered_subclass_method
 from common.proto.context_pb2 import ConfigRule, DeviceId, Service
-from common.proto.app_pb2 import App, QKDAppStatusEnum, QKDAppTypesEnum
+from common.proto.qkd_app_pb2 import App, QKDAppStatusEnum, QKDAppTypesEnum
 from common.tools.object_factory.ConfigRule import json_config_rule_delete, json_config_rule_set
 from common.tools.object_factory.Device import json_device_id
 from common.type_checkers.Checkers import chk_type
diff --git a/src/service/service/task_scheduler/TaskExecutor.py b/src/service/service/task_scheduler/TaskExecutor.py
index cd20faad23a06678be39dbacc476a0ea25d4d540..cb27993702963b4aac88ec04eca2a1c796d0c364 100644
--- a/src/service/service/task_scheduler/TaskExecutor.py
+++ b/src/service/service/task_scheduler/TaskExecutor.py
@@ -20,6 +20,7 @@ from common.proto.context_pb2 import (
     Connection, ConnectionId, Device, DeviceDriverEnum, DeviceId, Service, ServiceId,
     OpticalConfig, OpticalConfigId
 )
+from common.proto.qkd_app_pb2 import App
 from common.tools.context_queries.Connection import get_connection_by_id
 from common.tools.context_queries.Device import get_device
 from common.tools.context_queries.Service import get_service_by_id
@@ -27,11 +28,12 @@ from common.tools.grpc.Tools import grpc_message_to_json_string
 from common.tools.object_factory.Device import json_device_id
 from context.client.ContextClient import ContextClient
 from device.client.DeviceClient import DeviceClient
+from qkd_app.client.QKDAppClient import QKDAppClient
 from service.service.service_handler_api.Exceptions import (
     UnsatisfiedFilterException, UnsupportedFilterFieldException, UnsupportedFilterFieldValueException
 )
 from service.service.service_handler_api.ServiceHandlerFactory import ServiceHandlerFactory, get_service_handler_class
-from service.service.tools.ObjectKeys import get_connection_key, get_device_key, get_service_key
+from service.service.tools.ObjectKeys import get_connection_key, get_device_key, get_service_key, get_app_key
 
 if TYPE_CHECKING:
     from service.service.service_handler_api._ServiceHandler import _ServiceHandler
@@ -44,11 +46,14 @@ class CacheableObjectType(Enum):
     CONNECTION = 'connection'
     DEVICE     = 'device'
     SERVICE    = 'service'
+    QKD_APP    = 'qkd-app'
 
 class TaskExecutor:
     def __init__(self, service_handler_factory : ServiceHandlerFactory) -> None:
         self._service_handler_factory = service_handler_factory
         self._context_client = ContextClient()
+        # DEPENDENCY QKD
+        self._qkd_app_client = QKDAppClient()
         self._device_client = DeviceClient()
         self._grpc_objects_cache : Dict[str, CacheableObject] = dict()
 
@@ -220,3 +225,12 @@ class TaskExecutor:
                     str(dict_connection_devices)
                 )
             )
+
+
+    # ----- QkdApp-related methods -------------------------------------------------------------------------------------
+
+    def register_app(self, app: App) -> None:
+        app_key = get_app_key(app.app_id)
+        self._qkd_app_client.RegisterApp(app)
+        LOGGER.info("reg registered")
+        self._store_grpc_object(CacheableObjectType.QKD_APP, app_key, app)
diff --git a/src/service/service/tools/ObjectKeys.py b/src/service/service/tools/ObjectKeys.py
index f45126e07df6a74a20e507fd51d08f1a32de7f98..cfc719bba736a4ea0789b028a97ca267b2d04089 100644
--- a/src/service/service/tools/ObjectKeys.py
+++ b/src/service/service/tools/ObjectKeys.py
@@ -13,6 +13,7 @@
 # limitations under the License.
 
 from common.proto.context_pb2 import ConnectionId, DeviceId, ServiceId
+from common.proto.qkd_app_pb2 import AppId
 
 def get_connection_key(connection_id : ConnectionId) -> str:
     return connection_id.connection_uuid.uuid
@@ -24,3 +25,7 @@ def get_service_key(service_id : ServiceId) -> str:
     context_uuid = service_id.context_id.context_uuid.uuid
     service_uuid = service_id.service_uuid.uuid
     return '{:s}/{:s}'.format(context_uuid, service_uuid)
+
+def get_app_key(app_id : AppId) -> str:
+    return app_id.app_uuid.uuid
+
diff --git a/src/webui/Dockerfile b/src/webui/Dockerfile
index 55e67b670f36812a55cf60e411cf137bc5b8a2ee..204b6f7311d4d182d830ef5483204790e165599b 100644
--- a/src/webui/Dockerfile
+++ b/src/webui/Dockerfile
@@ -84,6 +84,8 @@ COPY --chown=webui:webui src/service/__init__.py service/__init__.py
 COPY --chown=webui:webui src/service/client/. service/client/
 COPY --chown=webui:webui src/slice/__init__.py slice/__init__.py
 COPY --chown=webui:webui src/slice/client/. slice/client/
+COPY --chown=webui:webui src/app/__init__.py app/__init__.py
+COPY --chown=webui:webui src/app/client/. app/client/
 COPY --chown=webui:webui src/webui/. webui/
 COPY --chown=webui:webui src/bgpls_speaker/__init__.py bgpls_speaker/__init__.py
 COPY --chown=webui:webui src/bgpls_speaker/client/. bgpls_speaker/client/
diff --git a/src/webui/service/__init__.py b/src/webui/service/__init__.py
index b864d3549e051b54e888c80547724da14fec5f67..990792069597f9116ad999487a8982708e978d40 100644
--- a/src/webui/service/__init__.py
+++ b/src/webui/service/__init__.py
@@ -19,6 +19,7 @@ from flask_healthz import healthz, HealthError
 from common.tools.grpc.Tools import grpc_message_to_json
 from context.client.ContextClient import ContextClient
 from device.client.DeviceClient import DeviceClient
+from qkd_app.client.QKDAppClient import QKDAppClient
 
 def get_working_context() -> str:
     return session['context_uuid'] if 'context_uuid' in session else '---'
@@ -37,6 +38,10 @@ def readiness():
         device_client = DeviceClient()
         device_client.connect()
         device_client.close()
+        # DEPENDENCY QKD
+        qkd_app_client = QKDAppClient()
+        qkd_app_client.connect()
+        qkd_app_client.close()
     except Exception as e:
         raise HealthError("Can't connect with the service: {:s}".format(str(e))) from e
 
@@ -102,6 +107,9 @@ def create_app(use_config=None, web_app_root=None):
     from webui.service.link.routes import link              # pylint: disable=import-outside-toplevel
     app.register_blueprint(link)
 
+    from webui.service.qkd_app.routes import qkd_app as _qkd_app             # pylint: disable=import-outside-toplevel
+    app.register_blueprint(_qkd_app)
+
     from webui.service.policy_rule.routes import policy_rule # pylint: disable=import-outside-toplevel
     app.register_blueprint(policy_rule)
 
diff --git a/src/webui/service/__main__.py b/src/webui/service/__main__.py
index e9a906e8a431e287911547abc4065d9d9364ccb4..8cb577d7149f08fa05c11961945f2b8d98beaa00 100644
--- a/src/webui/service/__main__.py
+++ b/src/webui/service/__main__.py
@@ -33,6 +33,7 @@ def main():
     logging.basicConfig(level=log_level)
     logger = logging.getLogger(__name__)
 
+    # DEPENDENCY QKD
     wait_for_environment_variables([
         get_env_var_name(ServiceNameEnum.CONTEXT, ENVVAR_SUFIX_SERVICE_HOST     ),
         get_env_var_name(ServiceNameEnum.CONTEXT, ENVVAR_SUFIX_SERVICE_PORT_GRPC),
@@ -42,6 +43,8 @@ def main():
         get_env_var_name(ServiceNameEnum.SERVICE, ENVVAR_SUFIX_SERVICE_PORT_GRPC),
         get_env_var_name(ServiceNameEnum.SLICE,   ENVVAR_SUFIX_SERVICE_HOST     ),
         get_env_var_name(ServiceNameEnum.SLICE,   ENVVAR_SUFIX_SERVICE_PORT_GRPC),
+        get_env_var_name(ServiceNameEnum.APP,   ENVVAR_SUFIX_SERVICE_HOST     ),
+        get_env_var_name(ServiceNameEnum.APP,   ENVVAR_SUFIX_SERVICE_PORT_GRPC),
     ])
 
     logger.info('Starting...')
diff --git a/src/webui/service/device/forms.py b/src/webui/service/device/forms.py
index e4c71d92170dc9fe46996a1c93978647800aa300..eebc06755f204cd270ff8feca21733cb4426493a 100644
--- a/src/webui/service/device/forms.py
+++ b/src/webui/service/device/forms.py
@@ -33,6 +33,7 @@ class AddDeviceForm(FlaskForm):
     device_drivers_gnmi_openconfig = BooleanField('GNMI OPENCONFIG')
     device_drivers_optical_tfs = BooleanField('OPTICAL TFS')
     device_drivers_ietf_actn = BooleanField('IETF ACTN')
+    device_drivers_qkd = BooleanField('QKD')
 
     device_config_address = StringField('connect/address',default='127.0.0.1',validators=[DataRequired(), Length(min=5)])
     device_config_port = StringField('connect/port',default='0',validators=[DataRequired(), Length(min=1)])
@@ -57,3 +58,4 @@ class UpdateDeviceForm(FlaskForm):
                            validators=[NumberRange(min=0)])
                         
     submit = SubmitField('Update')
+    
diff --git a/src/webui/service/device/routes.py b/src/webui/service/device/routes.py
index b7fdb78e85dc634627de02947c0861a7f13bdae9..429f4a2ea8539b7b12baf5e20eb30760694ede64 100644
--- a/src/webui/service/device/routes.py
+++ b/src/webui/service/device/routes.py
@@ -129,6 +129,8 @@ def add():
             device_drivers.append(DeviceDriverEnum.DEVICEDRIVER_OPTICAL_TFS)
         if form.device_drivers_ietf_actn.data:
             device_drivers.append(DeviceDriverEnum.DEVICEDRIVER_IETF_ACTN)
+        if form.device_drivers_qkd.data:
+            device_drivers.append(DeviceDriverEnum.DEVICEDRIVER_QKD)
         device_obj.device_drivers.extend(device_drivers) # pylint: disable=no-member
 
         try:
diff --git a/src/webui/service/qkd_app/__init__.py b/src/webui/service/qkd_app/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..bbfc943b68af13a11e562abbc8680ade71db8f02
--- /dev/null
+++ b/src/webui/service/qkd_app/__init__.py
@@ -0,0 +1,13 @@
+# Copyright 2022-2024 ETSI OSG/SDG TeraFlowSDN (TFS) (https://tfs.etsi.org/)
+#
+# 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.
diff --git a/src/webui/service/qkd_app/routes.py b/src/webui/service/qkd_app/routes.py
new file mode 100644
index 0000000000000000000000000000000000000000..5cbe715dcfe6de16d84706b28799e22018737f29
--- /dev/null
+++ b/src/webui/service/qkd_app/routes.py
@@ -0,0 +1,113 @@
+# Copyright 2022-2024 ETSI OSG/SDG TeraFlowSDN (TFS) (https://tfs.etsi.org/)
+#
+# 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.
+
+import grpc, json, logging
+
+from flask import current_app, render_template, Blueprint, flash, session, redirect, url_for
+from common.proto.context_pb2 import Empty, Link, LinkId, LinkList
+from common.proto.qkd_app_pb2 import App, QKDAppStatusEnum, QKDAppTypesEnum
+from common.tools.context_queries.Context import get_context
+from common.tools.context_queries.Device import get_device
+from common.tools.context_queries.Topology import get_topology
+from context.client.ContextClient import ContextClient
+from qkd_app.client.QKDAppClient import QKDAppClient
+
+
+LOGGER = logging.getLogger(__name__)
+app = Blueprint('qkd_app', __name__, url_prefix='/qkd_app')
+
+qkd_app_client = QKDAppClient()
+context_client = ContextClient()
+
+@app.get('/')
+def home():
+    if 'context_uuid' not in session or 'topology_uuid' not in session:
+        flash("Please select a context!", "warning")
+        return redirect(url_for("main.home"))
+    context_uuid = session['context_uuid']
+    topology_uuid = session['topology_uuid']
+
+    context_client.connect()
+    device_names = dict()
+
+    context_obj = get_context(context_client, context_uuid, rw_copy=False)
+    if context_obj is None:
+        flash('Context({:s}) not found'.format(str(context_uuid)), 'danger')
+        apps = list()
+    else:
+        try:
+            apps = qkd_app_client.ListApps(context_obj.context_id)
+            apps = apps.apps
+        except grpc.RpcError as e:
+            if e.code() != grpc.StatusCode.NOT_FOUND: raise
+            if e.details() != 'Context({:s}) not found'.format(context_uuid): raise
+            apps = list()
+        else:
+            # Too many requests to context_client if it has too many apps (update in the future)
+            for app in apps:
+                if app.local_device_id.device_uuid.uuid not in device_names:
+                    device = get_device(context_client, app.local_device_id.device_uuid.uuid)
+                    if device is not None:
+                        device_names[app.local_device_id.device_uuid.uuid] = device.name
+                
+                if app.remote_device_id.device_uuid.uuid and app.remote_device_id.device_uuid.uuid not in device_names:
+                    device = get_device(context_client, app.remote_device_id.device_uuid.uuid)
+                    if device is not None:
+                        device_names[app.remote_device_id.device_uuid.uuid] = device.name
+
+    context_client.close()
+    return render_template(
+        'app/home.html', apps=apps, device_names=device_names, ate=QKDAppTypesEnum, ase=QKDAppStatusEnum)
+
+
+@app.route('detail/<path:app_uuid>', methods=('GET', 'POST'))
+def detail(app_uuid: str):
+    '''
+    context_client.connect()
+    link_obj = get_link(context_client, link_uuid, rw_copy=False)
+    if link_obj is None:
+        flash('Link({:s}) not found'.format(str(link_uuid)), 'danger')
+        link_obj = Link()
+        device_names, endpoints_data = dict(), dict()
+    else:
+        device_names, endpoints_data = get_endpoint_names(context_client, link_obj.link_endpoint_ids)
+    context_client.close()
+    return render_template('link/detail.html',link=link_obj, device_names=device_names, endpoints_data=endpoints_data)
+    '''
+    pass
+
+@app.get('<path:app_uuid>/delete')
+def delete(app_uuid):
+    '''
+    try:
+
+        # first, check if link exists!
+        # request: LinkId = LinkId()
+        # request.link_uuid.uuid = link_uuid
+        # response: Link = client.GetLink(request)
+        # TODO: finalize implementation
+
+        request = LinkId()
+        request.link_uuid.uuid = link_uuid # pylint: disable=no-member
+        context_client.connect()
+        context_client.RemoveLink(request)
+        context_client.close()
+
+        flash(f'Link "{link_uuid}" deleted successfully!', 'success')
+    except Exception as e: # pylint: disable=broad-except
+        flash(f'Problem deleting link "{link_uuid}": {e.details()}', 'danger')
+        current_app.logger.exception(e)
+    return redirect(url_for('link.home'))
+    '''
+    pass
diff --git a/src/webui/service/service/__init__.py b/src/webui/service/service/__init__.py
index 3ee6f7071f145e06c3aeaefc09a43ccd88e619e3..5cf553eaaec41de7599b6723e31e4ca3f82cbcae 100644
--- a/src/webui/service/service/__init__.py
+++ b/src/webui/service/service/__init__.py
@@ -12,3 +12,4 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
+
diff --git a/src/webui/service/service/forms.py b/src/webui/service/service/forms.py
index f07acf54365b79245583e7f9567b8bc4a5cfd89d..dad15f1c2dbef3a5d1c9a3ecdc6f96c00b883aa2 100644
--- a/src/webui/service/service/forms.py
+++ b/src/webui/service/service/forms.py
@@ -17,6 +17,11 @@ from flask_wtf import FlaskForm
 from wtforms import StringField, SelectField, IntegerField, DecimalField
 from wtforms.validators import InputRequired, Optional, NumberRange, ValidationError, StopValidation
 
+# Custom uuid validator
+def validate_uuid_address(form, field):
+    if not re.match(r'^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$', field.data):
+        raise ValidationError('Invalid uuid format')
+
 # Custom IPv4 address validator
 def validate_ipv4_address(form, field):
     try:
@@ -60,7 +65,25 @@ class CustomInputRequired():
             raise StopValidation(self.message)
         
 class AddServiceForm_1(FlaskForm):
-    service_type = SelectField('Type of service', choices=[('', 'Select a type of service to add'), ('ACL_L2', 'ACL_L2'), ('ACL_IPV4', 'ACL_IPV4'), ('ACL_IPV6', 'ACL_IPV6'), ('L2VPN', 'L2VPN'), ('L3VPN', 'L3VPN')], validators=[InputRequired()])
+    service_type = SelectField('Type of service', choices=[('', 'Select a type of service to add'), ('ACL_L2', 'ACL_L2'), ('ACL_IPV4', 'ACL_IPV4'), ('ACL_IPV6', 'ACL_IPV6'), ('L2VPN', 'L2VPN'), ('L3VPN', 'L3VPN'), ('QKD', 'QKD')], validators=[InputRequired()])
+
+class AddServiceForm_QKD(FlaskForm):
+    #GENERIC SERVICE PARAMETERS (COMMON & MANDATORY)
+    service_name       = StringField('Service Name', validators=[CustomInputRequired()])
+    service_type       = SelectField('Service Type', choices=[(6, '6 (QKD)')], validators=[CustomInputRequired()])
+    service_device_1   = SelectField('Device_1', choices=[('', 'Select a device (Mandatory)')], validators=[CustomInputRequired()])
+    service_device_2   = SelectField('Device_2', choices=[('', 'Select a device (Mandatory)')], validators=[CustomInputRequired()])
+    service_endpoint_1 = StringField('Device_1 Endpoint', validators=[CustomInputRequired()])
+    service_endpoint_2 = StringField('Device_2 Endpoint', validators=[CustomInputRequired()])
+    
+    #GENERIC SERVICE CONSTRAINT PARAMETERS (ALL OPTIONAL)
+    service_capacity    = DecimalField('Service Capacity', places=2, default=10.00, validators=[Optional(), NumberRange(min=0)])
+    service_latency     = DecimalField('Service Latency', places=2, default=15.20, validators=[Optional(), NumberRange(min=0)])
+    service_availability= DecimalField('Service Availability', places=2, validators=[Optional(), NumberRange(min=0)])
+    service_isolation   = SelectField('Service Isolation', choices=[('', 'Select (Optional)'), ('NO_ISOLATION', 'NO_ISOLATION'), ('PHYSICAL_ISOLATION', 'PHYSICAL_ISOLATION'), 
+                                                                    ('LOGICAL_ISOLATION', 'LOGICAL_ISOLATION'), ('PROCESS_ISOLATION', 'PROCESS_ISOLATION'), ('PHYSICAL_MEMORY_ISOLATION', 'PHYSICAL_MEMORY_ISOLATION'), 
+                                                                    ('PHYSICAL_NETWORK_ISOLATION', 'PHYSICAL_NETWORK_ISOLATION'), ('VIRTUAL_RESOURCE_ISOLATION', 'VIRTUAL_RESOURCE_ISOLATION'), 
+                                                                    ('NETWORK_FUNCTIONS_ISOLATION', 'NETWORK_FUNCTIONS_ISOLATION'), ('SERVICE_ISOLATION', 'SERVICE_ISOLATION')], validators=[Optional()])
 
 class AddServiceForm_ACL_L2(FlaskForm):
     #GENERIC SERVICE PARAMETERS (COMMON & MANDATORY)
@@ -259,3 +282,4 @@ class AddServiceForm_L3VPN(FlaskForm):
     Device_2_IF_address_ip  = StringField('Device_2 IP Address', validators=[CustomInputRequired(), validate_ipv4_address])
     Device_2_IF_address_prefix = IntegerField('Device_2 IP Prefix length', validators=[CustomInputRequired(), validate_uint32])
     Device_2_IF_description = StringField ('Device_2 SubIF Description', validators=[Optional()])
+    
diff --git a/src/webui/service/service/routes.py b/src/webui/service/service/routes.py
index 92025b2bec4f7c70b446a8c422e2cdb166c95466..c164b41773e15ac4e9746753e1fdc3b56a51b0d2 100644
--- a/src/webui/service/service/routes.py
+++ b/src/webui/service/service/routes.py
@@ -35,14 +35,14 @@ from common.tools.object_factory.Constraint import (
 from common.tools.object_factory.Context import json_context_id
 from common.tools.object_factory.Device import json_device_id
 from common.tools.object_factory.EndPoint import json_endpoint_id
-from common.tools.object_factory.Service import json_service_l2nm_planned, json_service_l3nm_planned
+from common.tools.object_factory.Service import json_service_l2nm_planned, json_service_l3nm_planned, json_service_qkd_planned
 from common.tools.object_factory.Topology import json_topology_id
 from context.client.ContextClient import ContextClient
 from device.client.DeviceClient import DeviceClient
 from service.client.ServiceClient import ServiceClient
 from webui.service.service.forms import (
     AddServiceForm_1, AddServiceForm_ACL_L2, AddServiceForm_ACL_IPV4, AddServiceForm_ACL_IPV6,
-    AddServiceForm_L2VPN, AddServiceForm_L3VPN
+    AddServiceForm_L2VPN, AddServiceForm_L3VPN, AddServiceForm_QKD
 )
 
 LOGGER = logging.getLogger(__name__)
@@ -329,10 +329,83 @@ def add_configure():
     form_1 = AddServiceForm_1()
     if form_1.validate_on_submit():
         service_type = str(form_1.service_type.data)
-        if service_type in {'ACL_L2', 'ACL_IPV4', 'ACL_IPV6', 'L2VPN', 'L3VPN'}:
+        if service_type in {'ACL_L2', 'ACL_IPV4', 'ACL_IPV6', 'L2VPN', 'L3VPN', 'QKD'}:
             return redirect(url_for('service.add_configure_{:s}'.format(service_type)))
     return render_template('service/add.html', form_1=form_1, submit_text='Continue to configuraton')
 
+@service.route('add/configure/QKD', methods=['GET', 'POST'])
+def add_configure_QKD():
+    form_qkd = AddServiceForm_QKD()
+    service_obj = Service()
+
+    context_uuid, topology_uuid = get_context_and_topology_uuids()
+    if context_uuid and topology_uuid:
+        context_client.connect()
+        grpc_topology = get_topology(context_client, topology_uuid, context_uuid=context_uuid, rw_copy=False)
+        if grpc_topology:
+            topo_device_uuids = {device_id.device_uuid.uuid for device_id in grpc_topology.device_ids}          
+            devices = get_filtered_devices(context_client, topo_device_uuids)
+            grpc_devices = context_client.ListDevices(Empty())                                          
+            devices = [
+                device for device in grpc_devices.devices
+                if device.device_id.device_uuid.uuid in topo_device_uuids and DeviceDriverEnum.DEVICEDRIVER_QKD in device.device_drivers
+            ]
+            choices = get_device_choices(devices)
+            add_device_choices_to_form(choices, form_qkd.service_device_1)
+            add_device_choices_to_form(choices, form_qkd.service_device_2)
+        else:
+            flash('Context({:s})/Topology({:s}) not found'.format(str(context_uuid), str(topology_uuid)), 'danger')
+    else:
+        flash('Missing context or topology UUID', 'danger')
+
+    if form_qkd.validate_on_submit():
+        try:
+            [selected_device_1, selected_device_2, selected_endpoint_1, selected_endpoint_2] = validate_selected_devices_and_endpoints(form_qkd, devices)
+        except Exception as e:
+            flash('{:s}'.format(str(e.args[0])), 'danger')
+            current_app.logger.exception(e)
+            return render_template('service/configure_QKD.html', form_qkd=form_qkd, submit_text='Add New Service')
+        
+        service_uuid, service_type, endpoint_ids = set_service_parameters(service_obj, form_qkd, selected_device_1, selected_device_2, selected_endpoint_1, selected_endpoint_2)
+        constraints = add_constraints(form_qkd)
+        params_device_1_with_data = get_device_params(form_qkd, 1, service_type)
+        params_device_2_with_data = get_device_params(form_qkd, 2, service_type)
+        print(params_device_1_with_data)
+        print(params_device_2_with_data)
+        params_settings = {}
+        config_rules = [
+            json_config_rule_set(
+                    '/settings', params_settings
+                ),
+            json_config_rule_set(
+                '/device[{:s}]/endpoint[{:s}]/settings'.format(str(selected_device_1.name), str(selected_endpoint_1)), params_device_1_with_data
+            ),
+            json_config_rule_set(
+                '/device[{:s}]/endpoint[{:s}]/settings'.format(str(selected_device_2.name), str(selected_endpoint_2)), params_device_2_with_data
+            )
+        ]
+
+        service_client.connect()
+        context_client.connect()
+        device_client.connect()
+        descriptor_json = json_service_qkd_planned(service_uuid = service_uuid, endpoint_ids = endpoint_ids, constraints = constraints, config_rules = config_rules, context_uuid= context_uuid)
+        descriptor_json = {"services": [descriptor_json]}
+        try:
+            process_descriptors(descriptor_json)
+            flash('Service "{:s}" added successfully!'.format(service_obj.service_id.service_uuid.uuid), 'success')
+            return redirect(url_for('service.home', service_uuid=service_obj.service_id.service_uuid.uuid))
+        except Exception as e:
+            flash('Problem adding service: {:s}'.format((str(e.args[0]))), 'danger')
+            current_app.logger.exception(e)
+        finally:
+            context_client.close()                                                                                      
+            device_client.close()
+            service_client.close()
+
+    
+    return render_template('service/configure_QKD.html', form_qkd=form_qkd, submit_text='Add New Service')
+
+
 @service.route('add/configure/ACL_L2', methods=['GET', 'POST'])
 def add_configure_ACL_L2():
     form_acl = AddServiceForm_ACL_L2()
@@ -666,6 +739,9 @@ def get_device_params(form, device_num, form_type):
             'ni_description': str(getattr(form, 'NI_description').data),
             'subif_description': str(getattr(form, f'Device_{device_num}_IF_description').data),
         }
+    elif form_type == 6:
+        device_params = {
+        }
     else:
         raise ValueError(f'Unsupported form type: {form_type}')
 
diff --git a/src/webui/service/static/topology_icons/emu-qkd-node.png b/src/webui/service/static/topology_icons/emu-qkd-node.png
new file mode 100644
index 0000000000000000000000000000000000000000..d4dc1abaf42a56ff07d1f4a2c5d250b56486584d
Binary files /dev/null and b/src/webui/service/static/topology_icons/emu-qkd-node.png differ
diff --git a/src/webui/service/static/topology_icons/qkd-node.png b/src/webui/service/static/topology_icons/qkd-node.png
new file mode 100644
index 0000000000000000000000000000000000000000..79f40d2a600bd7f9e55d0360a132800c09a8ac85
Binary files /dev/null and b/src/webui/service/static/topology_icons/qkd-node.png differ
diff --git a/src/webui/service/templates/base.html b/src/webui/service/templates/base.html
index c154346204a4ad59eec54a7e9ae3956a7f3db655..f59e534bdd94b31b246051333e60d380a3e3bc91 100644
--- a/src/webui/service/templates/base.html
+++ b/src/webui/service/templates/base.html
@@ -103,6 +103,13 @@
                   <a class="nav-link" href="{{ url_for('load_gen.home') }}">Load Generator</a>
                   {% endif %}
                 </li>
+                <li class="nav-item">
+                  {% if '/qkd_app/' in request.path %}
+                  <a class="nav-link active" aria-current="page" href="{{ url_for('app.home') }}">App</a>
+                  {% else %}
+                  <a class="nav-link" href="{{ url_for('app.home') }}">App</a>
+                  {% endif %}
+                </li> 
                 <li class="nav-item">
                   {% if '/bgpls/' in request.path %}
                   <a class="nav-link active" aria-current="page" href="{{ url_for('bgpls.home') }}">BGPLS</a>
diff --git a/src/webui/service/templates/device/add.html b/src/webui/service/templates/device/add.html
index 3bea6ae719a75c91835ceb35f50b5bbeba2c7940..e11c37688c09b96849c63a5d51cd7e546468d558 100644
--- a/src/webui/service/templates/device/add.html
+++ b/src/webui/service/templates/device/add.html
@@ -95,6 +95,7 @@
                 <br />
                 {{ form.device_drivers_optical_tfs }} {{ form.device_drivers_optical_tfs.label(class="col-sm-3 col-form-label") }}
                 {{ form.device_drivers_ietf_actn }} {{ form.device_drivers_ietf_actn.label(class="col-sm-3 col-form-label") }}
+                {{ form.device_drivers_qkd }} {{ form.device_drivers_qkd.label(class="col-sm-3 col-form-label") }}
                 {% endif %}
             </div>
         </div>
diff --git a/src/webui/service/templates/qkd_app/home.html b/src/webui/service/templates/qkd_app/home.html
new file mode 100644
index 0000000000000000000000000000000000000000..e138acd1463202b64114921d70f1df3e3716d40a
--- /dev/null
+++ b/src/webui/service/templates/qkd_app/home.html
@@ -0,0 +1,94 @@
+<!--
+ Copyright 2022-2024 ETSI OSG/SDG TeraFlowSDN (TFS) (https://tfs.etsi.org/)
+
+ 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.
+-->
+ 
+{% extends 'base.html' %}
+
+{% block content %}
+    <h1>Apps</h1>
+
+    <div class="row">
+        <div class="col">
+            {{ apps | length }} apps found in context <i>{{ session['context_uuid'] }}</i>
+        </div>
+    </div>
+    
+    <table class="table table-striped table-hover">
+        <thead>
+          <tr>
+            <th scope="col">UUID</th>
+            <th scope="col">Status</th>
+            <th scope="col">Type</th>
+            <th scope="col">Device 1</th>
+            <th scope="col">Device 2</th>
+            <th scope="col"></th>
+          </tr>
+        </thead>
+        <tbody>
+            {% if apps %}
+                {% for app in apps %}
+                <tr>
+                    <td>
+                            {{ app.app_id.app_uuid.uuid }}
+                    </td>
+                    <td>
+                        {{ ase.Name(app.app_status).replace('QKDAPPSTATUS_', '') }}
+                    </td>
+                    <td>
+                        {{ ate.Name(app.app_type).replace('QKDAPPTYPES_', '').replace('CLIENT', 'EXTERNAL')  }}
+                    </td>
+                    <td>
+                        <li>
+                            <a href="{{ url_for('device.detail', device_uuid=app.local_device_id.device_uuid.uuid) }}">
+                                {{ device_names.get(app.local_device_id.device_uuid.uuid, app.local_device_id.device_uuid.uuid) }}
+                                <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-eye" viewBox="0 0 16 16">
+                                    <path d="M16 8s-3-5.5-8-5.5S0 8 0 8s3 5.5 8 5.5S16 8 16 8zM1.173 8a13.133 13.133 0 0 1 1.66-2.043C4.12 4.668 5.88 3.5 8 3.5c2.12 0 3.879 1.168 5.168 2.457A13.133 13.133 0 0 1 14.828 8c-.058.087-.122.183-.195.288-.335.48-.83 1.12-1.465 1.755C11.879 11.332 10.119 12.5 8 12.5c-2.12 0-3.879-1.168-5.168-2.457A13.134 13.134 0 0 1 1.172 8z"/>
+                                    <path d="M8 5.5a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5zM4.5 8a3.5 3.5 0 1 1 7 0 3.5 3.5 0 0 1-7 0z"/>
+                                </svg>
+                            </a>
+                        </li>
+                    </td>
+                    <td>
+                        {% if app.remote_device_id.device_uuid.uuid %}   
+                            <li>       
+                                <a href="{{ url_for('device.detail', device_uuid=app.remote_device_id.device_uuid.uuid) }}">
+                                    {{ device_names.get(app.remote_device_id.device_uuid.uuid, app.remote_device_id.device_uuid.uuid) }}
+                                    <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-eye" viewBox="0 0 16 16">
+                                        <path d="M16 8s-3-5.5-8-5.5S0 8 0 8s3 5.5 8 5.5S16 8 16 8zM1.173 8a13.133 13.133 0 0 1 1.66-2.043C4.12 4.668 5.88 3.5 8 3.5c2.12 0 3.879 1.168 5.168 2.457A13.133 13.133 0 0 1 14.828 8c-.058.087-.122.183-.195.288-.335.48-.83 1.12-1.465 1.755C11.879 11.332 10.119 12.5 8 12.5c-2.12 0-3.879-1.168-5.168-2.457A13.134 13.134 0 0 1 1.172 8z"/>
+                                        <path d="M8 5.5a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5zM4.5 8a3.5 3.5 0 1 1 7 0 3.5 3.5 0 0 1-7 0z"/>
+                                    </svg>
+                                </a>
+                            </li>
+                        {% endif %}
+                    </td>
+                    <td>
+                        <a href="{{ url_for('app.detail', app_uuid=app.app_id.app_uuid.uuid) }}">
+                            <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-eye" viewBox="0 0 16 16">
+                                <path d="M16 8s-3-5.5-8-5.5S0 8 0 8s3 5.5 8 5.5S16 8 16 8zM1.173 8a13.133 13.133 0 0 1 1.66-2.043C4.12 4.668 5.88 3.5 8 3.5c2.12 0 3.879 1.168 5.168 2.457A13.133 13.133 0 0 1 14.828 8c-.058.087-.122.183-.195.288-.335.48-.83 1.12-1.465 1.755C11.879 11.332 10.119 12.5 8 12.5c-2.12 0-3.879-1.168-5.168-2.457A13.134 13.134 0 0 1 1.172 8z"/>
+                                <path d="M8 5.5a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5zM4.5 8a3.5 3.5 0 1 1 7 0 3.5 3.5 0 0 1-7 0z"/>
+                            </svg>
+                        </a>
+                    </td>
+                </tr>
+                {% endfor %}
+            {% else %}
+                <tr>
+                    <td colspan="7">No apps found</td>
+                </tr>
+            {% endif %}
+        </tbody>
+    </table>
+
+{% endblock %}
diff --git a/src/webui/service/templates/service/configure_QKD.html b/src/webui/service/templates/service/configure_QKD.html
new file mode 100644
index 0000000000000000000000000000000000000000..a01f4519d7f4b09732fba6d24db034f143be9943
--- /dev/null
+++ b/src/webui/service/templates/service/configure_QKD.html
@@ -0,0 +1,188 @@
+<!--
+ Copyright 2022-2024 ETSI OSG/SDG TeraFlowSDN (TFS) (https://tfs.etsi.org/) 
+
+ 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.
+-->
+
+{% extends 'base.html' %}
+
+{% block content %}
+<h1>Add New Service [QKD]</h1>
+<form method="POST" action="{{ url_for('service.add_configure_QKD') }}">
+    <fieldset>
+        <div class="row mb-3">
+            {{ form_qkd.hidden_tag() }}
+        </div> 
+        <h3>Generic Service Parameters</h3>
+        {% if form_qkd.qkd_params is not none %}
+        <div class="row mb-3">
+            {{ form_qkd.service_name.label(class="col-sm-2 col-form-label") }}
+            <div class="col-sm-10">
+                {% if form_qkd.service_name.errors %}
+                {{ form_qkd.service_name(class="form-control is-invalid", placeholder="Mandatory") }}
+                <div class="invalid-feedback">
+                    {% for error in form_qkd.service_name.errors %}
+                    <span>{{ error }}</span>
+                    {% endfor %}
+                </div>
+                {% else %}
+                {{ form_qkd.service_name(class="form-control", placeholder="Mandatory") }}
+                {% endif %}
+            </div>
+        </div>
+        <div class="row mb-3">
+            {{ form_qkd.service_type.label(class="col-sm-2 col-form-label") }}
+            <div class="col-sm-10">
+                {% if form_qkd.service_type.errors %}
+                {{ form_qkd.service_type(class="form-control is-invalid", placeholder="Mandatory") }}
+                <div class="invalid-feedback">
+                    {% for error in form_qkd.service_type.errors %}
+                    <span>{{ error }}</span>
+                    {% endfor %}
+                </div>
+                {% else %}
+                {{ form_qkd.service_type(class="form-control", placeholder="Mandatory") }}
+                {% endif %}
+            </div>
+        </div>
+        <div class="row mb-3">
+            {{ form_qkd.service_device_1.label(class="col-sm-2 col-form-label") }}
+            <div class="col-sm-4">
+                {% if form_qkd.service_device_1.errors %}
+                {{ form_qkd.service_device_1(class="form-control is-invalid", placeholder="Mandatory") }}
+                <div class="invalid-feedback">
+                    {% for error in form_qkd.service_device_1.errors %}
+                    <span>{{ error }}</span>
+                    {% endfor %}
+                </div>
+                {% else %}
+                {{ form_qkd.service_device_1(class="form-control", placeholder="Mandatory") }}
+                {% endif %}
+            </div>
+            {{ form_qkd.service_device_2.label(class="col-sm-2 col-form-label") }}
+            <div class="col-sm-4">
+                {% if form_qkd.service_device_2.errors %}
+                {{ form_qkd.service_device_2(class="form-control is-invalid", placeholder="Mandatory") }}
+                <div class="invalid-feedback">
+                    {% for error in form_qkd.service_device_2.errors %}
+                    <span>{{ error }}</span>
+                    {% endfor %}
+                </div>
+                {% else %}
+                {{ form_qkd.service_device_2(class="form-control", placeholder="Mandatory") }}
+                {% endif %}
+            </div>
+        </div>
+        <div class="row mb-3">
+            {{ form_qkd.service_endpoint_1.label(class="col-sm-2 col-form-label") }}
+            <div class="col-sm-4">
+                {% if form_qkd.service_endpoint_1.errors %}
+                {{ form_qkd.service_endpoint_1(class="form-control is-invalid", placeholder="Mandatory") }}
+                <div class="invalid-feedback">
+                    {% for error in form_qkd.service_endpoint_1.errors %}
+                    <span>{{ error }}</span>
+                    {% endfor %}
+                </div>
+                {% else %}
+                {{ form_qkd.service_endpoint_1(class="form-control", placeholder="Mandatory") }}
+                {% endif %}
+            </div>
+            {{ form_qkd.service_endpoint_2.label(class="col-sm-2 col-form-label") }}
+            <div class="col-sm-4">
+                {% if form_qkd.service_endpoint_2.errors %}
+                {{ form_qkd.service_endpoint_2(class="form-control is-invalid", placeholder="Mandatory") }}
+                <div class="invalid-feedback">
+                    {% for error in form_qkd.service_endpoint_2.errors %}
+                    <span>{{ error }}</span>
+                    {% endfor %}
+                </div>
+                {% else %}
+                {{ form_qkd.service_endpoint_2(class="form-control", placeholder="Mandatory") }}
+                {% endif %}
+            </div>
+        </div>
+        </br>
+        <h3>Generic Service Constraints</h3>
+        <div class="row mb-3">
+            {{ form_qkd.service_capacity.label(class="col-sm-2 col-form-label") }}
+            <div class="col-sm-10">
+                {% if form_qkd.service_capacity.errors %}
+                {{ form_qkd.service_capacity(class="form-control is-invalid") }}
+                <div class="invalid-feedback">
+                    {% for error in form_qkd.service_capacity.errors %}
+                    <span>{{ error }}</span>
+                    {% endfor %}
+                </div>
+                {% else %}
+                {{ form_qkd.service_capacity(class="form-control") }}
+                {% endif %}
+            </div>
+        </div>
+        <div class="row mb-3">
+            {{ form_qkd.service_latency.label(class="col-sm-2 col-form-label") }}
+            <div class="col-sm-10">
+                {% if form_qkd.service_latency.errors %}
+                {{ form_qkd.service_latency(class="form-control is-invalid") }}
+                <div class="invalid-feedback">
+                    {% for error in form_qkd.service_latency.errors %}
+                    <span>{{ error }}</span>
+                    {% endfor %}
+                </div>
+                {% else %}
+                {{ form_qkd.service_latency(class="form-control") }}
+                {% endif %}
+            </div>
+        </div>
+        <div class="row mb-3">
+            {{ form_qkd.service_availability.label(class="col-sm-2 col-form-label") }}
+            <div class="col-sm-10">
+                {% if form_qkd.service_availability.errors %}
+                {{ form_qkd.service_availability(class="form-control is-invalid") }}
+                <div class="invalid-feedback">
+                    {% for error in form_qkd.service_availability.errors %}
+                    <span>{{ error }}</span>
+                    {% endfor %}
+                </div>
+                {% else %}
+                {{ form_qkd.service_availability(class="form-control") }}
+                {% endif %}
+            </div>
+        </div>
+        <div class="row mb-3">
+            {{ form_qkd.service_isolation.label(class="col-sm-2 col-form-label") }}
+            <div class="col-sm-10">
+                {% if form_qkd.service_isolation.errors %}
+                {{ form_qkd.service_isolation(class="form-control is-invalid") }}
+                <div class="invalid-feedback">
+                    {% for error in form_qkd.service_isolation.errors %}
+                    <span>{{ error }}</span>
+                    {% endfor %}
+                </div>
+                {% else %}
+                {{ form_qkd.service_isolation(class="form-control") }}
+                {% endif %}
+            </div>
+        </div>
+        {% endif %}
+        <button type="submit" class="btn btn-primary">
+            <i class="bi bi-plus-circle-fill"></i>
+            {{ submit_text }}
+        </button>
+        <button type="button" class="btn btn-block btn-secondary" onclick="javascript: history.back()">
+            <i class="bi bi-box-arrow-in-left"></i>
+            Cancel
+        </button>
+    </fieldset>
+    </form>
+    {% endblock %}
+