diff --git a/.codacy.yml b/.codacy.yml
index 0549c8f5e921..55fc06796848 100644
--- a/.codacy.yml
+++ b/.codacy.yml
@@ -1,3 +1,4 @@
 exclude_paths:
  - '**/3rdparty/**'
  - '**/engine/js/cvat-core.min.js'
+ - CHANGELOG.md
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8e560d05d5ba..f3f90e4efc8c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,25 @@ All notable changes to this project will be documented in this file.
 The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
 and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
 
+## [1.0.0.alpha] - 2020-02-XX
+### Added
+- Server only support for projects. Extend REST API v1 (/api/v1/projects*).
+
+### Changed
+-
+
+### Deprecated
+-
+
+### Removed
+-
+
+### Fixed
+-
+
+### Security
+-
+
 ## [0.5.0] - 2019-10-12
 ### Added
 - A converter to YOLO format
diff --git a/cvat/apps/authentication/auth.py b/cvat/apps/authentication/auth.py
index da4613bf3257..2f54558c62af 100644
--- a/cvat/apps/authentication/auth.py
+++ b/cvat/apps/authentication/auth.py
@@ -63,20 +63,33 @@ def authenticate(self, request):
 has_annotator_role = rules.is_group_member(str(AUTH_ROLE.ANNOTATOR))
 has_observer_role = rules.is_group_member(str(AUTH_ROLE.OBSERVER))
 
+@rules.predicate
+def is_project_owner(db_user, db_project):
+    # If owner is None (null) the task can be accessed/changed/deleted
+    # only by admin. At the moment each task has an owner.
+    return db_project is not None and db_project.owner == db_user
+
+@rules.predicate
+def is_project_assignee(db_user, db_project):
+    return db_project is not None and db_project.assignee == db_user
+
+@rules.predicate
+def is_project_annotator(db_user, db_project):
+    db_tasks = list(db_project.tasks.prefetch_related('segment_set').all())
+    return any([is_task_annotator(db_user, db_task) for db_task in db_tasks])
+
 @rules.predicate
 def is_task_owner(db_user, db_task):
     # If owner is None (null) the task can be accessed/changed/deleted
     # only by admin. At the moment each task has an owner.
-    return db_task.owner == db_user
+    return db_task.owner == db_user or is_project_owner(db_user, db_task.project)
 
 @rules.predicate
 def is_task_assignee(db_user, db_task):
-    return db_task.assignee == db_user
+    return db_task.assignee == db_user or is_project_assignee(db_user, db_task.project)
 
 @rules.predicate
 def is_task_annotator(db_user, db_task):
-    from functools import reduce
-
     db_segments = list(db_task.segment_set.prefetch_related('job_set__assignee').all())
     return any([is_job_annotator(db_user, db_job)
         for db_segment in db_segments for db_job in db_segment.job_set.all()])
@@ -101,6 +114,13 @@ def is_job_annotator(db_user, db_job):
 rules.add_perm('engine.role.annotator', has_annotator_role)
 rules.add_perm('engine.role.observer', has_observer_role)
 
+rules.add_perm('engine.project.create', has_admin_role | has_user_role)
+rules.add_perm('engine.project.access', has_admin_role | has_observer_role |
+    is_project_owner | is_project_annotator)
+rules.add_perm('engine.project.change', has_admin_role | is_project_owner |
+    is_project_assignee)
+rules.add_perm('engine.project.delete', has_admin_role | is_project_owner)
+
 rules.add_perm('engine.task.create', has_admin_role | has_user_role)
 rules.add_perm('engine.task.access', has_admin_role | has_observer_role |
     is_task_owner | is_task_annotator)
@@ -133,6 +153,26 @@ class ObserverRolePermission(BasePermission):
     def has_permission(self, request, view):
         return request.user.has_perm("engine.role.observer")
 
+class ProjectCreatePermission(BasePermission):
+    # pylint: disable=no-self-use
+    def has_permission(self, request, view):
+        return request.user.has_perm("engine.project.create")
+
+class ProjectAccessPermission(BasePermission):
+    # pylint: disable=no-self-use
+    def has_object_permission(self, request, view, obj):
+        return request.user.has_perm("engine.project.access", obj)
+
+class ProjectChangePermission(BasePermission):
+    # pylint: disable=no-self-use
+    def has_object_permission(self, request, view, obj):
+        return request.user.has_perm("engine.project.change", obj)
+
+class ProjectDeletePermission(BasePermission):
+    # pylint: disable=no-self-use
+    def has_object_permission(self, request, view, obj):
+        return request.user.has_perm("engine.project.delete", obj)
+
 class TaskCreatePermission(BasePermission):
     # pylint: disable=no-self-use
     def has_permission(self, request, view):
@@ -143,7 +183,8 @@ class TaskAccessPermission(BasePermission):
     def has_object_permission(self, request, view, obj):
         return request.user.has_perm("engine.task.access", obj)
 
-class TaskGetQuerySetMixin(object):
+
+class ProjectGetQuerySetMixin(object):
     def get_queryset(self):
         queryset = super().get_queryset()
         user = self.request.user
@@ -152,7 +193,26 @@ def get_queryset(self):
             return queryset
         else:
             return queryset.filter(Q(owner=user) | Q(assignee=user) |
-                Q(segment__job__assignee=user) | Q(assignee=None)).distinct()
+                Q(task__owner=user) | Q(task__assignee=user) |
+                Q(task__segment__job__assignee=user)).distinct()
+
+def filter_task_queryset(queryset, user):
+    # Don't filter queryset for admin, observer
+    if has_admin_role(user) or has_observer_role(user):
+        return queryset
+    else:
+        return queryset.filter(Q(owner=user) | Q(assignee=user) |
+            Q(segment__job__assignee=user) | Q(assignee=None)).distinct()
+
+class TaskGetQuerySetMixin(object):
+    def get_queryset(self):
+        queryset = super().get_queryset()
+        user = self.request.user
+        # Don't filter queryset for detail methods
+        if self.detail:
+            return queryset
+        else:
+            return filter_task_queryset(queryset, user)
 
 class TaskChangePermission(BasePermission):
     # pylint: disable=no-self-use
diff --git a/cvat/apps/engine/migrations/0022_auto_20191004_0817.py b/cvat/apps/engine/migrations/0022_auto_20191004_0817.py
new file mode 100644
index 000000000000..d6701bf1167d
--- /dev/null
+++ b/cvat/apps/engine/migrations/0022_auto_20191004_0817.py
@@ -0,0 +1,38 @@
+# Generated by Django 2.2.3 on 2019-10-04 08:17
+
+import cvat.apps.engine.models
+from django.conf import settings
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+        ('engine', '0021_auto_20190826_1827'),
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name='Project',
+            fields=[
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('name', cvat.apps.engine.models.SafeCharField(max_length=256)),
+                ('bug_tracker', models.CharField(blank=True, default='', max_length=2000)),
+                ('created_date', models.DateTimeField(auto_now_add=True)),
+                ('updated_date', models.DateTimeField(auto_now_add=True)),
+                ('status', models.CharField(choices=[('annotation', 'ANNOTATION'), ('validation', 'VALIDATION'), ('completed', 'COMPLETED')], default=cvat.apps.engine.models.StatusChoice('annotation'), max_length=32)),
+                ('assignee', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)),
+                ('owner', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)),
+            ],
+            options={
+                'default_permissions': (),
+            },
+        ),
+        migrations.AddField(
+            model_name='task',
+            name='project',
+            field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='tasks', related_query_name='task', to='engine.Project'),
+        ),
+    ]
diff --git a/cvat/apps/engine/models.py b/cvat/apps/engine/models.py
index d41000ade61a..e55dcd24f215 100644
--- a/cvat/apps/engine/models.py
+++ b/cvat/apps/engine/models.py
@@ -33,7 +33,26 @@ def choices(self):
     def __str__(self):
         return self.value
 
+class Project(models.Model):
+    name = SafeCharField(max_length=256)
+    owner = models.ForeignKey(User, null=True, blank=True,
+        on_delete=models.SET_NULL, related_name="+")
+    assignee = models.ForeignKey(User, null=True,  blank=True,
+        on_delete=models.SET_NULL, related_name="+")
+    bug_tracker = models.CharField(max_length=2000, blank=True, default="")
+    created_date = models.DateTimeField(auto_now_add=True)
+    updated_date = models.DateTimeField(auto_now_add=True)
+    status = models.CharField(max_length=32, choices=StatusChoice.choices(),
+        default=StatusChoice.ANNOTATION)
+
+    # Extend default permission model
+    class Meta:
+        default_permissions = ()
+
 class Task(models.Model):
+    project = models.ForeignKey(Project, on_delete=models.CASCADE,
+        null=True, blank=True, related_name="tasks",
+        related_query_name="task")
     name = SafeCharField(max_length=256)
     size = models.PositiveIntegerField()
     mode = models.CharField(max_length=32)
diff --git a/cvat/apps/engine/serializers.py b/cvat/apps/engine/serializers.py
index da90c66f86e9..24a38c1cbf06 100644
--- a/cvat/apps/engine/serializers.py
+++ b/cvat/apps/engine/serializers.py
@@ -196,7 +196,8 @@ class Meta:
         fields = ('url', 'id', 'name', 'size', 'mode', 'owner', 'assignee',
             'bug_tracker', 'created_date', 'updated_date', 'overlap',
             'segment_size', 'z_order', 'status', 'labels', 'segments',
-            'image_quality', 'start_frame', 'stop_frame', 'frame_filter')
+            'image_quality', 'start_frame', 'stop_frame', 'frame_filter',
+            'project')
         read_only_fields = ('size', 'mode', 'created_date', 'updated_date',
             'status')
         write_once_fields = ('overlap', 'segment_size', 'image_quality')
@@ -245,6 +246,7 @@ def update(self, instance, validated_data):
         instance.start_frame = validated_data.get('start_frame', instance.start_frame)
         instance.stop_frame = validated_data.get('stop_frame', instance.stop_frame)
         instance.frame_filter = validated_data.get('frame_filter', instance.frame_filter)
+        instance.project = validated_data.get('project', instance.project)
         labels = validated_data.get('label_set', [])
         for label in labels:
             attributes = label.pop('attributespec_set', [])
@@ -276,6 +278,14 @@ def update(self, instance, validated_data):
         instance.save()
         return instance
 
+class ProjectSerializer(serializers.ModelSerializer):
+    class Meta:
+        model = models.Project
+        fields = ('url', 'id', 'name', 'owner', 'assignee', 'bug_tracker',
+            'created_date', 'updated_date', 'status')
+        read_only_fields = ('created_date', 'updated_date', 'status')
+        ordering = ['-id']
+
 class UserSerializer(serializers.ModelSerializer):
     groups = serializers.SlugRelatedField(many=True,
         slug_field='name', queryset=Group.objects.all())
diff --git a/cvat/apps/engine/tests/test_rest_api.py b/cvat/apps/engine/tests/test_rest_api.py
index 457a46e8a08b..ad6cac4c79ba 100644
--- a/cvat/apps/engine/tests/test_rest_api.py
+++ b/cvat/apps/engine/tests/test_rest_api.py
@@ -12,7 +12,7 @@
 from django.conf import settings
 from django.contrib.auth.models import User, Group
 from cvat.apps.engine.models import (Task, Segment, Job, StatusChoice,
-    AttributeType)
+    AttributeType, Project)
 from cvat.apps.annotation.models import AnnotationFormat
 from unittest import mock
 import io
@@ -68,7 +68,7 @@ def create_db_task(data):
 
     return db_task
 
-def create_dummy_db_tasks(obj):
+def create_dummy_db_tasks(obj, project=None):
     tasks = []
 
     data = {
@@ -79,7 +79,8 @@ def create_dummy_db_tasks(obj):
         "segment_size": 100,
         "z_order": False,
         "image_quality": 75,
-        "size": 100
+        "size": 100,
+        "project": project
     }
     db_task = create_db_task(data)
     tasks.append(db_task)
@@ -91,7 +92,8 @@ def create_dummy_db_tasks(obj):
         "segment_size": 100,
         "z_order": True,
         "image_quality": 50,
-        "size": 200
+        "size": 200,
+        "project": project
     }
     db_task = create_db_task(data)
     tasks.append(db_task)
@@ -104,7 +106,8 @@ def create_dummy_db_tasks(obj):
         "segment_size": 100,
         "z_order": False,
         "image_quality": 75,
-        "size": 100
+        "size": 100,
+        "project": project
     }
     db_task = create_db_task(data)
     tasks.append(db_task)
@@ -116,13 +119,61 @@ def create_dummy_db_tasks(obj):
         "segment_size": 50,
         "z_order": False,
         "image_quality": 95,
-        "size": 50
+        "size": 50,
+        "project": project
     }
     db_task = create_db_task(data)
     tasks.append(db_task)
 
     return tasks
 
+def create_dummy_db_projects(obj):
+    projects = []
+
+    data = {
+        "name": "my empty project",
+        "owner": obj.owner,
+        "assignee": obj.assignee,
+    }
+    db_project = Project.objects.create(**data)
+    projects.append(db_project)
+
+    data = {
+        "name": "my project without assignee",
+        "owner": obj.user,
+    }
+    db_project = Project.objects.create(**data)
+    create_dummy_db_tasks(obj, db_project)
+    projects.append(db_project)
+
+    data = {
+        "name": "my big project",
+        "owner": obj.owner,
+        "assignee": obj.assignee,
+    }
+    db_project = Project.objects.create(**data)
+    create_dummy_db_tasks(obj, db_project)
+    projects.append(db_project)
+
+    data = {
+        "name": "public project",
+    }
+    db_project = Project.objects.create(**data)
+    create_dummy_db_tasks(obj, db_project)
+    projects.append(db_project)
+
+    data = {
+        "name": "super project",
+        "owner": obj.admin,
+        "assignee": obj.assignee,
+    }
+    db_project = Project.objects.create(**data)
+    create_dummy_db_tasks(obj, db_project)
+    projects.append(db_project)
+
+    return projects
+
+
 class ForceLogin:
     def __init__(self, user, client):
         self.user = user
@@ -587,6 +638,323 @@ def test_api_v1_users_id_no_auth_partial(self):
         response = self._run_api_v1_users_id(None, self.user.id, data)
         self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
 
+class ProjectListAPITestCase(APITestCase):
+    def setUp(self):
+        self.client = APIClient()
+
+    @classmethod
+    def setUpTestData(cls):
+        create_db_users(cls)
+        cls.projects = create_dummy_db_projects(cls)
+
+    def _run_api_v1_projects(self, user, params=""):
+        with ForceLogin(user, self.client):
+            response = self.client.get('/api/v1/projects{}'.format(params))
+
+        return response
+
+    def test_api_v1_projects_admin(self):
+        response = self._run_api_v1_projects(self.admin)
+        self.assertEqual(response.status_code, status.HTTP_200_OK)
+        self.assertListEqual(
+            sorted([project.name for project in self.projects]),
+            sorted([res["name"] for res in response.data["results"]]))
+
+    def test_api_v1_projects_user(self):
+        response = self._run_api_v1_projects(self.user)
+        self.assertEqual(response.status_code, status.HTTP_200_OK)
+        self.assertListEqual(
+            sorted([project.name for project in self.projects
+                if 'my empty project' != project.name]),
+            sorted([res["name"] for res in response.data["results"]]))
+
+    def test_api_v1_projects_observer(self):
+        response = self._run_api_v1_projects(self.observer)
+        self.assertEqual(response.status_code, status.HTTP_200_OK)
+        self.assertListEqual(
+            sorted([project.name for project in self.projects]),
+            sorted([res["name"] for res in response.data["results"]]))
+
+    def test_api_v1_projects_no_auth(self):
+        response = self._run_api_v1_projects(None)
+        self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
+
+class ProjectGetAPITestCase(APITestCase):
+    def setUp(self):
+        self.client = APIClient()
+
+    @classmethod
+    def setUpTestData(cls):
+        create_db_users(cls)
+        cls.projects = create_dummy_db_projects(cls)
+
+    def _run_api_v1_projects_id(self, pid, user):
+        with ForceLogin(user, self.client):
+            response = self.client.get('/api/v1/projects/{}'.format(pid))
+
+        return response
+
+    def _check_response(self, response, db_project):
+        self.assertEqual(response.status_code, status.HTTP_200_OK)
+        self.assertEqual(response.data["name"], db_project.name)
+        owner = db_project.owner.id if db_project.owner else None
+        self.assertEqual(response.data["owner"], owner)
+        assignee = db_project.assignee.id if db_project.assignee else None
+        self.assertEqual(response.data["assignee"], assignee)
+        self.assertEqual(response.data["status"], db_project.status)
+
+    def _check_api_v1_projects_id(self, user):
+        for db_project in self.projects:
+            response = self._run_api_v1_projects_id(db_project.id, user)
+            if user and user.has_perm("engine.project.access", db_project):
+                self._check_response(response, db_project)
+            elif user:
+                self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
+            else:
+                self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
+
+    def test_api_v1_projects_id_admin(self):
+        self._check_api_v1_projects_id(self.admin)
+
+    def test_api_v1_projects_id_user(self):
+        self._check_api_v1_projects_id(self.user)
+
+    def test_api_v1_projects_id_observer(self):
+        self._check_api_v1_projects_id(self.observer)
+
+    def test_api_v1_projects_id_no_auth(self):
+        self._check_api_v1_projects_id(None)
+
+class ProjectDeleteAPITestCase(APITestCase):
+    def setUp(self):
+        self.client = APIClient()
+
+    @classmethod
+    def setUpTestData(cls):
+        create_db_users(cls)
+        cls.projects = create_dummy_db_projects(cls)
+
+    def _run_api_v1_projects_id(self, pid, user):
+        with ForceLogin(user, self.client):
+            response = self.client.delete('/api/v1/projects/{}'.format(pid), format="json")
+
+        return response
+
+    def _check_api_v1_projects_id(self, user):
+        for db_project in self.projects:
+            response = self._run_api_v1_projects_id(db_project.id, user)
+            if user and user.has_perm("engine.project.delete", db_project):
+                self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
+            elif user:
+                self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
+            else:
+                self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
+
+    def test_api_v1_projects_id_admin(self):
+        self._check_api_v1_projects_id(self.admin)
+
+    def test_api_v1_projects_id_user(self):
+        self._check_api_v1_projects_id(self.user)
+
+    def test_api_v1_projects_id_observer(self):
+        self._check_api_v1_projects_id(self.observer)
+
+    def test_api_v1_projects_id_no_auth(self):
+        self._check_api_v1_projects_id(None)
+
+class ProjectCreateAPITestCase(APITestCase):
+    def setUp(self):
+        self.client = APIClient()
+
+    @classmethod
+    def setUpTestData(cls):
+        create_db_users(cls)
+
+    def _run_api_v1_projects(self, user, data):
+        with ForceLogin(user, self.client):
+            response = self.client.post('/api/v1/projects', data=data, format="json")
+
+        return response
+
+    def _check_response(self, response, user, data):
+        self.assertEqual(response.status_code, status.HTTP_201_CREATED)
+        self.assertEqual(response.data["name"], data["name"])
+        self.assertEqual(response.data["owner"], data.get("owner", user.id))
+        self.assertEqual(response.data["assignee"], data.get("assignee"))
+        self.assertEqual(response.data["bug_tracker"], data.get("bug_tracker", ""))
+        self.assertEqual(response.data["status"], StatusChoice.ANNOTATION)
+
+    def _check_api_v1_projects(self, user, data):
+        response = self._run_api_v1_projects(user, data)
+        if user and user.has_perm("engine.project.create"):
+            self._check_response(response, user, data)
+        elif user:
+            self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
+        else:
+            self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
+
+    def test_api_v1_projects_admin(self):
+        data = {
+            "name": "new name for the project",
+            "bug_tracker": "http://example.com"
+        }
+        self._check_api_v1_projects(self.admin, data)
+
+        data = {
+            "owner": self.owner.id,
+            "assignee": self.assignee.id,
+            "name": "new name for the project"
+        }
+        self._check_api_v1_projects(self.admin, data)
+
+        data = {
+            "owner": self.admin.id,
+            "name": "2"
+        }
+        self._check_api_v1_projects(self.admin, data)
+
+
+    def test_api_v1_projects_user(self):
+        data = {
+            "name": "Dummy name",
+            "bug_tracker": "it is just text"
+        }
+        self._check_api_v1_projects(self.user, data)
+
+        data = {
+            "owner": self.owner.id,
+            "assignee": self.assignee.id,
+            "name": "My import project with data"
+        }
+        self._check_api_v1_projects(self.user, data)
+
+
+    def test_api_v1_projects_observer(self):
+        data = {
+            "name": "My Project #1",
+            "owner": self.owner.id,
+            "assignee": self.assignee.id
+        }
+        self._check_api_v1_projects(self.observer, data)
+
+    def test_api_v1_projects_no_auth(self):
+        data = {
+            "name": "My Project #2",
+            "owner": self.admin.id,
+        }
+        self._check_api_v1_projects(None, data)
+
+class ProjectPartialUpdateAPITestCase(APITestCase):
+    def setUp(self):
+        self.client = APIClient()
+
+    @classmethod
+    def setUpTestData(cls):
+        create_db_users(cls)
+        cls.projects = create_dummy_db_projects(cls)
+
+    def _run_api_v1_projects_id(self, pid, user, data):
+        with ForceLogin(user, self.client):
+            response = self.client.patch('/api/v1/projects/{}'.format(pid),
+                data=data, format="json")
+
+        return response
+
+    def _check_response(self, response, db_project, data):
+        self.assertEqual(response.status_code, status.HTTP_200_OK)
+        name = data.get("name", db_project.name)
+        self.assertEqual(response.data["name"], name)
+        owner = db_project.owner.id if db_project.owner else None
+        owner = data.get("owner", owner)
+        self.assertEqual(response.data["owner"], owner)
+        assignee = db_project.assignee.id if db_project.assignee else None
+        assignee = data.get("assignee", assignee)
+        self.assertEqual(response.data["assignee"], assignee)
+        self.assertEqual(response.data["status"], db_project.status)
+
+    def _check_api_v1_projects_id(self, user, data):
+        for db_project in self.projects:
+            response = self._run_api_v1_projects_id(db_project.id, user, data)
+            if user and user.has_perm("engine.project.change", db_project):
+                self._check_response(response, db_project, data)
+            elif user:
+                self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
+            else:
+                self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
+
+    def test_api_v1_projects_id_admin(self):
+        data = {
+            "name": "new name for the project",
+            "owner": self.owner.id,
+        }
+        self._check_api_v1_projects_id(self.admin, data)
+
+    def test_api_v1_projects_id_user(self):
+        data = {
+            "name": "new name for the project",
+            "owner": self.assignee.id,
+        }
+        self._check_api_v1_projects_id(self.user, data)
+
+    def test_api_v1_projects_id_observer(self):
+        data = {
+            "name": "new name for the project",
+        }
+        self._check_api_v1_projects_id(self.observer, data)
+
+    def test_api_v1_projects_id_no_auth(self):
+        data = {
+            "name": "new name for the project",
+        }
+        self._check_api_v1_projects_id(None, data)
+
+class ProjectListOfTasksAPITestCase(APITestCase):
+    def setUp(self):
+        self.client = APIClient()
+
+    @classmethod
+    def setUpTestData(cls):
+        create_db_users(cls)
+        cls.projects = create_dummy_db_projects(cls)
+
+    def _run_api_v1_projects_id_tasks(self, user, pid):
+        with ForceLogin(user, self.client):
+            response = self.client.get('/api/v1/projects/{}/tasks'.format(pid))
+
+        return response
+
+    def test_api_v1_projects_id_tasks_admin(self):
+        project = self.projects[1]
+        response = self._run_api_v1_projects_id_tasks(self.admin, project.id)
+        self.assertEqual(response.status_code, status.HTTP_200_OK)
+        self.assertListEqual(
+            sorted([task.name for task in project.tasks.all()]),
+            sorted([res["name"] for res in response.data["results"]]))
+
+    def test_api_v1_projects_id_tasks_user(self):
+        project = self.projects[1]
+        response = self._run_api_v1_projects_id_tasks(self.user, project.id)
+        self.assertEqual(response.status_code, status.HTTP_200_OK)
+        self.assertListEqual(
+            sorted([task.name for task in project.tasks.all()
+                if  task.owner in [None, self.user] or
+                    task.assignee in [None, self.user]]),
+            sorted([res["name"] for res in response.data["results"]]))
+
+    def test_api_v1_projects_id_tasks_observer(self):
+        project = self.projects[1]
+        response = self._run_api_v1_projects_id_tasks(self.observer, project.id)
+        self.assertEqual(response.status_code, status.HTTP_200_OK)
+        self.assertListEqual(
+            sorted([task.name for task in project.tasks.all()]),
+            sorted([res["name"] for res in response.data["results"]]))
+
+    def test_api_v1_projects_id_tasks_no_auth(self):
+        project = self.projects[1]
+        response = self._run_api_v1_projects_id_tasks(None, project.id)
+        self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
+
+
 class TaskListAPITestCase(APITestCase):
     def setUp(self):
         self.client = APIClient()
diff --git a/cvat/apps/engine/urls.py b/cvat/apps/engine/urls.py
index 534d72b7b5e4..3abde35ff06c 100644
--- a/cvat/apps/engine/urls.py
+++ b/cvat/apps/engine/urls.py
@@ -24,6 +24,7 @@
 )
 
 router = routers.DefaultRouter(trailing_slash=False)
+router.register('projects', views.ProjectViewSet)
 router.register('tasks', views.TaskViewSet)
 router.register('jobs', views.JobViewSet)
 router.register('users', views.UserViewSet)
diff --git a/cvat/apps/engine/views.py b/cvat/apps/engine/views.py
index 08bd89c5b094..2f548c3777e4 100644
--- a/cvat/apps/engine/views.py
+++ b/cvat/apps/engine/views.py
@@ -36,7 +36,8 @@
 from cvat.apps.engine.serializers import (TaskSerializer, UserSerializer,
    ExceptionSerializer, AboutSerializer, JobSerializer, ImageMetaSerializer,
    RqStatusSerializer, TaskDataSerializer, LabeledDataSerializer,
-   PluginSerializer, FileInfoSerializer, LogEventSerializer)
+   PluginSerializer, FileInfoSerializer, LogEventSerializer,
+   ProjectSerializer)
 from cvat.apps.annotation.serializers import AnnotationFileSerializer
 from django.contrib.auth.models import User
 from django.core.exceptions import ObjectDoesNotExist
@@ -158,7 +159,65 @@ def formats(request):
         data = get_annotation_formats()
         return Response(data)
 
+class ProjectFilter(filters.FilterSet):
+    name = filters.CharFilter(field_name="name", lookup_expr="icontains")
+    owner = filters.CharFilter(field_name="owner__username", lookup_expr="icontains")
+    status = filters.CharFilter(field_name="status", lookup_expr="icontains")
+    assignee = filters.CharFilter(field_name="assignee__username", lookup_expr="icontains")
+
+    class Meta:
+        model = models.Project
+        fields = ("id", "name", "owner", "status", "assignee")
+
+class ProjectViewSet(auth.ProjectGetQuerySetMixin, viewsets.ModelViewSet):
+    queryset = models.Project.objects.all().order_by('-id')
+    serializer_class = ProjectSerializer
+    search_fields = ("name", "owner__username", "assignee__username", "status")
+    filterset_class = ProjectFilter
+    ordering_fields = ("id", "name", "owner", "status", "assignee")
+    http_method_names = ['get', 'post', 'head', 'patch', 'delete']
+
+    def get_permissions(self):
+        http_method = self.request.method
+        permissions = [IsAuthenticated]
+
+        if http_method in SAFE_METHODS:
+            permissions.append(auth.ProjectAccessPermission)
+        elif http_method in ["POST"]:
+            permissions.append(auth.ProjectCreatePermission)
+        elif http_method in ["PATCH"]:
+            permissions.append(auth.ProjectChangePermission)
+        elif http_method in ["DELETE"]:
+            permissions.append(auth.ProjectDeletePermission)
+        else:
+            permissions.append(auth.AdminRolePermission)
+
+        return [perm() for perm in permissions]
+
+    def perform_create(self, serializer):
+        if self.request.data.get('owner', None):
+            serializer.save()
+        else:
+            serializer.save(owner=self.request.user)
+
+    @action(detail=True, methods=['GET'], serializer_class=TaskSerializer)
+    def tasks(self, request, pk):
+        self.get_object() # force to call check_object_permissions
+        queryset = Task.objects.filter(project_id=pk).order_by('-id')
+        queryset = auth.filter_task_queryset(queryset, request.user)
+
+        page = self.paginate_queryset(queryset)
+        if page is not None:
+            serializer = self.get_serializer(page, many=True,
+                context={"request": request})
+            return self.get_paginated_response(serializer.data)
+
+        serializer = self.get_serializer(queryset, many=True,
+            context={"request": request})
+        return Response(serializer.data)
+
 class TaskFilter(filters.FilterSet):
+    project = filters.CharFilter(field_name="project__name", lookup_expr="icontains")
     name = filters.CharFilter(field_name="name", lookup_expr="icontains")
     owner = filters.CharFilter(field_name="owner__username", lookup_expr="icontains")
     mode = filters.CharFilter(field_name="mode", lookup_expr="icontains")
@@ -167,7 +226,8 @@ class TaskFilter(filters.FilterSet):
 
     class Meta:
         model = Task
-        fields = ("id", "name", "owner", "mode", "status", "assignee")
+        fields = ("id", "project_id", "project", "name", "owner", "mode", "status",
+            "assignee")
 
 class TaskViewSet(auth.TaskGetQuerySetMixin, viewsets.ModelViewSet):
     queryset = Task.objects.all().prefetch_related(