diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index cb660f5401c3d375e88badc16c7e255dca6d3b01..447a9728f58d483f834e24ecc08b86e11f6a3040 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -137,7 +137,7 @@ publish-dev-docker-container-job:
   dependencies: []
   only:
     refs:
-      - development
+      - main
   before_script:
     - echo "{\"auths\":{\"${CI_REGISTRY}\":{\"auth\":\"$(printf "%s:%s" "${CI_REGISTRY_USER}" "${CI_REGISTRY_PASSWORD}" | base64 | tr -d '\n')\"},\"$CI_DEPENDENCY_PROXY_SERVER\":{\"auth\":\"$(printf "%s:%s" ${CI_DEPENDENCY_PROXY_USER} "${CI_DEPENDENCY_PROXY_PASSWORD}" | base64 | tr -d '\n')\"}}}" > /kaniko/.docker/config.json
   script:
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index fbf6789224c87f8ec3f3e9684c1fa991e437f390..51be2846e3cf0c92f204586eca2e1ae2aec9202e 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -15,7 +15,7 @@ repos:
     -   id: check-merge-conflict
     -   id: check-ast
 -   repo: https://github.com/psf/black
-    rev: 23.11.0
+    rev: 23.12.0
     hooks:
     -   id: black
         files: app
@@ -25,7 +25,7 @@ repos:
     hooks:
     -   id: ruff
 -   repo: https://github.com/PyCQA/isort
-    rev: 5.12.0
+    rev: 5.13.1
     hooks:
     -   id: isort
         files: app
diff --git a/app/api/dependencies.py b/app/api/dependencies.py
index 4bf42b800da560e6fb3219c2f3eae1e749f106f3..0b491edef86419832e372889f9f1db10300e6ba7 100644
--- a/app/api/dependencies.py
+++ b/app/api/dependencies.py
@@ -1,4 +1,4 @@
-from typing import TYPE_CHECKING, Annotated, AsyncIterator, Awaitable, Callable, Dict
+from typing import TYPE_CHECKING, Annotated, AsyncIterator, Awaitable, Callable, Dict, List
 from uuid import UUID
 
 from authlib.jose.errors import BadSignatureError, DecodeError, ExpiredTokenError
@@ -13,7 +13,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
 
 from app.core.config import settings
 from app.core.security import decode_token, request_authorization
-from app.crud import CRUDUser
+from app.crud import CRUDResource, CRUDUser
 from app.schemas.security import JWT, AuthzRequest, AuthzResponse
 from app.slurm.rest_client import SlurmClient
 from app.utils.otlp import start_as_current_span_async
@@ -28,7 +28,7 @@ bearer_token = HTTPBearer(description="JWT Header")
 tracer = trace.get_tracer_provider().get_tracer(__name__)
 
 
-async def get_s3_resource(request: Request) -> S3ServiceResource:
+async def get_s3_resource(request: Request) -> S3ServiceResource:  # pragma: no cover
     return request.app.s3_resource
 
 
@@ -205,17 +205,16 @@ async def get_current_resource(rid: Annotated[UUID, Path()], db: DBSession) -> R
     resource : clowmdb.models.Resource
         Resource associated with the ID in the path.
     """
-    return Resource(
-        name="name", _resource_id=rid.bytes, short_description="a" * 32, source="a" * 8, maintainer_id="abc"
-    )
+    resource = await CRUDResource.get(db, resource_id=rid)
+    if resource:
+        return resource
+    raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Resource not found")
 
 
 CurrentResource = Annotated[Resource, Depends(get_current_resource)]
 
 
-async def get_current_resource_version(
-    rvid: Annotated[UUID, Path()], resource: CurrentResource, db: DBSession
-) -> ResourceVersion:
+async def get_current_resource_version(rvid: Annotated[UUID, Path()], resource: CurrentResource) -> ResourceVersion:
     """
     Get the current resource version from the database based on the ID in the path.
 
@@ -227,20 +226,18 @@ async def get_current_resource_version(
         ID of a resource version. Path parameter.
     resource : clowmdb.models.Resource
         Resource associated with the ID in the path.
-    db : sqlalchemy.ext.asyncio.AsyncSession.
-        Async database session to perform query on. Dependency Injection.
 
     Returns
     -------
     resource_version : clowmdb.models.ResourceVersion
         Resource Version associated with the ID in the path.
     """
-    return ResourceVersion(
-        _resource_version_id=rvid.bytes,
-        _resource_id=resource.resource_id.bytes,
-        release="1.0.0",
-        status=ResourceVersion.Status.RESOURCE_REQUESTED,
-    )
+    resource_version: List[ResourceVersion] = [
+        version for version in resource.versions if version.resource_version_id == rvid
+    ]
+    if len(resource_version) > 0:
+        return resource_version[0]
+    raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Resource version not found")
 
 
 CurrentResourceVersion = Annotated[ResourceVersion, Depends(get_current_resource_version)]
diff --git a/app/api/endpoints/resource_version.py b/app/api/endpoints/resource_version.py
index 8adc44253b034bbe593dfad2fd8b90a21e2d58f0..3db5b21614020acd13e38ed9488f6194aace81ce 100644
--- a/app/api/endpoints/resource_version.py
+++ b/app/api/endpoints/resource_version.py
@@ -1,11 +1,17 @@
 from typing import Annotated, Any, Awaitable, Callable, List, Optional
-from uuid import uuid4
 
 from clowmdb.models import ResourceVersion
 from fastapi import APIRouter, Depends, Query, status
 from opentelemetry import trace
 
-from app.api.dependencies import AuthorizationDependency, CurrentResource, CurrentResourceVersion, CurrentUser
+from app.api.dependencies import (
+    AuthorizationDependency,
+    CurrentResource,
+    CurrentResourceVersion,
+    CurrentUser,
+    DBSession,
+)
+from app.crud import CRUDResourceVersion
 from app.schemas.resource_version import ResourceVersionIn, ResourceVersionOut
 from app.utils.otlp import start_as_current_span_async
 
@@ -53,8 +59,14 @@ async def list_resource_versions(
         "list_filter" if resource.maintainer_id != current_user.uid and version_status is not None else "list"
     )
     await authorization(rbac_operation)
-
-    return []
+    requested_versions = (
+        version_status if version_status else [ResourceVersion.Status.LATEST, ResourceVersion.Status.SYNCHRONIZED]
+    )
+    return [
+        ResourceVersionOut.from_db_resource_version(version)
+        for version in resource.versions
+        if version.status in requested_versions
+    ]
 
 
 @router.post("", summary="Request new version of a resource", status_code=status.HTTP_201_CREATED)
@@ -62,8 +74,9 @@ async def list_resource_versions(
 async def request_resource_version(
     authorization: Authorization,
     resource: CurrentResource,
-    resource_version: ResourceVersionIn,
+    resource_version_in: ResourceVersionIn,
     current_user: CurrentUser,
+    db: DBSession,
 ) -> ResourceVersionOut:
     """
     Request a new resource version.
@@ -76,26 +89,25 @@ async def request_resource_version(
         Async function to ask the auth service for authorization. Dependency Injection.
     resource : clowmdb.models.Resource
         Resource associated with the ID in the path. Dependency Injection.
-    resource_version : app.schemas.resource_version.ResourceVersionIn
+    resource_version_in : app.schemas.resource_version.ResourceVersionIn
         Data about the new resource version. HTTP Body.
     current_user : clowmdb.models.User
         Current user. Dependency injection.
+    db : sqlalchemy.ext.asyncio.AsyncSession.
+        Async database session to perform query on. Dependency Injection.
     """
     current_span = trace.get_current_span()
     current_span.set_attributes(
-        {"resource_id": str(resource.resource_id), "resource_version_in": resource_version.model_dump_json(indent=2)}
+        {"resource_id": str(resource.resource_id), "resource_version_in": resource_version_in.model_dump_json(indent=2)}
     )
     rbac_operation = "update"
     if current_user.uid != resource.maintainer_id:
         rbac_operation = "update_any"
     await authorization(rbac_operation)
-    return ResourceVersionOut(
-        release="1.0.0",
-        status=ResourceVersion.Status.RESOURCE_REQUESTED,
-        resource_version_id=uuid4(),
-        resource_id=resource.resource_id,
-        created_at=0,
+    resource_version = await CRUDResourceVersion.create(
+        db, resource_id=resource.resource_id, release=resource_version_in.release
     )
+    return ResourceVersionOut.from_db_resource_version(resource_version)
 
 
 @router.get("/{rvid}", summary="Get version of a resource")
@@ -134,13 +146,7 @@ async def get_resource_version(
     ]:
         rbac_operation = "read_any"
     await authorization(rbac_operation)
-    return ResourceVersionOut(
-        release="1.0.0",
-        status=ResourceVersion.Status.RESOURCE_REQUESTED,
-        resource_version_id=resource_version.resource_version_id,
-        resource_id=resource.resource_id,
-        created_at=0,
-    )
+    return ResourceVersionOut.from_db_resource_version(resource_version)
 
 
 @router.put("/{rvid}/request_sync", summary="Request resource version synchronization")
@@ -150,6 +156,7 @@ async def request_resource_version_sync(
     resource: CurrentResource,
     resource_version: CurrentResourceVersion,
     current_user: CurrentUser,
+    db: DBSession,
 ) -> ResourceVersionOut:
     """
     Request the synchronization of a resource version to the cluster.
@@ -166,6 +173,8 @@ async def request_resource_version_sync(
         Current user. Dependency injection.
     resource_version : clowmdb.models.ResourceVersion
         Resource Version associated with the ID in the path. Dependency Injection.
+    db : sqlalchemy.ext.asyncio.AsyncSession.
+        Async database session to perform query on. Dependency Injection.
     """
     trace.get_current_span().set_attributes(
         {"resource_id": str(resource.resource_id), "resource_version_id": str(resource_version.resource_version_id)}
@@ -174,19 +183,20 @@ async def request_resource_version_sync(
     if current_user.uid != resource.maintainer_id:
         rbac_operation = "request_sync_any"
     await authorization(rbac_operation)
-    return ResourceVersionOut(
-        release="1.0.0",
-        status=ResourceVersion.Status.SYNC_REQUESTED,
-        resource_version_id=resource_version.resource_version_id,
-        resource_id=resource.resource_id,
-        created_at=0,
+    await CRUDResourceVersion.update_status(
+        db, resource_version_id=resource_version.resource_version_id, status=ResourceVersion.Status.SYNC_REQUESTED
     )
+    resource_version.status = ResourceVersion.Status.SYNC_REQUESTED
+    return ResourceVersionOut.from_db_resource_version(resource_version)
 
 
 @router.put("/{rvid}/sync", summary="Synchronize resource version with cluster")
 @start_as_current_span_async("api_resource_version_sync", tracer=tracer)
 async def resource_version_sync(
-    authorization: Authorization, resource: CurrentResource, resource_version: CurrentResourceVersion
+    authorization: Authorization,
+    resource: CurrentResource,
+    resource_version: CurrentResourceVersion,
+    db: DBSession,
 ) -> ResourceVersionOut:
     """
     Synchronize the resource version to the cluster.
@@ -201,18 +211,18 @@ async def resource_version_sync(
         Resource associated with the ID in the path. Dependency Injection.
     resource_version : clowmdb.models.ResourceVersion
         Resource Version associated with the ID in the path. Dependency Injection.
+    db : sqlalchemy.ext.asyncio.AsyncSession.
+        Async database session to perform query on. Dependency Injection.
     """
     trace.get_current_span().set_attributes(
         {"resource_id": str(resource.resource_id), "resource_version_id": str(resource_version.resource_version_id)}
     )
     await authorization("sync")
-    return ResourceVersionOut(
-        release="1.0.0",
-        status=ResourceVersion.Status.SYNCHRONIZING,
-        resource_version_id=resource_version.resource_version_id,
-        resource_id=resource.resource_id,
-        created_at=0,
+    await CRUDResourceVersion.update_status(
+        db, resource_version_id=resource_version.resource_version_id, status=ResourceVersion.Status.SYNCHRONIZING
     )
+    resource_version.status = ResourceVersion.Status.SYNCHRONIZING
+    return ResourceVersionOut.from_db_resource_version(resource_version)
 
 
 @router.put("/{rvid}/latest", summary="Set resource version to latest")
@@ -238,19 +248,16 @@ async def resource_version_latest(
         {"resource_id": str(resource.resource_id), "resource_version_id": str(resource_version.resource_version_id)}
     )
     await authorization("set_latest")
-    return ResourceVersionOut(
-        release="1.0.0",
-        status=ResourceVersion.Status.LATEST,
-        resource_version_id=resource_version.resource_version_id,
-        resource_id=resource.resource_id,
-        created_at=0,
-    )
+    return resource_version
 
 
 @router.delete("/{rvid}/cluster", summary="Delete resource version on cluster")
 @start_as_current_span_async("api_resource_version_delete_cluster", tracer=tracer)
 async def delete_resource_version_cluster(
-    authorization: Authorization, resource: CurrentResource, resource_version: CurrentResourceVersion
+    authorization: Authorization,
+    resource: CurrentResource,
+    resource_version: CurrentResourceVersion,
+    db: DBSession,
 ) -> ResourceVersionOut:
     """
     Delete the resource version on the cluster.
@@ -265,24 +272,27 @@ async def delete_resource_version_cluster(
         Resource associated with the ID in the path. Dependency Injection.
     resource_version : clowmdb.models.ResourceVersion
         Resource Version associated with the ID in the path. Dependency Injection.
+    db : sqlalchemy.ext.asyncio.AsyncSession.
+        Async database session to perform query on. Dependency Injection.
     """
     trace.get_current_span().set_attributes(
         {"resource_id": str(resource.resource_id), "resource_version_id": str(resource_version.resource_version_id)}
     )
     await authorization("delete_cluster")
-    return ResourceVersionOut(
-        release="1.0.0",
-        status=ResourceVersion.Status.CLUSTER_DELETED,
-        resource_version_id=resource_version.resource_version_id,
-        resource_id=resource.resource_id,
-        created_at=0,
+    await CRUDResourceVersion.update_status(
+        db, resource_version_id=resource_version.resource_version_id, status=ResourceVersion.Status.CLUSTER_DELETED
     )
+    resource_version.status = ResourceVersion.Status.CLUSTER_DELETED
+    return ResourceVersionOut.from_db_resource_version(resource_version)
 
 
 @router.delete("/{rvid}/s3", summary="Delete resource version in S3")
 @start_as_current_span_async("api_resource_version_delete_cluster", tracer=tracer)
 async def delete_resource_version_s3(
-    authorization: Authorization, resource: CurrentResource, resource_version: CurrentResourceVersion
+    authorization: Authorization,
+    resource: CurrentResource,
+    resource_version: CurrentResourceVersion,
+    db: DBSession,
 ) -> ResourceVersionOut:
     """
     Delete the resource version in the S3 bucket.
@@ -297,15 +307,15 @@ async def delete_resource_version_s3(
         Resource associated with the ID in the path. Dependency Injection.
     resource_version : clowmdb.models.ResourceVersion
         Resource Version associated with the ID in the path. Dependency Injection.
+    db : sqlalchemy.ext.asyncio.AsyncSession.
+        Async database session to perform query on. Dependency Injection.
     """
     trace.get_current_span().set_attributes(
         {"resource_id": str(resource.resource_id), "resource_version_id": str(resource_version.resource_version_id)}
     )
-    await authorization("delete_s3")
-    return ResourceVersionOut(
-        release="1.0.0",
-        status=ResourceVersion.Status.S3_DELETED,
-        resource_version_id=resource_version.resource_version_id,
-        resource_id=resource.resource_id,
-        created_at=0,
+    await CRUDResourceVersion.update_status(
+        db, resource_version_id=resource_version.resource_version_id, status=ResourceVersion.Status.S3_DELETED
     )
+    await authorization("delete_s3")
+    resource_version.status = ResourceVersion.Status.S3_DELETED
+    return ResourceVersionOut.from_db_resource_version(resource_version)
diff --git a/app/api/endpoints/resources.py b/app/api/endpoints/resources.py
index c0d538001b36d87f552845c7042d5f9b05873272..c37dbc381f806d161e5625c2dad7977ba9cc4ba6 100644
--- a/app/api/endpoints/resources.py
+++ b/app/api/endpoints/resources.py
@@ -1,13 +1,12 @@
 from typing import Annotated, Any, Awaitable, Callable, List, Optional
-from uuid import uuid4
 
 from clowmdb.models import ResourceVersion
 from fastapi import APIRouter, Depends, Query, status
 from opentelemetry import trace
 
-from app.api.dependencies import AuthorizationDependency, CurrentResource, CurrentUser
+from app.api.dependencies import AuthorizationDependency, CurrentResource, CurrentUser, DBSession
+from app.crud import CRUDResource
 from app.schemas.resource import ResourceIn, ResourceOut
-from app.schemas.resource_version import ResourceVersionOut
 from app.utils.otlp import start_as_current_span_async
 
 router = APIRouter(prefix="/resources", tags=["Resource"])
@@ -21,6 +20,7 @@ tracer = trace.get_tracer_provider().get_tracer(__name__)
 async def list_resources(
     authorization: Authorization,
     current_user: CurrentUser,
+    db: DBSession,
     maintainer_id: Annotated[
         Optional[str],
         Query(
@@ -49,6 +49,8 @@ async def list_resources(
         Async function to ask the auth service for authorization. Dependency Injection.
     current_user : clowmdb.models.User
         Current user. Dependency injection.
+    db : sqlalchemy.ext.asyncio.AsyncSession.
+        Async database session to perform query on. Dependency Injection.
     maintainer_id : str | None, default None
         Filter resources by a maintainer. Query Parameter.
     version_status : List[clowmdb.models.ResourceVersion.Status] | None, default None
@@ -70,8 +72,15 @@ async def list_resources(
     elif version_status is not None and maintainer_id is None:
         rbac_operation = "list_filter"
     await authorization(rbac_operation)
-
-    return []
+    resources = await CRUDResource.list_resources(
+        db,
+        name_substring=name_substring,
+        maintainer_id=maintainer_id,
+        version_status=version_status
+        if version_status
+        else [ResourceVersion.Status.LATEST, ResourceVersion.Status.SYNCHRONIZED],
+    )
+    return [ResourceOut.from_db_resource(resource) for resource in resources]
 
 
 @router.post("", summary="Request a new resource", status_code=status.HTTP_201_CREATED)
@@ -80,6 +89,7 @@ async def request_resource(
     authorization: Authorization,
     current_user: CurrentUser,
     resource: ResourceIn,
+    db: DBSession,
 ) -> ResourceOut:
     """
     Request a new resources.
@@ -94,27 +104,14 @@ async def request_resource(
         Current user. Dependency injection.
     resource : app.schemas.resource.ResourceIn
         Data about the new resource. HTTP Body.
+    db : sqlalchemy.ext.asyncio.AsyncSession.
+        Async database session to perform query on. Dependency Injection.
     """
     current_span = trace.get_current_span()
     current_span.set_attribute("resource_in", resource.model_dump_json(indent=2))
     await authorization("create")
-    rid = uuid4()
-    return ResourceOut(
-        name=resource.name,
-        description=resource.description,
-        source=resource.source,
-        resource_id=rid,
-        maintainer_id=current_user.uid,
-        versions=[
-            ResourceVersionOut(
-                status=ResourceVersion.Status.RESOURCE_REQUESTED,
-                resource_version_id=uuid4(),
-                created_at=0,
-                resource_id=rid,
-                release="1.0.0",
-            )
-        ],
-    )
+    resource = await CRUDResource.create(db, resource_in=resource, maintainer_id=current_user.uid)
+    return ResourceOut.from_db_resource(db_resource=resource)
 
 
 @router.get("/{rid}", summary="Get a resource")
@@ -153,13 +150,11 @@ async def get_resource(
     current_span.set_attribute("resource_id", str(resource.resource_id))
     rbac_operation = "read_any" if resource.maintainer_id != current_user.uid and version_status is not None else "read"
     await authorization(rbac_operation)
-    return ResourceOut(
-        name="name",
-        description="a" * 16,
-        source="a" * 8,
-        resource_id=resource.resource_id,
-        maintainer_id=current_user.uid,
-        versions=[],
+    requested_versions = (
+        version_status if version_status else [ResourceVersion.Status.LATEST, ResourceVersion.Status.SYNCHRONIZED]
+    )
+    return ResourceOut.from_db_resource(
+        resource, versions=[version for version in resource.versions if version.status in requested_versions]
     )
 
 
@@ -168,6 +163,7 @@ async def get_resource(
 async def delete_resource(
     authorization: Authorization,
     resource: CurrentResource,
+    db: DBSession,
 ) -> None:
     """
     Delete a resources.
@@ -180,6 +176,9 @@ async def delete_resource(
         Async function to ask the auth service for authorization. Dependency Injection.
     resource : clowmdb.models.Resource
         Resource associated with the ID in the path. Dependency Injection.
+    db : sqlalchemy.ext.asyncio.AsyncSession.
+        Async database session to perform query on. Dependency Injection.
     """
     trace.get_current_span().set_attribute("resource_id", str(resource.resource_id))
     await authorization("delete")
+    await CRUDResource.delete(db, resource_id=resource.resource_id)
diff --git a/app/crud/__init__.py b/app/crud/__init__.py
index 9c4f7cf5f07b50b25921333249d76a71b6f0dba4..a7eee27026a1d35950a8df3ba67d52e533278034 100644
--- a/app/crud/__init__.py
+++ b/app/crud/__init__.py
@@ -1 +1,3 @@
+from .crud_resource import CRUDResource  # noqa: F401
+from .crud_resource_version import CRUDResourceVersion  # noqa: F401
 from .crud_user import CRUDUser  # noqa: F401
diff --git a/app/crud/crud_resource.py b/app/crud/crud_resource.py
new file mode 100644
index 0000000000000000000000000000000000000000..20a3b235d9da287c77feed67e9e3476f0683e469
--- /dev/null
+++ b/app/crud/crud_resource.py
@@ -0,0 +1,137 @@
+from typing import List, Optional
+from uuid import UUID
+
+from clowmdb.models import Resource, ResourceVersion
+from opentelemetry import trace
+from sqlalchemy import delete, or_, select
+from sqlalchemy.ext.asyncio import AsyncSession
+from sqlalchemy.orm import joinedload
+
+from app.crud.crud_resource_version import CRUDResourceVersion
+from app.schemas.resource import ResourceIn
+
+tracer = trace.get_tracer_provider().get_tracer(__name__)
+
+
+class CRUDResource:
+    @staticmethod
+    async def get(db: AsyncSession, resource_id: UUID) -> Optional[Resource]:
+        """
+        Get a resource by its ID.
+
+        Parameters
+        ----------
+        db : sqlalchemy.ext.asyncio.AsyncSession.
+            Async database session to perform query on.
+        resource_id : uuid.UUID
+            ID of a resource.
+
+        Returns
+        -------
+        resource : clowmdb.models.Resource | None
+            The resource with the given ID if it exists, None otherwise
+        """
+
+        stmt = select(Resource).where(Resource._resource_id == resource_id.bytes).options(joinedload(Resource.versions))
+        with tracer.start_as_current_span(
+            "db_get_resource", attributes={"resource_id": str(resource_id), "sql_query": str(stmt)}
+        ):
+            return await db.scalar(stmt)
+
+    @staticmethod
+    async def list_resources(
+        db: AsyncSession,
+        name_substring: Optional[str] = None,
+        maintainer_id: Optional[str] = None,
+        version_status: Optional[List[ResourceVersion.Status]] = None,
+    ) -> List[Resource]:
+        """
+        List all resources. Populates the version attribute of the resources.
+
+        Parameters
+        ----------
+        db : sqlalchemy.ext.asyncio.AsyncSession.
+            Async database session to perform query on.
+        name_substring : str | None, default None
+            Substring to filter for in the name of a resource.
+        maintainer_id : str | None, default None
+            Filter resources by maintainer.
+        version_status : List[clowmdb.models.ResourceVersion.Status] | None, default None
+            Filter versions of a resource based on the status. Removes resources that have no version after this filter.
+
+        Returns
+        -------
+        workflows : List[clowmdb.models.Resource]
+            List of resources.
+        """
+        with tracer.start_as_current_span("db_list_resources") as span:
+            stmt = select(Resource).options(joinedload(Resource.versions))
+            if name_substring is not None:
+                span.set_attribute("name_substring", name_substring)
+                stmt = stmt.where(Resource.name.contains(name_substring))
+            if maintainer_id is not None:
+                span.set_attribute("maintainer_id", maintainer_id)
+                stmt = stmt.where(Resource.maintainer_id == maintainer_id)
+            if version_status is not None and len(version_status) > 0:
+                span.set_attribute("status", [status.name for status in version_status])
+                stmt = stmt.options(
+                    joinedload(
+                        Resource.versions.and_(or_(*[ResourceVersion.status == status for status in version_status]))
+                    )
+                )
+            span.set_attribute("sql_query", str(stmt))
+            return [w for w in (await db.scalars(stmt)).unique().all() if len(w.versions) > 0]
+
+    @staticmethod
+    async def delete(db: AsyncSession, resource_id: UUID) -> None:
+        """
+        Delete a resource.
+
+        Parameters
+        ----------
+        db : sqlalchemy.ext.asyncio.AsyncSession.
+            Async database session to perform query on.
+        resource_id : uuid.UUID
+            ID of a resource
+        """
+        stmt = delete(Resource).where(Resource._resource_id == resource_id.bytes)
+        with tracer.start_as_current_span(
+            "db_delete_resource", attributes={"resource_id": str(resource_id), "sql_query": str(stmt)}
+        ):
+            await db.execute(stmt)
+            await db.commit()
+
+    @staticmethod
+    async def create(db: AsyncSession, resource_in: ResourceIn, maintainer_id: str) -> Resource:
+        """
+        Create a new resource.
+
+        Parameters
+        ----------
+        db : sqlalchemy.ext.asyncio.AsyncSession
+            Async database session to perform query on.
+        resource_in : app.schemas.resource.ResourceIn
+            Data about the new resource.
+        maintainer_id : str
+            UID of the maintainer.
+
+        Returns
+        -------
+        resource : clowmdb.models.Resource
+            The newly created resource
+        """
+        with tracer.start_as_current_span(
+            "db_create_resource",
+            attributes={"maintainer_id": maintainer_id, "resource_in": resource_in.model_dump_json(indent=2)},
+        ) as span:
+            resource_db = Resource(
+                name=resource_in.name,
+                short_description=resource_in.description,
+                source=resource_in.source,
+                maintainer_id=maintainer_id,
+            )
+            db.add(resource_db)
+            await db.commit()
+            span.set_attribute("resource_id", str(resource_db.resource_id))
+            await CRUDResourceVersion.create(db, resource_id=resource_db.resource_id, release=resource_in.release)
+            return await CRUDResource.get(db, resource_db.resource_id)
diff --git a/app/crud/crud_resource_version.py b/app/crud/crud_resource_version.py
new file mode 100644
index 0000000000000000000000000000000000000000..a7c3b02c49d2d550df02c39910e4920110c27244
--- /dev/null
+++ b/app/crud/crud_resource_version.py
@@ -0,0 +1,98 @@
+from typing import Optional
+from uuid import UUID
+
+from clowmdb.models import ResourceVersion
+from opentelemetry import trace
+from sqlalchemy import select, update
+from sqlalchemy.ext.asyncio import AsyncSession
+
+tracer = trace.get_tracer_provider().get_tracer(__name__)
+
+
+class CRUDResourceVersion:
+    @staticmethod
+    async def create(db: AsyncSession, resource_id: UUID, release: str) -> ResourceVersion:
+        """
+        Create a new resource version.
+
+        Parameters
+        ----------
+        db : sqlalchemy.ext.asyncio.AsyncSession.
+        resource_id : uuid.UUID
+            ID of the resource.
+        release : str
+            Versions of the resource.
+
+        Returns
+        -------
+        resource_version : clowmdb.models.ResourceVersion
+            The newly created resource version
+        """
+        with tracer.start_as_current_span(
+            "db_create_resource_version", attributes={"resource_id": str(resource_id), "release": release}
+        ) as span:
+            resource_version_db = ResourceVersion(release=release, _resource_id=resource_id.bytes)
+            db.add(resource_version_db)
+            await db.commit()
+            span.set_attribute("resource_version_id", str(resource_version_db.resource_version_id))
+            return resource_version_db
+
+    @staticmethod
+    async def update_status(db: AsyncSession, resource_version_id: UUID, status: ResourceVersion.Status) -> None:
+        """
+        Update the status of a resource version.
+
+        Parameters
+        ----------
+        db : sqlalchemy.ext.asyncio.AsyncSession.
+            Async database session to perform query on.
+        resource_version_id : uuid.UUID
+            Git commit git_commit_hash of the version.
+        status : clowmdb.models.ResourceVersion.Status
+            New status of the resource version
+        """
+        stmt = (
+            update(ResourceVersion)
+            .where(ResourceVersion._resource_version_id == resource_version_id.bytes)
+            .values(status=status.name)
+        )
+        with tracer.start_as_current_span(
+            "db_update_resource_version_status",
+            attributes={"status": status.name, "resource_version_id": str(resource_version_id), "sql_query": str(stmt)},
+        ):
+            await db.execute(stmt)
+            await db.commit()
+
+    @staticmethod
+    async def get(
+        db: AsyncSession, resource_version_id: UUID, resource_id: Optional[UUID] = None
+    ) -> Optional[ResourceVersion]:
+        """
+        Get a resource version by its ID.
+
+        Parameters
+        ----------
+        db : sqlalchemy.ext.asyncio.AsyncSession.
+            Async database session to perform query on.
+        resource_version_id : uuid.UUID
+            ID of a resource version.
+        resource_id : uuid.UUID | None
+
+
+        Returns
+        -------
+        resource_version : clowmdb.models.ResourceVersion | None
+            The resource version with the given ID if it exists, None otherwise
+        """
+        stmt = select(ResourceVersion).where(ResourceVersion._resource_version_id == resource_version_id.bytes)
+        with tracer.start_as_current_span(
+            "db_get_resource_version",
+            attributes={
+                "resource_version_id": str(resource_version_id),
+            },
+        ) as span:
+            if resource_id:
+                span.set_attribute("resource_id", str(resource_id))
+                stmt = stmt.where(ResourceVersion._resource_id == resource_id.bytes)
+            span.set_attribute("sql_query", str(stmt))
+            return await db.scalar(stmt)
diff --git a/app/s3/s3_resource.py b/app/s3/s3_resource.py
index 219463088251023fae54bfeb58792ffa51e2c80c..3ef709c59a4ae45a37dc4c824366b5dda6c1a621 100644
--- a/app/s3/s3_resource.py
+++ b/app/s3/s3_resource.py
@@ -1,6 +1,7 @@
-from typing import TYPE_CHECKING, List
+from typing import TYPE_CHECKING, Optional
 
 import aioboto3
+from botocore.exceptions import ClientError
 from opentelemetry import trace
 
 from app.core.config import settings
@@ -21,20 +22,33 @@ botosession = aioboto3.Session(
 )
 
 
-async def get_s3_bucket_policy(s3: S3ServiceResource, bucket_name: str) -> BucketPolicy:
-    with tracer.start_as_current_span("s3_get_bucket_policy", attributes={"bucket_name": bucket_name}):
-        s3_policy = await (await s3.Bucket(bucket_name)).Policy()
-        await s3_policy.load()
-        return s3_policy
+async def get_s3_bucket_policy(s3: S3ServiceResource, bucket_name: str) -> Optional[str]:
+    with tracer.start_as_current_span("s3_get_bucket_policy", attributes={"bucket_name": bucket_name}) as span:
+        s3_policy = await s3.BucketPolicy(bucket_name=bucket_name)
+        try:
+            policy = await s3_policy.policy
+        except ClientError:
+            return None
+        span.set_attribute("policy", policy)
+        return policy
 
 
 async def put_s3_bucket_policy(s3: S3ServiceResource, bucket_name: str, policy: str) -> None:
-    with tracer.start_as_current_span("s3_put_bucket_policy", attributes={"bucket_name": bucket_name}):
+    with tracer.start_as_current_span(
+        "s3_put_bucket_policy", attributes={"bucket_name": bucket_name, "policy": policy}
+    ):
         bucket_policy = await s3.BucketPolicy(bucket_name=bucket_name)
         await bucket_policy.put(Policy=policy)
 
 
-async def get_s3_bucket_objects(s3: S3ServiceResource, bucket_name: str) -> List[ObjectSummary]:
-    with tracer.start_as_current_span("s3_get_object_meta_data", attributes={"bucket_name": bucket_name}):
-        bucket = await s3.Bucket(name=bucket_name)
-        return [obj async for obj in bucket.objects.all()]
+async def get_s3_bucket_object(s3: S3ServiceResource, bucket_name: str, key: str) -> Optional[ObjectSummary]:
+    with tracer.start_as_current_span(
+        "s3_get_object_meta_data", attributes={"bucket_name": bucket_name, "key": key}
+    ) as span:
+        obj = await s3.ObjectSummary(bucket_name=bucket_name, key=key)
+        try:
+            await obj.load()
+        except ClientError:
+            return None
+        span.set_attributes({"size": await obj.size, "last_modified": await obj.last_modified, "etag": await obj.e_tag})
+        return obj
diff --git a/app/schemas/resource.py b/app/schemas/resource.py
index 7ed9c869085b46b42cc4e8b846368ad459aadf99..679c9e9ef9a25878a0995823979f222e13d90511 100644
--- a/app/schemas/resource.py
+++ b/app/schemas/resource.py
@@ -1,6 +1,7 @@
-from typing import List
+from typing import List, Optional, Sequence
 from uuid import UUID
 
+from clowmdb.models import Resource, ResourceVersion
 from pydantic import BaseModel, Field
 
 from app.schemas.resource_version import ResourceVersionIn, ResourceVersionOut
@@ -32,3 +33,33 @@ class ResourceOut(BaseResource):
     resource_id: UUID = Field(..., description="ID of the resource", examples=["4c072e39-2bd9-4fa3-b564-4d890e240ccd"])
     maintainer_id: str = Field(..., description="ID of the maintainer", examples=["28c5353b8bb34984a8bd4169ba94c606"])
     versions: List[ResourceVersionOut] = Field(..., description="Versions of the resource")
+
+    @staticmethod
+    def from_db_resource(db_resource: Resource, versions: Optional[Sequence[ResourceVersion]] = None) -> "ResourceOut":
+        """
+        Create a ResourceOut schema from the resource database model.
+
+        Parameters
+        ----------
+        db_resource: clowmdb.models.Resource
+            Database model of a resource.
+        versions : List[clowmdb.models.ResourceVersion] | None, default None
+            List of versions to attach to the schema. If None, they will be loaded from the DB model.
+
+        Returns
+        -------
+        resource : ResourceOut
+            Schema from the database model
+        """
+        if versions is not None:
+            temp_versions = versions
+        else:
+            temp_versions = db_resource.versions
+        return ResourceOut(
+            resource_id=db_resource.resource_id,
+            name=db_resource.name,
+            description=db_resource.short_description,
+            source=db_resource.source,
+            maintainer_id=db_resource.maintainer_id,
+            versions=[ResourceVersionOut.from_db_resource_version(v) for v in temp_versions],
+        )
diff --git a/app/schemas/resource_version.py b/app/schemas/resource_version.py
index 57ac8095e54f4185899dd28d5c0461155654e0be..ac5b77aac8a7ffbff9cb374792c07aefb137e897 100644
--- a/app/schemas/resource_version.py
+++ b/app/schemas/resource_version.py
@@ -54,3 +54,13 @@ class ResourceVersionOut(BaseResourceVersion):
     @property
     def s3_path(self) -> str:
         return f"s3://{settings.RESOURCE_BUCKET}/CLDB-{self.resource_id.hex[:8]}/{self.resource_version_id.hex}/resource.tar.gz"
+
+    @staticmethod
+    def from_db_resource_version(db_resource_version: ResourceVersion) -> "ResourceVersionOut":
+        return ResourceVersionOut(
+            release=db_resource_version.release,
+            status=db_resource_version.status,
+            resource_version_id=db_resource_version.resource_version_id,
+            resource_id=db_resource_version.resource_id,
+            created_at=db_resource_version.created_at,
+        )
diff --git a/app/tests/api/test_resource.py b/app/tests/api/test_resource.py
index a0dc4c0973f85f4d1be788e888de038110bb8b18..204c6d06791c8cadcb32a62472d27fb9fd05baa9 100644
--- a/app/tests/api/test_resource.py
+++ b/app/tests/api/test_resource.py
@@ -1,11 +1,15 @@
+from uuid import uuid4
+
 import pytest
-from clowmdb.models import Resource
+from clowmdb.models import Resource, ResourceVersion
 from fastapi import status
 from httpx import AsyncClient
+from sqlalchemy import delete, select
+from sqlalchemy.ext.asyncio import AsyncSession
 
-from app.schemas.resource import ResourceIn
+from app.schemas.resource import ResourceIn, ResourceOut
 from app.tests.utils.user import UserWithAuthHeader
-from app.tests.utils.utils import random_lower_string
+from app.tests.utils.utils import CleanupList, random_lower_string
 
 
 class _TestResourceRoutes:
@@ -14,7 +18,9 @@ class _TestResourceRoutes:
 
 class TestResourceRouteCreate(_TestResourceRoutes):
     @pytest.mark.asyncio
-    async def test_create_resource_route(self, client: AsyncClient, random_user: UserWithAuthHeader) -> None:
+    async def test_create_resource_route(
+        self, client: AsyncClient, random_user: UserWithAuthHeader, db: AsyncSession, cleanup: CleanupList
+    ) -> None:
         """
         Test for creating a new resource.
 
@@ -24,6 +30,10 @@ class TestResourceRouteCreate(_TestResourceRoutes):
             HTTP Client to perform the request on.
         random_user : app.tests.utils.user.UserWithAuthHeader
             Random user for testing.
+        db : sqlalchemy.ext.asyncio.AsyncSession.
+            Async database session to perform query on.
+        cleanup : list[sqlalchemy.sql.dml.Delete]
+            List were to append sql deletes that gets executed after the test
         """
         resource = ResourceIn(
             release=random_lower_string(6),
@@ -33,6 +43,19 @@ class TestResourceRouteCreate(_TestResourceRoutes):
         )
         response = await client.post(self.base_path, json=resource.model_dump(), headers=random_user.auth_headers)
         assert response.status_code == status.HTTP_201_CREATED
+        resource_out = ResourceOut.model_validate_json(response.content)
+
+        cleanup.append(delete(Resource).where(Resource._resource_id == resource_out.resource_id.bytes))
+
+        assert resource_out.name == resource.name
+        assert len(resource_out.versions) == 1
+        resource_version = resource_out.versions[0]
+        assert resource_version.resource_id == resource_out.resource_id
+        assert resource_version.status == ResourceVersion.Status.RESOURCE_REQUESTED
+        assert resource_version.release == resource.release
+
+        resource_db = await db.scalar(select(Resource).where(Resource._resource_id == resource_out.resource_id.bytes))
+        assert resource_db is not None
 
 
 class TestResourceRouteDelete(_TestResourceRoutes):
@@ -98,3 +121,20 @@ class TestResourceRouteGet(_TestResourceRoutes):
             f"{self.base_path}/{str(random_resource.resource_id)}", headers=random_user.auth_headers
         )
         assert response.status_code == status.HTTP_200_OK
+
+    @pytest.mark.asyncio
+    async def test_get_non_existing_resource_route(
+        self, client: AsyncClient, random_resource: Resource, random_user: UserWithAuthHeader
+    ) -> None:
+        """
+        Test for getting a non-existing resource.
+
+        Parameters
+        ----------
+        client : httpx.AsyncClient
+            HTTP Client to perform the request on.
+        random_user : app.tests.utils.user.UserWithAuthHeader
+            Random user for testing.
+        """
+        response = await client.get(f"{self.base_path}/{str(uuid4())}", headers=random_user.auth_headers)
+        assert response.status_code == status.HTTP_404_NOT_FOUND
diff --git a/app/tests/api/test_resource_version.py b/app/tests/api/test_resource_version.py
index f2c87a18f126c76135a54477ddb0eea1c4c01f83..4b3fa5cd4d51732bb5b589a7b80de8af97c57725 100644
--- a/app/tests/api/test_resource_version.py
+++ b/app/tests/api/test_resource_version.py
@@ -1,3 +1,5 @@
+from uuid import uuid4
+
 import pytest
 from clowmdb.models import Resource, ResourceVersion
 from fastapi import status
@@ -99,6 +101,26 @@ class TestResourceVersionRouteGet(_TestResourceVersionRoutes):
         )
         assert response.status_code == status.HTTP_200_OK
 
+    @pytest.mark.asyncio
+    async def test_get_non_existing_resource_route(
+        self, client: AsyncClient, random_resource: Resource, random_user: UserWithAuthHeader
+    ) -> None:
+        """
+        Test for getting a non-existing resource version.
+
+        Parameters
+        ----------
+        client : httpx.AsyncClient
+            HTTP Client to perform the request on.
+        random_user : app.tests.utils.user.UserWithAuthHeader
+            Random user for testing.
+        """
+        response = await client.get(
+            f"{self.base_path}/{str(random_resource.resource_id)}/versions/{str(uuid4())}",
+            headers=random_user.auth_headers,
+        )
+        assert response.status_code == status.HTTP_404_NOT_FOUND
+
 
 class TestResourceVersionRouteDelete(_TestResourceVersionRoutes):
     @pytest.mark.asyncio
diff --git a/app/tests/conftest.py b/app/tests/conftest.py
index c52da4a18d588860b5759983ba82821f9ef5a22f..4a3200156f416e73a5b07fa2c24bc05895728a78 100644
--- a/app/tests/conftest.py
+++ b/app/tests/conftest.py
@@ -2,7 +2,6 @@ import asyncio
 from functools import partial
 from secrets import token_urlsafe
 from typing import AsyncGenerator, AsyncIterator, Dict, Iterator
-from uuid import uuid4
 
 import httpx
 import pytest
@@ -10,6 +9,7 @@ import pytest_asyncio
 from clowmdb.db.session import get_async_session
 from clowmdb.models import Resource, ResourceVersion
 from pytrie import SortedStringTrie as Trie
+from sqlalchemy import delete
 from sqlalchemy.ext.asyncio import AsyncSession
 
 from app.api.dependencies import get_db, get_decode_jwt_function, get_httpx_client, get_s3_resource
@@ -20,7 +20,7 @@ from app.tests.mocks.mock_opa_service import MockOpaService
 from app.tests.mocks.mock_s3_resource import MockS3ServiceResource
 from app.tests.mocks.mock_slurm_cluster import MockSlurmCluster
 from app.tests.utils.user import UserWithAuthHeader, create_random_user, decode_mock_token, get_authorization_headers
-from app.tests.utils.utils import random_lower_string
+from app.tests.utils.utils import CleanupList, random_lower_string
 
 jwt_secret = token_urlsafe(32)
 
@@ -172,23 +172,41 @@ async def random_resource(db: AsyncSession, random_user: UserWithAuthHeader) ->
     """
     Create a random resource and deletes it afterward.
     """
-    yield Resource(
-        _resource_id=uuid4().bytes,
+    resource_db = Resource(
         name=random_lower_string(8),
         short_description=random_lower_string(32),
         source=random_lower_string(32),
         maintainer_id=random_user.user.uid,
     )
+    db.add(resource_db)
+    await db.commit()
+    resource_version_db = ResourceVersion(
+        release=random_lower_string(8), _resource_id=resource_db.resource_id.bytes, status=ResourceVersion.Status.LATEST
+    )
+    db.add(resource_version_db)
+    await db.commit()
+    await db.refresh(resource_db, attribute_names=["versions"])
+    yield resource_db
+    await db.execute(delete(Resource).where(Resource._resource_id == resource_db.resource_id.bytes))
+    await db.commit()
 
 
 @pytest_asyncio.fixture(scope="function")
-async def random_resource_version(db: AsyncSession, random_resource: Resource) -> AsyncIterator[ResourceVersion]:
+async def random_resource_version(db: AsyncSession, random_resource: Resource) -> ResourceVersion:
     """
     Create a random resource version and deletes it afterward.
     """
-    yield ResourceVersion(
-        _resource_version_id=uuid4().bytes,
-        _resource_id=random_resource.resource_id.bytes,
-        release=random_lower_string(8),
-        status=ResourceVersion.Status.RESOURCE_REQUESTED,
-    )
+    resource_version: ResourceVersion = random_resource.versions[0]
+    return resource_version
+
+
+@pytest_asyncio.fixture(scope="function")
+async def cleanup(db: AsyncSession) -> AsyncIterator[CleanupList]:
+    """
+    Yield a list with sql delete statements that gets executed after a (failed) test
+    """
+    to_delete: CleanupList = []
+    yield to_delete
+    for stmt in to_delete:
+        await db.execute(stmt)
+    await db.commit()
diff --git a/app/tests/crud/test_resource.py b/app/tests/crud/test_resource.py
new file mode 100644
index 0000000000000000000000000000000000000000..1f1c50eb49ca2df99e02e209e3dcf3e88e6844bd
--- /dev/null
+++ b/app/tests/crud/test_resource.py
@@ -0,0 +1,202 @@
+import random
+from uuid import uuid4
+
+import pytest
+from clowmdb.models import Resource, ResourceVersion
+from sqlalchemy import delete, select
+from sqlalchemy.ext.asyncio import AsyncSession
+from sqlalchemy.orm import joinedload
+
+from app.crud import CRUDResource
+from app.schemas.resource import ResourceIn
+from app.tests.utils.user import UserWithAuthHeader
+from app.tests.utils.utils import CleanupList, random_lower_string
+
+
+class TestResourceCRUDGet:
+    @pytest.mark.asyncio
+    async def test_get_resource(self, db: AsyncSession, random_resource: Resource) -> None:
+        """
+        Test for getting an existing resource from the database
+
+        Parameters
+        ----------
+        db : sqlalchemy.ext.asyncio.AsyncSession.
+            Async database session to perform query on.
+        random_resource : clowmdb.models.Resource
+            Random resource for testing.
+        """
+        resource = await CRUDResource.get(db, resource_id=random_resource.resource_id)
+        assert resource is not None
+        assert resource == random_resource
+
+    @pytest.mark.asyncio
+    async def test_get_non_existing_resource(self, db: AsyncSession) -> None:
+        """
+        Test for getting a non-existing resource from the database
+
+        Parameters
+        ----------
+        db : sqlalchemy.ext.asyncio.AsyncSession.
+            Async database session to perform query on.
+        """
+        resource = await CRUDResource.get(db, resource_id=uuid4())
+        assert resource is None
+
+
+class TestResourceCRUDList:
+    @pytest.mark.asyncio
+    async def test_get_all_resources(self, db: AsyncSession, random_resource: Resource) -> None:
+        """
+        Test get all resources from the CRUD Repository.
+
+        Parameters
+        ----------
+        db : sqlalchemy.ext.asyncio.AsyncSession.
+            Async database session to perform query on.
+        random_resource : app.schemas.resource.ResourceOut
+            Random resource for testing.
+        """
+        resources = await CRUDResource.list_resources(db)
+        assert len(resources) == 1
+        assert resources[0].resource_id == random_resource.resource_id
+
+    @pytest.mark.asyncio
+    async def test_get_resources_by_maintainer(
+        self, db: AsyncSession, random_resource: Resource, random_user: UserWithAuthHeader
+    ) -> None:
+        """
+        Test get only resources from the CRUD Repository by specific maintainer.
+
+        Parameters
+        ----------
+        db : sqlalchemy.ext.asyncio.AsyncSession.
+            Async database session to perform query on.
+        random_resource : app.schemas.resource.ResourceOut
+            Random resource for testing.
+        random_user : app.tests.utils.user.UserWithAuthHeader
+            Random user for testing.
+        """
+        resources = await CRUDResource.list_resources(db, maintainer_id=random_user.user.uid)
+        assert len(resources) == 1
+        assert resources[0].resource_id == random_resource.resource_id
+
+    @pytest.mark.asyncio
+    async def test_get_resources_with_unpublished_version(self, db: AsyncSession, random_resource: Resource) -> None:
+        """
+        Test get only resources from the CRUD Repository with an unpublished version.
+
+        Parameters
+        ----------
+        db : sqlalchemy.ext.asyncio.AsyncSession.
+            Async database session to perform query on.
+        random_resource : app.schemas.resource.ResourceOut
+            Random resource for testing.
+        """
+        resources = await CRUDResource.list_resources(db, version_status=[ResourceVersion.Status.LATEST])
+        assert len(resources) == 1
+        assert resources[0].resource_id == random_resource.resource_id
+
+    @pytest.mark.asyncio
+    async def test_get_resource_with_name_substring(self, db: AsyncSession, random_resource: Resource) -> None:
+        """
+        Test get resources with a substring in their name from the CRUD Repository.
+
+        Parameters
+        ----------
+        db : sqlalchemy.ext.asyncio.AsyncSession.
+            Async database session to perform query on.
+        random_resource : app.schemas.resource.ResourceOut
+            Random resource for testing.
+        """
+        substring_indices = [0, 0]
+        while substring_indices[0] == substring_indices[1]:
+            substring_indices = sorted(random.choices(range(len(random_resource.name)), k=2))
+
+        random_substring = random_resource.name[substring_indices[0] : substring_indices[1]]
+        resources = await CRUDResource.list_resources(db, name_substring=random_substring)
+        assert len(resources) > 0
+        assert random_resource.resource_id in map(lambda w: w.resource_id, resources)
+
+    @pytest.mark.asyncio
+    async def test_search_non_existing_resource_by_name(self, db: AsyncSession, random_resource: Resource) -> None:
+        """
+        Test for getting a non-existing resource by its name from CRUD Repository.
+
+        Parameters
+        ----------
+        db : sqlalchemy.ext.asyncio.AsyncSession.
+            Async database session to perform query on.
+        random_resource : app.schemas.resource.ResourceOut
+            Random resource for testing.
+        """
+        resources = await CRUDResource.list_resources(db, name_substring=2 * random_resource.name)
+        assert sum(1 for w in resources if w.resource_id == random_resource.resource_id) == 0
+
+
+class TestResourceCRUDDelete:
+    @pytest.mark.asyncio
+    async def test_delete_resource(self, db: AsyncSession, random_resource: Resource) -> None:
+        """
+        Test for deleting a resource from CRUD Repository.
+
+        Parameters
+        ----------
+        db : sqlalchemy.ext.asyncio.AsyncSession.
+            Async database session to perform query on.
+        random_resource : app.schemas.resource.ResourceOut
+            Random resource for testing.
+        """
+        await CRUDResource.delete(db, resource_id=random_resource.resource_id)
+
+        deleted_resource = await db.scalar(
+            select(Resource).where(Resource._resource_id == random_resource.resource_id.bytes)
+        )
+        assert deleted_resource is None
+
+        deleted_resource_versions = (
+            await db.scalars(
+                select(ResourceVersion).where(ResourceVersion._resource_id == random_resource.resource_id.bytes)
+            )
+        ).all()
+        assert len(deleted_resource_versions) == 0
+
+
+class TestResourceCRUDCreate:
+    @pytest.mark.asyncio
+    async def test_create_resource(
+        self, db: AsyncSession, random_user: UserWithAuthHeader, cleanup: CleanupList
+    ) -> None:
+        """
+        Test for creating a new resource with the CRUD Repository.
+
+        Parameters
+        ----------
+        db : sqlalchemy.ext.asyncio.AsyncSession.
+            Async database session to perform query on.
+        random_user : app.tests.utils.user.UserWithAuthHeader
+            Random user for testing.
+        cleanup : list[sqlalchemy.sql.dml.Delete]
+            List were to append sql deletes that gets executed after the test
+        """
+        resource_in = ResourceIn(
+            name=random_lower_string(8),
+            description=random_lower_string(),
+            source=random_lower_string(),
+            release=random_lower_string(8),
+        )
+
+        resource = await CRUDResource.create(db, resource_in=resource_in, maintainer_id=random_user.user.uid)
+        assert resource is not None
+        cleanup.append(delete(Resource).where(Resource._resource_id == resource.resource_id.bytes))
+
+        created_resource = await db.scalar(
+            select(Resource)
+            .where(Resource._resource_id == resource.resource_id.bytes)
+            .options(joinedload(Resource.versions))
+        )
+        assert created_resource is not None
+        assert created_resource == resource
+
+        assert len(created_resource.versions) == 1
+        assert created_resource.versions[0].release == resource_in.release
diff --git a/app/tests/crud/test_resource_version.py b/app/tests/crud/test_resource_version.py
new file mode 100644
index 0000000000000000000000000000000000000000..04254c68dee69bbdbbdacce671752a1f05df6e2a
--- /dev/null
+++ b/app/tests/crud/test_resource_version.py
@@ -0,0 +1,197 @@
+from uuid import uuid4
+
+import pytest
+from clowmdb.models import Resource, ResourceVersion
+from sqlalchemy import delete, select
+from sqlalchemy.exc import IntegrityError
+from sqlalchemy.ext.asyncio import AsyncSession
+
+from app.crud import CRUDResourceVersion
+from app.tests.utils.utils import CleanupList, random_lower_string
+
+
+class TestResourceVersionCRUDGet:
+    @pytest.mark.asyncio
+    async def test_get_resource_version(self, db: AsyncSession, random_resource_version: ResourceVersion) -> None:
+        """
+        Test for getting an existing resource version from the database
+
+        Parameters
+        ----------
+        db : sqlalchemy.ext.asyncio.AsyncSession.
+            Async database session to perform query on.
+        random_resource_version : clowmdb.models.ResourceVersion
+            Random resource for testing.
+        """
+        resource = await CRUDResourceVersion.get(db, resource_version_id=random_resource_version.resource_version_id)
+        assert resource is not None
+        assert resource == random_resource_version
+
+    @pytest.mark.asyncio
+    async def test_get_resource_version_with_resource_id(
+        self, db: AsyncSession, random_resource_version: ResourceVersion
+    ) -> None:
+        """
+        Test for getting an existing resource version from the database and filter is with the correct resource id
+
+        Parameters
+        ----------
+        db : sqlalchemy.ext.asyncio.AsyncSession.
+            Async database session to perform query on.
+        random_resource_version : clowmdb.models.ResourceVersion
+            Random resource for testing.
+        """
+        resource = await CRUDResourceVersion.get(
+            db,
+            resource_version_id=random_resource_version.resource_version_id,
+            resource_id=random_resource_version.resource_id,
+        )
+        assert resource is not None
+        assert resource == random_resource_version
+
+    @pytest.mark.asyncio
+    async def test_get_resource_version_with_wrong_resource_id(
+        self, db: AsyncSession, random_resource_version: ResourceVersion
+    ) -> None:
+        """
+        Test for getting an existing resource version from the database and filter it with a wrong resource id
+
+        Parameters
+        ----------
+        db : sqlalchemy.ext.asyncio.AsyncSession.
+            Async database session to perform query on.
+        random_resource_version : clowmdb.models.ResourceVersion
+            Random resource for testing.
+        """
+        resource = await CRUDResourceVersion.get(
+            db, resource_version_id=random_resource_version.resource_id, resource_id=uuid4()
+        )
+        assert resource is None
+
+    @pytest.mark.asyncio
+    async def test_get_non_existing_resource_version(self, db: AsyncSession) -> None:
+        """
+        Test for getting a non-existing resource version from the database
+
+        Parameters
+        ----------
+        db : sqlalchemy.ext.asyncio.AsyncSession.
+            Async database session to perform query on.
+        """
+        resource = await CRUDResourceVersion.get(db, resource_version_id=uuid4())
+        assert resource is None
+
+
+class TestResourceCRUDUpdate:
+    @pytest.mark.asyncio
+    async def test_update_resource_version(self, db: AsyncSession, random_resource_version: ResourceVersion) -> None:
+        """
+        Test for updating the resource version status from the CRUD Repository.
+
+        Parameters
+        ----------
+        db : sqlalchemy.ext.asyncio.AsyncSession.
+            Async database session to perform query on.
+        random_resource_version : clowmdb.models.ResourceVersion
+            Random resource for testing.
+        """
+        await CRUDResourceVersion.update_status(
+            db,
+            resource_version_id=random_resource_version.resource_version_id,
+            status=ResourceVersion.Status.S3_DELETED,
+        )
+
+        updated_resource_version = await db.scalar(
+            select(ResourceVersion).where(
+                ResourceVersion._resource_version_id == random_resource_version.resource_version_id.bytes
+            )
+        )
+        assert updated_resource_version is not None
+        assert updated_resource_version == random_resource_version
+
+        assert updated_resource_version.status == ResourceVersion.Status.S3_DELETED
+
+    @pytest.mark.asyncio
+    async def test_update_non_existing_resource_version(
+        self, db: AsyncSession, random_resource_version: ResourceVersion
+    ) -> None:
+        """
+        Test for updating a non-existing resource version status from the CRUD Repository.
+
+        Parameters
+        ----------
+        db : sqlalchemy.ext.asyncio.AsyncSession.
+            Async database session to perform query on.
+        random_resource_version : clowmdb.models.ResourceVersion
+            Random resource for testing.
+        """
+        await CRUDResourceVersion.update_status(
+            db,
+            resource_version_id=uuid4(),
+            status=ResourceVersion.Status.S3_DELETED,
+        )
+
+        resource_version = await db.scalar(
+            select(ResourceVersion).where(
+                ResourceVersion._resource_version_id == random_resource_version.resource_version_id.bytes
+            )
+        )
+        assert resource_version is not None
+        assert resource_version == random_resource_version
+
+        assert resource_version.status == random_resource_version.status
+
+
+class TestResourceCRUDCreate:
+    @pytest.mark.asyncio
+    async def test_create_resource_version(
+        self, db: AsyncSession, random_resource: Resource, cleanup: CleanupList
+    ) -> None:
+        """
+        Test for creating a new resource version with the CRUD Repository.
+
+        Parameters
+        ----------
+        db : sqlalchemy.ext.asyncio.AsyncSession.
+            Async database session to perform query on.
+        random_resource : clowmdb.models.Resource
+            Random resource for testing.
+        cleanup : list[sqlalchemy.sql.dml.Delete]
+            List were to append sql deletes that gets executed after the test
+        """
+        release = random_lower_string(8)
+
+        resource_version = await CRUDResourceVersion.create(
+            db, resource_id=random_resource.resource_id, release=release
+        )
+
+        assert resource_version is not None
+
+        cleanup.append(
+            delete(ResourceVersion).where(
+                ResourceVersion._resource_version_id == resource_version.resource_version_id.bytes
+            )
+        )
+
+        created_resource_version = await db.scalar(
+            select(ResourceVersion).where(
+                ResourceVersion._resource_version_id == resource_version.resource_version_id.bytes
+            )
+        )
+        assert created_resource_version is not None
+        assert created_resource_version == resource_version
+
+        assert resource_version.status == ResourceVersion.Status.RESOURCE_REQUESTED
+
+    @pytest.mark.asyncio
+    async def test_create_resource_version_with_wrong_resource_id(self, db: AsyncSession) -> None:
+        """
+        Test for creating a new resource version with a wrong resource id the CRUD Repository.
+
+        Parameters
+        ----------
+        db : sqlalchemy.ext.asyncio.AsyncSession.
+            Async database session to perform query on.
+        """
+        with pytest.raises(IntegrityError):
+            await CRUDResourceVersion.create(db, resource_id=uuid4(), release=random_lower_string(8))
diff --git a/app/tests/mocks/mock_s3_resource.py b/app/tests/mocks/mock_s3_resource.py
index e6fcd218a3575d0b6539c99897e3ec72d581ab50..80a5d1cd6fc9f477479018dc051beac475a51c50 100644
--- a/app/tests/mocks/mock_s3_resource.py
+++ b/app/tests/mocks/mock_s3_resource.py
@@ -3,6 +3,8 @@ from typing import TYPE_CHECKING, Dict, List, Optional
 
 from botocore.exceptions import ClientError
 
+from app.tests.utils.utils import random_hex_string
+
 if TYPE_CHECKING:
     from types_aiobotocore_s3.type_defs import CORSConfigurationTypeDef
 else:
@@ -59,6 +61,8 @@ class MockS3ObjectSummary:
         Size of object in bytes. Always 100.
     last_modified : datetime
         Time and date of last modification of this object.
+    etag : str
+        Hash of the object content
     """
 
     def __init__(self, bucket_name: str, key: str) -> None:
@@ -76,6 +80,7 @@ class MockS3ObjectSummary:
         self.bucket_name = bucket_name
         self.size = 100
         self.last_modified = datetime.now()
+        self.etag = random_hex_string(32)
 
     def __repr__(self) -> str:
         return f"MockS3ObjectSummary(key={self.key}, bucket={self.bucket_name})"
diff --git a/app/tests/utils/utils.py b/app/tests/utils/utils.py
index daba17cbe309b89b4e27e3728ac2306851a63f59..18687fb64e90134d5fb7306b6ae55e6211cce761 100644
--- a/app/tests/utils/utils.py
+++ b/app/tests/utils/utils.py
@@ -1,5 +1,10 @@
 import random
 import string
+from typing import List
+
+from sqlalchemy.sql.dml import Delete
+
+CleanupList = List[Delete]
 
 
 def random_lower_string(length: int = 32) -> str:
diff --git a/pyproject.toml b/pyproject.toml
index dbdfd79801d399025e9d43e8a03eeda9421a5865..e48e191ed42e52535b20dea692f0263c77c27b55 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -24,7 +24,7 @@ concurrency = [
 omit = [
     "app/tests/**",
     "app/check_database_connection.py",
-    "app/check_ceph_connection.py",
+    "app/check_s3_connection.py",
     "app/check_slurm_connection.py",
     "app/core/config.py",
 ]
diff --git a/requirements-dev.txt b/requirements-dev.txt
index f7559f876963cb3c1f9ad337a7e96426f1c82581..174bb00e549b7d9625c6a149ca30934f701a1477 100644
--- a/requirements-dev.txt
+++ b/requirements-dev.txt
@@ -5,12 +5,12 @@ pytest-cov>=4.1.0,<4.2.0
 coverage[toml]>=7.3.0,<7.4.0
 # Linters
 ruff>=0.1.0,<0.2.0
-black>=23.11.0,<23.12.0
-isort>=5.12.0,<5.13.0
+black>=23.12.0,<24.0.0
+isort>=5.13.0,<5.14.0
 mypy>=1.7.0,<1.8.0
 # stubs for mypy
 types-aiobotocore-lite[s3]>=2.8.0,<2.9.0
 types-requests
 # Miscellaneous
-pre-commit>=3.5.0,<3.6.0
+pre-commit>=3.6.0,<3.7.0
 PyTrie>=0.4.0,<0.5.0
diff --git a/requirements.txt b/requirements.txt
index 3c10cc145b5c02d4572b65d6acea5c7a1cebc515..9b690e87b5fc1394d0da433925b5c64bca20c83a 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -3,7 +3,7 @@ clowmdb>=2.3.0,<2.4.0
 
 # Webserver packages
 anyio>=3.7.0,<4.0.0
-fastapi>=0.104.0,<0.105.0
+fastapi>=0.105.0,<0.106.0
 pydantic>=2.5.0,<2.6.0
 pydantic-settings>=2.1.0,<2.2.0
 uvicorn>=0.24.0,<0.25.0