diff --git a/backend/src/baserow/api/v0/__init__.py b/backend/src/baserow/api/v0/__init__.py
index 0b51eadc4..50a568ebf 100644
--- a/backend/src/baserow/api/v0/__init__.py
+++ b/backend/src/baserow/api/v0/__init__.py
@@ -1 +1 @@
-app_name = 'baserow.api.v0'
+default_app_config = 'baserow.api.v0.config.ApiConfig'
diff --git a/backend/src/baserow/api/v0/applications/__init__.py b/backend/src/baserow/api/v0/applications/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/backend/src/baserow/api/v0/applications/errors.py b/backend/src/baserow/api/v0/applications/errors.py
new file mode 100644
index 000000000..a6d2b0e6d
--- /dev/null
+++ b/backend/src/baserow/api/v0/applications/errors.py
@@ -0,0 +1 @@
+ERROR_USER_NOT_IN_GROUP = 'ERROR_USER_NOT_IN_GROUP'
diff --git a/backend/src/baserow/api/v0/applications/serializers.py b/backend/src/baserow/api/v0/applications/serializers.py
new file mode 100644
index 000000000..fe73d1b67
--- /dev/null
+++ b/backend/src/baserow/api/v0/applications/serializers.py
@@ -0,0 +1,35 @@
+from rest_framework import serializers
+
+from baserow.core.applications import registry
+from baserow.core.models import Application
+
+
+class ApplicationSerializer(serializers.ModelSerializer):
+    type = serializers.SerializerMethodField()
+
+    class Meta:
+        model = Application
+        fields = ('id', 'name', 'order', 'type')
+        extra_kwargs = {
+            'id': {
+                'read_only': True
+            }
+        }
+
+    def get_type(self, instance):
+        application = registry.get_by_model(instance.specific_class)
+        return application.type
+
+
+class ApplicationCreateSerializer(serializers.ModelSerializer):
+    type = serializers.ChoiceField(choices=registry.get_types())
+
+    class Meta:
+        model = Application
+        fields = ('name', 'type')
+
+
+class ApplicationUpdateSerializer(serializers.ModelSerializer):
+    class Meta:
+        model = Application
+        fields = ('name',)
diff --git a/backend/src/baserow/api/v0/applications/urls.py b/backend/src/baserow/api/v0/applications/urls.py
new file mode 100644
index 000000000..bdb9f24b1
--- /dev/null
+++ b/backend/src/baserow/api/v0/applications/urls.py
@@ -0,0 +1,11 @@
+from django.conf.urls import url
+
+from .views import ApplicationsView, ApplicationView
+
+
+app_name = 'baserow.api.v0.group'
+
+urlpatterns = [
+    url(r'group/(?P<group_id>[0-9]+)/$', ApplicationsView.as_view(), name='list'),
+    url(r'(?P<application_id>[0-9]+)/$', ApplicationView.as_view(), name='item'),
+]
diff --git a/backend/src/baserow/api/v0/applications/views.py b/backend/src/baserow/api/v0/applications/views.py
new file mode 100644
index 000000000..f5fd73311
--- /dev/null
+++ b/backend/src/baserow/api/v0/applications/views.py
@@ -0,0 +1,89 @@
+from django.shortcuts import get_object_or_404
+from django.db import transaction
+
+from rest_framework.views import APIView
+from rest_framework.response import Response
+from rest_framework.permissions import IsAuthenticated
+
+from baserow.api.v0.decorators import validate_body, map_exceptions
+from baserow.core.models import GroupUser, Application
+from baserow.core.handler import CoreHandler
+from baserow.core.exceptions import UserNotIngroupError
+
+from .serializers import (
+    ApplicationSerializer, ApplicationCreateSerializer, ApplicationUpdateSerializer
+)
+from .errors import ERROR_USER_NOT_IN_GROUP
+
+
+class ApplicationsView(APIView):
+    permission_classes = (IsAuthenticated,)
+    core_handler = CoreHandler()
+
+    def load_group(self, request, group_id):
+        return get_object_or_404(
+            GroupUser.objects.select_related('group'),
+            group_id=group_id,
+            user=request.user
+        )
+
+    def get(self, request, group_id):
+        """
+        Responds with a list of applications that belong to the group if the user has
+        access to that group.
+        """
+
+        group_user = self.load_group(request, group_id)
+        applications = Application.objects.filter(
+            group=group_user.group
+        ).select_related('content_type')
+        serializer = ApplicationSerializer(applications, many=True)
+        return Response(serializer.data)
+
+    @transaction.atomic
+    @validate_body(ApplicationCreateSerializer)
+    def post(self, request, data, group_id):
+        """Creates a new application for a user."""
+
+        group_user = self.load_group(request, group_id)
+        application = self.core_handler.create_application(
+            request.user, group_user.group, data['type'], name=data['name'])
+
+        return Response(ApplicationSerializer(application).data)
+
+
+class ApplicationView(APIView):
+    permission_classes = (IsAuthenticated,)
+    core_handler = CoreHandler()
+
+    @transaction.atomic
+    @validate_body(ApplicationUpdateSerializer)
+    @map_exceptions({
+        UserNotIngroupError: ERROR_USER_NOT_IN_GROUP
+    })
+    def patch(self, request, data, application_id):
+        """Updates the application if the user belongs to the group."""
+
+        application = get_object_or_404(
+            Application.objects.select_related('group').select_for_update(),
+            pk=application_id
+        )
+        application = self.core_handler.update_application(
+            request.user, application, name=data['name'])
+
+        return Response(ApplicationSerializer(application).data)
+
+    @transaction.atomic
+    @map_exceptions({
+        UserNotIngroupError: ERROR_USER_NOT_IN_GROUP
+    })
+    def delete(self, request, application_id):
+        """Deletes an existing application if the user belongs to the group."""
+
+        application = get_object_or_404(
+            Application.objects.select_related('group'),
+            pk=application_id
+        )
+        self.core_handler.delete_application(request.user, application)
+
+        return Response(status=204)
diff --git a/backend/src/baserow/api/v0/apps.py b/backend/src/baserow/api/v0/config.py
similarity index 100%
rename from backend/src/baserow/api/v0/apps.py
rename to backend/src/baserow/api/v0/config.py
diff --git a/backend/src/baserow/api/v0/decorators.py b/backend/src/baserow/api/v0/decorators.py
index 4e3857594..d909decfd 100644
--- a/backend/src/baserow/api/v0/decorators.py
+++ b/backend/src/baserow/api/v0/decorators.py
@@ -1,9 +1,15 @@
+from collections import defaultdict
+
+from django.utils.encoding import force_text
+
 from rest_framework import status
 from rest_framework.exceptions import APIException
+from rest_framework.request import Request
 
 
 def map_exceptions(exceptions):
-    """This decorator easily maps specific exceptions to a standard api response.
+    """
+    This decorator simplifies mapping specific exceptions to a standard api response.
 
     Example:
       @map_exceptions({ SomeException: 'ERROR_1' })
@@ -54,3 +60,70 @@ def map_exceptions(exceptions):
                 raise exc
         return func_wrapper
     return map_exceptions_decorator
+
+
+def validate_body(serializer_class):
+    """
+    This decorator can validate the request body using a serializer. If the body is
+    valid it will add the data to the kwargs. If not it will raise an APIException with
+    structured details about what is wrong.
+
+    Example:
+        class LoginSerializer(serializers.Serializer):
+            username = serializers.EmailField()
+            password = serializers.CharField()
+
+        @validate_body(LoginSerializer)
+        def post(self, request):
+           raise SomeException('This is a test')
+
+        HTTP/1.1 400
+        {
+          "error": "ERROR_REQUEST_BODY_VALIDATION",
+          "detail": {
+            "username": [
+              {
+                "error": "This field is required.",
+                "code": "required"
+              }
+            ]
+          }
+        }
+
+    :param serializer_class: The serializer that must be used for validating.
+    :type serializer_class: Serializer
+    """
+
+    def validate_decorator(func):
+        def func_wrapper(*args, **kwargs):
+            # Check if the request
+            if len(args) < 2 or not isinstance(args[1], Request):
+                raise ValueError('There must be a request in the kwargs.')
+
+            request = args[1]
+            serializer = serializer_class(data=request.data)
+            if not serializer.is_valid():
+                # Create a serialized detail on why the validation failed.
+                detail = defaultdict(list)
+                for key, errors in serializer.errors.items():
+                    for error in errors:
+                        detail[key].append({
+                            'error': force_text(error),
+                            'code': error.code
+                        })
+
+                exc = APIException({
+                    'error': 'ERROR_REQUEST_BODY_VALIDATION',
+                    'detail': detail
+                })
+                exc.status_code = 400
+                raise exc
+
+            # We do not want to override already existing data value in the kwargs.
+            if 'data' in kwargs:
+                raise ValueError('The data attribute is already in the kwargs.')
+
+            kwargs['data'] = serializer.data
+            return func(*args, **kwargs)
+        return func_wrapper
+    return validate_decorator
diff --git a/backend/src/baserow/api/v0/groups/views.py b/backend/src/baserow/api/v0/groups/views.py
index 3167910ac..da5784191 100644
--- a/backend/src/baserow/api/v0/groups/views.py
+++ b/backend/src/baserow/api/v0/groups/views.py
@@ -5,6 +5,7 @@ from rest_framework.views import APIView
 from rest_framework.response import Response
 from rest_framework.permissions import IsAuthenticated
 
+from baserow.api.v0.decorators import validate_body
 from baserow.core.models import GroupUser
 from baserow.core.handler import CoreHandler
 
@@ -17,19 +18,17 @@ class GroupsView(APIView):
 
     def get(self, request):
         """Responds with a list of groups where the users takes part in."""
+
         groups = GroupUser.objects.filter(user=request.user).select_related('group')
         serializer = GroupUserSerializer(groups, many=True)
         return Response(serializer.data)
 
     @transaction.atomic
-    def post(self, request):
+    @validate_body(GroupSerializer)
+    def post(self, request, data):
         """Creates a new group for a user."""
-        serializer = GroupSerializer(data=request.data)
-        serializer.is_valid(raise_exception=True)
 
-        data = serializer.data
         group_user = self.core_handler.create_group(request.user, name=data['name'])
-
         return Response(GroupUserSerializer(group_user).data)
 
 
@@ -38,14 +37,16 @@ class GroupView(APIView):
     core_handler = CoreHandler()
 
     @transaction.atomic
-    def patch(self, request, group_id):
+    @validate_body(GroupSerializer)
+    def patch(self, request, data, group_id):
         """Updates the group if it belongs to a user."""
-        group_user = get_object_or_404(GroupUser, group_id=group_id, user=request.user)
 
-        serializer = GroupSerializer(data=request.data)
-        serializer.is_valid(raise_exception=True)
+        group_user = get_object_or_404(
+            GroupUser.objects.select_for_update(),
+            group_id=group_id,
+            user=request.user
+        )
 
-        data = serializer.data
         group_user.group = self.core_handler.update_group(
             request.user,  group_user.group, name=data['name'])
 
@@ -54,6 +55,7 @@ class GroupView(APIView):
     @transaction.atomic
     def delete(self, request, group_id):
         """Deletes an existing group if it belongs to a user."""
+
         group_user = get_object_or_404(GroupUser, group_id=group_id, user=request.user)
         self.core_handler.delete_group(request.user,  group_user.group)
         return Response(status=204)
@@ -63,11 +65,9 @@ class GroupOrderView(APIView):
     permission_classes = (IsAuthenticated,)
     core_handler = CoreHandler()
 
-    def post(self, request):
+    @validate_body(OrderGroupsSerializer)
+    def post(self, request, data):
         """Updates to order of some groups for a user."""
-        serializer = OrderGroupsSerializer(data=request.data)
-        serializer.is_valid(raise_exception=True)
-
-        self.core_handler.order_groups(request.user, serializer.data['groups'])
 
+        self.core_handler.order_groups(request.user, data['groups'])
         return Response(status=204)
diff --git a/backend/src/baserow/api/v0/urls.py b/backend/src/baserow/api/v0/urls.py
index e434d41b2..7f33d5da6 100644
--- a/backend/src/baserow/api/v0/urls.py
+++ b/backend/src/baserow/api/v0/urls.py
@@ -1,12 +1,16 @@
 from django.urls import path, include
 
+from baserow.core.applications import registry
+
 from .user import urls as user_urls
 from .groups import urls as group_urls
+from .applications import urls as application_urls
 
 
 app_name = 'baserow.api.v0'
 
 urlpatterns = [
     path('user/', include(user_urls, namespace='user')),
-    path('groups/', include(group_urls, namespace='groups'))
-]
+    path('groups/', include(group_urls, namespace='groups')),
+    path('applications/', include(application_urls, namespace='applications'))
+] + registry.api_urls
diff --git a/backend/src/baserow/api/v0/user/errors.py b/backend/src/baserow/api/v0/user/errors.py
new file mode 100644
index 000000000..238292a17
--- /dev/null
+++ b/backend/src/baserow/api/v0/user/errors.py
@@ -0,0 +1 @@
+ERROR_ALREADY_EXISTS = 'ERROR_ALREADY_EXISTS'
diff --git a/backend/src/baserow/api/v0/user/views.py b/backend/src/baserow/api/v0/user/views.py
index 15a1d85f3..db72bdc9a 100644
--- a/backend/src/baserow/api/v0/user/views.py
+++ b/backend/src/baserow/api/v0/user/views.py
@@ -5,12 +5,12 @@ from rest_framework.response import Response
 from rest_framework.permissions import AllowAny
 from rest_framework_jwt.settings import api_settings
 
-from baserow.api.v0.decorators import map_exceptions
+from baserow.api.v0.decorators import map_exceptions, validate_body
 from baserow.user.handler import UserHandler
 from baserow.user.exceptions import UserAlreadyExist
 
-
 from .serializers import RegisterSerializer, UserSerializer
+from .errors import ERROR_ALREADY_EXISTS
 
 
 jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER
@@ -23,13 +23,12 @@ class UserView(APIView):
 
     @transaction.atomic
     @map_exceptions({
-        UserAlreadyExist: 'ERROR_ALREADY_EXISTS'
+        UserAlreadyExist: ERROR_ALREADY_EXISTS
     })
-    def post(self, request):
-        serializer = RegisterSerializer(data=request.data)
-        serializer.is_valid(raise_exception=True)
+    @validate_body(RegisterSerializer)
+    def post(self, request, data):
+        """Registers a new user."""
 
-        data = serializer.data
         user = self.user_handler.create_user(name=data['name'], email=data['email'],
                                              password=data['password'])
 
diff --git a/backend/src/baserow/config/settings/base.py b/backend/src/baserow/config/settings/base.py
index aae81bca5..ba7c57766 100644
--- a/backend/src/baserow/config/settings/base.py
+++ b/backend/src/baserow/config/settings/base.py
@@ -26,7 +26,8 @@ INSTALLED_APPS = [
     'corsheaders',
 
     'baserow.core',
-    'baserow.api.v0'
+    'baserow.api.v0',
+    'baserow.contrib.database'
 ]
 
 MIDDLEWARE = [
diff --git a/backend/src/baserow/contrib/__init__.py b/backend/src/baserow/contrib/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/backend/src/baserow/contrib/database/__init__.py b/backend/src/baserow/contrib/database/__init__.py
new file mode 100644
index 000000000..f0b024840
--- /dev/null
+++ b/backend/src/baserow/contrib/database/__init__.py
@@ -0,0 +1 @@
+default_app_config = 'baserow.contrib.database.config.DatabaseConfig'
diff --git a/backend/src/baserow/contrib/database/api_urls.py b/backend/src/baserow/contrib/database/api_urls.py
new file mode 100644
index 000000000..916b115e0
--- /dev/null
+++ b/backend/src/baserow/contrib/database/api_urls.py
@@ -0,0 +1,5 @@
+app_name = 'baserow.contrib.database'
+
+urlpatterns = [
+
+]
diff --git a/backend/src/baserow/contrib/database/applications.py b/backend/src/baserow/contrib/database/applications.py
new file mode 100644
index 000000000..327f4deae
--- /dev/null
+++ b/backend/src/baserow/contrib/database/applications.py
@@ -0,0 +1,16 @@
+from django.urls import path, include
+
+from baserow.core.applications import Application
+
+from . import api_urls
+from .models import Database
+
+
+class DatabaseApplication(Application):
+    type = 'database'
+    instance_model = Database
+
+    def get_api_urls(self):
+        return [
+            path('database/', include(api_urls, namespace=self.type)),
+        ]
diff --git a/backend/src/baserow/contrib/database/config.py b/backend/src/baserow/contrib/database/config.py
new file mode 100644
index 000000000..4679e3159
--- /dev/null
+++ b/backend/src/baserow/contrib/database/config.py
@@ -0,0 +1,11 @@
+from django.apps import AppConfig
+
+from baserow.core.applications import registry
+
+
+class DatabaseConfig(AppConfig):
+    name = 'baserow.contrib.database'
+
+    def ready(self):
+        from .applications import DatabaseApplication
+        registry.register(DatabaseApplication())
diff --git a/backend/src/baserow/contrib/database/migrations/0001_initial.py b/backend/src/baserow/contrib/database/migrations/0001_initial.py
new file mode 100644
index 000000000..1012e38fd
--- /dev/null
+++ b/backend/src/baserow/contrib/database/migrations/0001_initial.py
@@ -0,0 +1,46 @@
+# Generated by Django 2.2.2 on 2019-09-13 12:08
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+    initial = True
+
+    dependencies = [
+        ('core', '0002_application'),
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name='Database',
+            fields=[
+                ('application_ptr', models.OneToOneField(
+                    auto_created=True,
+                    on_delete=django.db.models.deletion.CASCADE,
+                    parent_link=True,
+                    primary_key=True,
+                    serialize=False,
+                    to='core.Application')
+                 ),
+            ],
+            bases=('core.application',),
+        ),
+        migrations.CreateModel(
+            name='Table',
+            fields=[
+                ('id', models.AutoField(
+                    auto_created=True,
+                    primary_key=True,
+                    serialize=False,
+                    verbose_name='ID')
+                 ),
+                ('order', models.PositiveIntegerField()),
+                ('group', models.ForeignKey(
+                    on_delete=django.db.models.deletion.CASCADE,
+                    to='database.Database')
+                 ),
+            ],
+        ),
+    ]
diff --git a/backend/src/baserow/contrib/database/migrations/__init__.py b/backend/src/baserow/contrib/database/migrations/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/backend/src/baserow/contrib/database/models.py b/backend/src/baserow/contrib/database/models.py
new file mode 100644
index 000000000..344526c13
--- /dev/null
+++ b/backend/src/baserow/contrib/database/models.py
@@ -0,0 +1,12 @@
+from django.db import models
+
+from baserow.core.models import Application
+
+
+class Database(Application):
+    pass
+
+
+class Table(models.Model):
+    group = models.ForeignKey(Database, on_delete=models.CASCADE)
+    order = models.PositiveIntegerField()
diff --git a/backend/src/baserow/core/__init__.py b/backend/src/baserow/core/__init__.py
index 1a371c919..feef96834 100644
--- a/backend/src/baserow/core/__init__.py
+++ b/backend/src/baserow/core/__init__.py
@@ -1 +1 @@
-app_name = 'baserow.group'
+default_app_config = 'baserow.core.config.CoreConfig'
diff --git a/backend/src/baserow/core/applications.py b/backend/src/baserow/core/applications.py
new file mode 100644
index 000000000..221689d42
--- /dev/null
+++ b/backend/src/baserow/core/applications.py
@@ -0,0 +1,175 @@
+from django.core.exceptions import ImproperlyConfigured
+
+from .exceptions import ApplicationAlreadyRegistered, ApplicationTypeDoesNotExist
+
+
+class Application(object):
+    """
+    This abstract class represents a custom application that can be added to the
+    application registry. It must be extended so customisation can be done. Each
+    application will have his own model that must extend the Application model, this is
+    needed so that the user can set custom settings per application instance he has
+    created.
+
+    Example:
+        from baserow.core.models import Application as ApplicationModel
+        from baserow.core.applications import Application, registry
+
+        class ExampleApplicationModel(ApplicationModel):
+            pass
+
+        class ExampleApplication(Application):
+            type = 'a-unique-type-name'
+            instance_model = ExampleApplicationModel
+
+        registry.register(ExampleApplication())
+
+    """
+
+    type = None
+    instance_model = None
+
+    def __init__(self):
+        if not self.type:
+            raise ImproperlyConfigured('The type of an application must be set.')
+
+        if not self.instance_model:
+            raise ImproperlyConfigured('The instance model of an application must be '
+                                       'set.')
+
+    def get_api_urls(self):
+        """
+        If needed custom api related urls to the application can be added here.
+
+        Example:
+
+            def get_api_urls(self):
+                from . import api_urls
+
+                return [
+                    path('some-application/', include(api_urls, namespace=self.type)),
+                ]
+
+            # api_urls.py
+            from django.conf.urls import url
+
+            urlpatterns = [
+              url(r'some-view^$', SomeView.as_view(), name='some_view'),
+            ]
+
+        :return: A list containing the urls.
+        :rtype: list
+        """
+
+        return []
+
+
+class ApplicationRegistry(object):
+    """
+    With the application registry it is possible to register new applications.  An
+    application is an abstraction made specifically for Baserow. If added to the
+    registry a user can create new instances of that application via the ap and register
+    api related urls.
+    """
+
+    def __init__(self):
+        self.registry = {}
+
+    def get(self, type):
+        """
+        Returns a registered application their type name.
+
+        :param type: The type name of the registered application.
+        :param type: str
+        :return: The requested application.
+        :rtype: Application
+        """
+
+        if type not in self.registry:
+            raise ApplicationTypeDoesNotExist(f'The application type {type} does not '
+                                              f'exist.')
+
+        return self.registry[type]
+
+    def get_by_model(self, instance):
+        """Returns the application instance of a model or model instance.
+
+        :param instance: The modal that must be the applications model_instance.
+        :type instance: Model or an instance of model.
+        :return: The registered application instance.
+        :rtype: Application
+        """
+
+        for value in self.registry.values():
+            if value.instance_model == instance \
+               or isinstance(instance, value.instance_model):
+                return value
+
+        raise ApplicationTypeDoesNotExist(f'The application with model instance '
+                                          f'{instance} does not exist. ')
+
+    def get_types(self):
+        """
+        Returns a list of available type names.
+
+        :return: A list of available types.
+        :rtype: List
+        """
+
+        return list(self.registry.keys())
+
+    def register(self, application):
+        """
+        Registers a new application in the registry.
+
+        :param application: The application that needs to be registered.
+        :type application:
+        """
+
+        if not isinstance(application, Application):
+            raise ValueError('The application must be an instance of Application.')
+
+        if application.type in self.registry:
+            raise ApplicationAlreadyRegistered(
+                f'The application with type {application.type} is already registered.')
+
+        self.registry[application.type] = application
+
+    def unregister(self, value):
+        """
+        Removes a registered application from the registry. An application instance or
+        type name can be provided as value.
+
+        :param value: The application instance or type name.
+        :type value: Application or str
+        """
+
+        if isinstance(value, Application):
+            for type, application in self.registry.items():
+                if application == value:
+                    value = type
+
+        if isinstance(value, str):
+            del self.registry[value]
+        else:
+            raise ValueError('The value must either be an application instance or type '
+                             'name')
+
+    @property
+    def api_urls(self):
+        """
+        Returns a list of all the api urls that are in the registered applications.
+
+        :return: The api urls of the registered applications.
+        :rtype: list
+        """
+
+        api_urls = []
+        for application in self.registry.values():
+            api_urls += application.get_api_urls()
+        return api_urls
+
+
+# A default application is created here, this is the one that is used throughout the
+# whole Baserow application. To add a new application use this registry.
+registry = ApplicationRegistry()
diff --git a/backend/src/baserow/core/apps.py b/backend/src/baserow/core/config.py
similarity index 68%
rename from backend/src/baserow/core/apps.py
rename to backend/src/baserow/core/config.py
index b2ef0f46f..7e99c3758 100644
--- a/backend/src/baserow/core/apps.py
+++ b/backend/src/baserow/core/config.py
@@ -1,5 +1,5 @@
 from django.apps import AppConfig
 
 
-class ApiConfig(AppConfig):
+class CoreConfig(AppConfig):
     name = 'baserow.core'
diff --git a/backend/src/baserow/core/exceptions.py b/backend/src/baserow/core/exceptions.py
index 6a8d1ffa0..1b585adf8 100644
--- a/backend/src/baserow/core/exceptions.py
+++ b/backend/src/baserow/core/exceptions.py
@@ -1,2 +1,10 @@
 class UserNotIngroupError(Exception):
     pass
+
+
+class ApplicationAlreadyRegistered(Exception):
+    pass
+
+
+class ApplicationTypeDoesNotExist(Exception):
+    pass
diff --git a/backend/src/baserow/core/handler.py b/backend/src/baserow/core/handler.py
index cb8071b98..7e3b853a2 100644
--- a/backend/src/baserow/core/handler.py
+++ b/backend/src/baserow/core/handler.py
@@ -1,10 +1,13 @@
-from .models import Group, GroupUser
+from .models import Group, GroupUser, Application
 from .exceptions import UserNotIngroupError
+from .utils import extract_allowed, set_allowed_attrs
+from .applications import registry
 
 
 class CoreHandler:
     def create_group(self, user, **kwargs):
-        """Creates a new group for an existing user.
+        """
+        Creates a new group for an existing user.
 
         :param user: The user that must be in the group.
         :type user: User
@@ -12,13 +15,7 @@ class CoreHandler:
         :rtype: GroupUser
         """
 
-        allowed_fields = ['name']
-
-        group_values = {}
-        for field in allowed_fields:
-            if field in allowed_fields:
-                group_values[field] = kwargs[field]
-
+        group_values = extract_allowed(kwargs, ['name'])
         group = Group.objects.create(**group_values)
         last_order = GroupUser.get_last_order(user)
         group_user = GroupUser.objects.create(group=group, user=user, order=last_order)
@@ -26,36 +23,41 @@ class CoreHandler:
         return group_user
 
     def update_group(self, user, group, **kwargs):
-        """Updates fields of a group.
-
-        :param user:
-        :param group:
-        :return:
         """
+        Updates the values of a group.
+
+        :param user: The user on whose behalf the change is made.
+        :type user: User
+        :param group: The group instance that must be updated.
+        :type group: Group
+        :return: The updated group
+        :rtype: Group
+        """
+
+        if not isinstance(group, Group):
+            raise ValueError('The group is not an instance of Group.')
 
         if not group.has_user(user):
             raise UserNotIngroupError(f'The user {user} does not belong to the group '
                                       f'{group}.')
 
-        allowed_fields = ['name']
-
-        for field in allowed_fields:
-            if field in kwargs:
-                setattr(group, field, kwargs[field])
-
+        group = set_allowed_attrs(kwargs, ['name'], group)
         group.save()
 
         return group
 
     def delete_group(self, user, group):
-        """Deletes an existing group.
-
-        :param user:
-        :type: user: User
-        :param group:
-        :type: group: Group
-        :return:
         """
+        Deletes an existing group.
+
+        :param user: The user on whose behalf the delete is done.
+        :type: user: User
+        :param group: The group instance that must be deleted.
+        :type: group: Group
+        """
+
+        if not isinstance(group, Group):
+            raise ValueError('The group is not an instance of Group.')
 
         if not group.has_user(user):
             raise UserNotIngroupError(f'The user {user} does not belong to the group '
@@ -64,9 +66,10 @@ class CoreHandler:
         group.delete()
 
     def order_groups(self, user, group_ids):
-        """Changes the order of groups for a user.
+        """
+        Changes the order of groups for a user.
 
-        :param user:
+        :param user: The user on whose behalf the ordering is done.
         :type: user: User
         :param group_ids: A list of group ids ordered the way they need to be ordered.
         :type group_ids: List[int]
@@ -77,3 +80,81 @@ class CoreHandler:
                 user=user,
                 group_id=group_id
             ).update(order=index + 1)
+
+    def create_application(self, user, group, type, **kwargs):
+        """
+        Creates a new application based on the provided type.
+
+        :param user: The user on whose behalf the application is created.
+        :type user: User
+        :param group: The group that the application instance belongs to.
+        :type group: Group
+        :param type: The type name of the application. Application can be registered via
+                     the ApplicationRegistry.
+        :type type: str
+        :param kwargs: The fields that need to be set upon creation.
+        :type kwargs: object
+        :return: The created application instance.
+        :rtype: Application
+        """
+
+        if not group.has_user(user):
+            raise UserNotIngroupError(f'The user {user} does not belong to the group '
+                                      f'{group}.')
+
+        # Figure out which model is used for the given application type.
+        application = registry.get(type)
+        model = application.instance_model
+        application_values = extract_allowed(kwargs, ['name'])
+
+        if 'order' not in application_values:
+            application_values['order'] = model.get_last_order(group)
+
+        instance = model.objects.create(group=group, **application_values)
+
+        return instance
+
+    def update_application(self, user, application, **kwargs):
+        """
+        Updates an existing application instance.
+
+        :param user: The user on whose behalf the application is updated.
+        :type user: User
+        :param application: The application instance that needs to be updated.
+        :type application: Application
+        :param kwargs: The fields that need to be updated.
+        :type kwargs: object
+        :return: The updated application instance.
+        :rtype: Application
+        """
+
+        if not isinstance(application, Application):
+            raise ValueError('The application is not an instance of Application')
+
+        if not application.group.has_user(user):
+            raise UserNotIngroupError(f'The user {user} does not belong to the group '
+                                      f'{application.group}.')
+
+        application = set_allowed_attrs(kwargs, ['name'], application)
+        application.save()
+
+        return application
+
+    def delete_application(self, user, application):
+        """
+        Deletes an existing application instance.
+
+        :param user: The user on whose behalf the application is deleted.
+        :type user: User
+        :param application: The application instance that needs to be deleted.
+        :type application: Application
+        """
+
+        if not isinstance(application, Application):
+            raise ValueError('The application is not an instance of Application')
+
+        if not application.group.has_user(user):
+            raise UserNotIngroupError(f'The user {user} does not belong to the group '
+                                      f'{application.group}.')
+
+        application.delete()
diff --git a/backend/src/baserow/core/migrations/0002_application.py b/backend/src/baserow/core/migrations/0002_application.py
new file mode 100644
index 000000000..5661ba577
--- /dev/null
+++ b/backend/src/baserow/core/migrations/0002_application.py
@@ -0,0 +1,38 @@
+# Generated by Django 2.2.2 on 2019-09-13 12:08
+
+import baserow.core.mixins
+import baserow.core.models
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('contenttypes', '0002_remove_content_type_name'),
+        ('core', '0001_initial'),
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name='Application',
+            fields=[
+                ('id', models.AutoField(auto_created=True, primary_key=True,
+                                        serialize=False, verbose_name='ID')),
+                ('name', models.CharField(max_length=50)),
+                ('order', models.PositiveIntegerField()),
+                ('content_type', models.ForeignKey(
+                    on_delete=models.SET(
+                        baserow.core.models.get_default_application_content_type),
+                    related_name='applications', to='contenttypes.ContentType',
+                    verbose_name='content type')
+                 ),
+                ('group', models.ForeignKey(
+                    on_delete=django.db.models.deletion.CASCADE, to='core.Group')),
+            ],
+            options={
+                'ordering': ('order',),
+            },
+            bases=(baserow.core.mixins.OrderableMixin, models.Model),
+        ),
+    ]
diff --git a/backend/src/baserow/core/mixins.py b/backend/src/baserow/core/mixins.py
new file mode 100644
index 000000000..26926daf1
--- /dev/null
+++ b/backend/src/baserow/core/mixins.py
@@ -0,0 +1,20 @@
+from django.db import models
+
+
+class OrderableMixin:
+    """
+    This mixin introduces a set of helpers of the model is orderable by a field.
+    """
+
+    @classmethod
+    def get_highest_order_of_queryset(cls, queryset, field='order'):
+        """
+
+        :param queryset:
+        :param field:
+        :return:
+        """
+
+        return queryset.aggregate(
+            models.Max(field)
+        ).get(f'{field}__max', 0) or 0
diff --git a/backend/src/baserow/core/models.py b/backend/src/baserow/core/models.py
index b4e833b4c..7d0299c20 100644
--- a/backend/src/baserow/core/models.py
+++ b/backend/src/baserow/core/models.py
@@ -1,12 +1,19 @@
 from django.db import models
 from django.contrib.auth import get_user_model
+from django.contrib.contenttypes.models import ContentType
+from django.utils.functional import cached_property
 
 from .managers import GroupQuerySet
+from .mixins import OrderableMixin
 
 
 User = get_user_model()
 
 
+def get_default_application_content_type():
+    return ContentType.objects.get_for_model(Application)
+
+
 class Group(models.Model):
     name = models.CharField(max_length=100)
     users = models.ManyToManyField(User, through='GroupUser')
@@ -15,13 +22,14 @@ class Group(models.Model):
 
     def has_user(self, user):
         """Returns true is the user belongs to the group."""
+
         return self.users.filter(id=user.id).exists()
 
     def __str__(self):
         return f'<Group id={self.id}, name={self.name}>'
 
 
-class GroupUser(models.Model):
+class GroupUser(OrderableMixin, models.Model):
     user = models.ForeignKey(User, on_delete=models.CASCADE)
     group = models.ForeignKey(Group, on_delete=models.CASCADE)
     order = models.PositiveIntegerField()
@@ -31,11 +39,54 @@ class GroupUser(models.Model):
 
     @classmethod
     def get_last_order(cls, user):
-        """Returns a new position that will be last for a new group."""
-        highest_order = cls.objects.filter(
-            user=user
-        ).aggregate(
-            models.Max('order')
-        ).get('order__max', 0) or 0
+        return cls.get_highest_order_of_queryset(cls.objects.filter(user=user)) + 1
 
-        return highest_order + 1
+
+class Application(OrderableMixin, models.Model):
+    group = models.ForeignKey(Group, on_delete=models.CASCADE)
+    name = models.CharField(max_length=50)
+    order = models.PositiveIntegerField()
+    content_type = models.ForeignKey(
+        ContentType,
+        verbose_name='content type',
+        related_name='applications',
+        on_delete=models.SET(get_default_application_content_type)
+    )
+
+    class Meta:
+        ordering = ('order',)
+
+    def __init__(self, *args, **kwargs):
+        super().__init__(*args, **kwargs)
+
+        if not self.id:
+            if not self.content_type_id:
+                self.content_type = ContentType.objects.get_for_model(self)
+
+    @cached_property
+    def specific(self):
+        """Return this page in its most specific subclassed form."""
+
+        content_type = ContentType.objects.get_for_id(self.content_type_id)
+        model_class = self.specific_class
+        if model_class is None:
+            return self
+        elif isinstance(self, model_class):
+            return self
+        else:
+            return content_type.get_object_for_this_type(id=self.id)
+
+    @cached_property
+    def specific_class(self):
+        """
+        Return the class that this application would be if instantiated in its
+        most specific form
+        """
+
+        content_type = ContentType.objects.get_for_id(self.content_type_id)
+        return content_type.model_class()
+
+    @classmethod
+    def get_last_order(cls, group):
+        return cls.get_highest_order_of_queryset(
+            Application.objects.filter(group=group)) + 1
diff --git a/backend/src/baserow/core/utils.py b/backend/src/baserow/core/utils.py
new file mode 100644
index 000000000..0ad6022cd
--- /dev/null
+++ b/backend/src/baserow/core/utils.py
@@ -0,0 +1,67 @@
+def extract_allowed(values, allowed_fields):
+    """
+    Returns a new dict with the values of the key names that are in the allowed_fields.
+    The other keys are ignored.
+
+    Example:
+        object_1 = {
+            'value_1': 'value',
+            'value_2': 'value'
+        }
+
+        extract_allowed(object_1, ['value_1'])
+        >> {'value_1': 'value'}
+
+    :param values: A dict containing the values.
+    :type dict:
+    :param allowed_fields: A list containing the keys of the values that need to be
+                           extracted from the values.
+    :type allowed_fields: list
+    :return: The extracted values.
+    :rtype: dict
+    """
+
+    allowed_values = {}
+    for field in allowed_fields:
+        if field in values:
+            allowed_values[field] = values[field]
+
+    return allowed_values
+
+
+def set_allowed_attrs(values, allowed_fields, instance):
+    """
+    Sets the attributes of the instance with the values of the key names that are in the
+    allowed_fields. The other keys are ignored.
+
+    Examples:
+        class Tmp(object):
+            value_1 = 'value'
+            value_2 = 'value'
+
+        object_1 = {
+            'value_1': 'value_2',
+            'value_2': 'value_2'
+        }
+
+        tmp = set_allowed_attrs(object_1, ['value_1'], Tmp())
+        tmp.value_1
+        >> 'value_2'
+        tmp.value_2
+        >> 'value'
+
+    :param values: The dict containing the values.
+    :type values: dict
+    :param allowed_fields: A list containing the keys of the value that need to be set
+                           on the instance.
+    :type allowed_fields: list
+    :param instance: The instance of which the attributes must be updated.
+    :type instance: object
+    :return: The updated instance.
+    """
+
+    for field in allowed_fields:
+        if field in values:
+            setattr(instance, field, values[field])
+
+    return instance
diff --git a/backend/tests/baserow/api/v0/applications/test_application_views.py b/backend/tests/baserow/api/v0/applications/test_application_views.py
new file mode 100644
index 000000000..7f0a8272d
--- /dev/null
+++ b/backend/tests/baserow/api/v0/applications/test_application_views.py
@@ -0,0 +1,165 @@
+import pytest
+
+from django.shortcuts import reverse
+
+from baserow.contrib.database.models import Database
+
+
+@pytest.mark.django_db
+def test_list_applications(api_client, data_fixture):
+    user, token = data_fixture.create_user_and_token(
+        email='test@test.nl', password='password', first_name='Test1')
+    group_1 = data_fixture.create_group(user=user)
+    group_2 = data_fixture.create_group()
+    application_1 = data_fixture.create_database_application(group=group_1, order=1)
+    application_2 = data_fixture.create_database_application(group=group_1, order=3)
+    application_3 = data_fixture.create_database_application(group=group_1, order=2)
+    data_fixture.create_database_application(group=group_2, order=1)
+
+    response = api_client.get(
+        reverse('api_v0:applications:list', kwargs={'group_id': group_1.id}), **{
+            'HTTP_AUTHORIZATION': f'JWT {token}'
+        }
+    )
+    assert response.status_code == 200
+    response_json = response.json()
+
+    assert len(response_json) == 3
+
+    assert response_json[0]['id'] == application_1.id
+    assert response_json[0]['type'] == 'database'
+
+    assert response_json[1]['id'] == application_3.id
+    assert response_json[1]['type'] == 'database'
+
+    assert response_json[2]['id'] == application_2.id
+    assert response_json[2]['type'] == 'database'
+
+    response = api_client.get(
+        reverse('api_v0:applications:list', kwargs={'group_id': group_2.id}), **{
+            'HTTP_AUTHORIZATION': f'JWT {token}'
+        }
+    )
+    assert response.status_code == 404
+
+
+@pytest.mark.django_db
+def test_create_application(api_client, data_fixture):
+    user, token = data_fixture.create_user_and_token()
+    user_2, token_2 = data_fixture.create_user_and_token()
+    group = data_fixture.create_group(user=user)
+    group_2 = data_fixture.create_group(user=user_2)
+
+    response = api_client.post(
+        reverse('api_v0:applications:list', kwargs={'group_id': group.id}),
+        {
+            'name': 'Test 1',
+            'type': 'NOT_EXISTING'
+        },
+        format='json',
+        HTTP_AUTHORIZATION=f'JWT {token}'
+    )
+    response_json = response.json()
+    assert response.status_code == 400
+    assert response_json['error'] == 'ERROR_REQUEST_BODY_VALIDATION'
+    assert response_json['detail']['type'][0]['code'] == 'invalid_choice'
+
+    response = api_client.post(
+        reverse('api_v0:applications:list', kwargs={'group_id': group_2.id}),
+        {
+            'name': 'Test 1',
+            'type': 'database'
+        },
+        format='json',
+        HTTP_AUTHORIZATION=f'JWT {token}'
+    )
+    assert response.status_code == 404
+
+    response = api_client.post(
+        reverse('api_v0:applications:list', kwargs={'group_id': group.id}),
+        {
+            'name': 'Test 1',
+            'type': 'database'
+        },
+        format='json',
+        HTTP_AUTHORIZATION=f'JWT {token}'
+    )
+    response_json = response.json()
+    assert response.status_code == 200
+    assert response_json['type'] == 'database'
+
+    database = Database.objects.filter()[0]
+    assert response_json['id'] == database.id
+    assert response_json['name'] == database.name
+    assert response_json['order'] == database.order
+
+
+@pytest.mark.django_db
+def test_update_application(api_client, data_fixture):
+    user, token = data_fixture.create_user_and_token()
+    user_2, token_2 = data_fixture.create_user_and_token()
+    group = data_fixture.create_group(user=user)
+    group_2 = data_fixture.create_group(user=user_2)
+    application = data_fixture.create_database_application(group=group)
+    application_2 = data_fixture.create_database_application(group=group_2)
+
+    url = reverse('api_v0:applications:item',
+                  kwargs={'application_id': application_2.id})
+    response = api_client.patch(
+        url,
+        {'name': 'Test 1'},
+        format='json',
+        HTTP_AUTHORIZATION=f'JWT {token}'
+    )
+    response_json = response.json()
+    assert response.status_code == 400
+    assert response_json['error'] == 'ERROR_USER_NOT_IN_GROUP'
+
+    url = reverse('api_v0:applications:item', kwargs={'application_id': application.id})
+    response = api_client.patch(
+        url,
+        {'UNKNOWN_FIELD': 'Test 1'},
+        format='json',
+        HTTP_AUTHORIZATION=f'JWT {token}'
+    )
+    response_json = response.json()
+    assert response.status_code == 400
+    assert response_json['error'] == 'ERROR_REQUEST_BODY_VALIDATION'
+
+    url = reverse('api_v0:applications:item', kwargs={'application_id': application.id})
+    response = api_client.patch(
+        url,
+        {'name': 'Test 1'},
+        format='json',
+        HTTP_AUTHORIZATION=f'JWT {token}'
+    )
+    response_json = response.json()
+    assert response.status_code == 200
+    assert response_json['id'] == application.id
+    assert response_json['name'] == 'Test 1'
+
+    application.refresh_from_db()
+    assert application.name == 'Test 1'
+
+
+@pytest.mark.django_db
+def test_delete_application(api_client, data_fixture):
+    user, token = data_fixture.create_user_and_token()
+    user_2, token_2 = data_fixture.create_user_and_token()
+    group = data_fixture.create_group(user=user)
+    group_2 = data_fixture.create_group(user=user_2)
+    application = data_fixture.create_database_application(group=group)
+    application_2 = data_fixture.create_database_application(group=group_2)
+
+    url = reverse('api_v0:applications:item',
+                  kwargs={'application_id': application_2.id})
+    response = api_client.delete(url, HTTP_AUTHORIZATION=f'JWT {token}')
+    response_json = response.json()
+    assert response.status_code == 400
+    assert response_json['error'] == 'ERROR_USER_NOT_IN_GROUP'
+
+    url = reverse('api_v0:applications:item', kwargs={'application_id': application.id})
+    response = api_client.delete(url, HTTP_AUTHORIZATION=f'JWT {token}')
+    assert response.status_code == 204
+
+    assert Database.objects.all().count() == 1
diff --git a/backend/tests/baserow/api/v0/group/test_group_views.py b/backend/tests/baserow/api/v0/groups/test_group_views.py
similarity index 93%
rename from backend/tests/baserow/api/v0/group/test_group_views.py
rename to backend/tests/baserow/api/v0/groups/test_group_views.py
index a5aeb1af8..e015d1759 100644
--- a/backend/tests/baserow/api/v0/group/test_group_views.py
+++ b/backend/tests/baserow/api/v0/groups/test_group_views.py
@@ -9,17 +9,17 @@ from baserow.core.models import Group, GroupUser
 def test_list_groups(api_client, data_fixture):
     user, token = data_fixture.create_user_and_token(
         email='test@test.nl', password='password', first_name='Test1')
-    group_2 = data_fixture.create_user_group(user=user, order=2)
-    group_1 = data_fixture.create_user_group(user=user, order=1)
+    user_group_2 = data_fixture.create_user_group(user=user, order=2)
+    user_group_1 = data_fixture.create_user_group(user=user, order=1)
 
     response = api_client.get(reverse('api_v0:groups:list'), **{
         'HTTP_AUTHORIZATION': f'JWT {token}'
     })
     assert response.status_code == 200
     response_json = response.json()
-    assert response_json[0]['id'] == group_1.id
+    assert response_json[0]['id'] == user_group_1.group.id
     assert response_json[0]['order'] == 1
-    assert response_json[1]['id'] == group_2.id
+    assert response_json[1]['id'] == user_group_2.group.id
     assert response_json[1]['order'] == 2
 
 
diff --git a/backend/tests/baserow/api/v0/test_api_decorators.py b/backend/tests/baserow/api/v0/test_api_decorators.py
new file mode 100644
index 000000000..b83533cf4
--- /dev/null
+++ b/backend/tests/baserow/api/v0/test_api_decorators.py
@@ -0,0 +1,106 @@
+import pytest
+import json
+
+from unittest.mock import MagicMock
+
+from django.http.request import HttpRequest
+
+from rest_framework import status, serializers
+from rest_framework.request import Request
+from rest_framework.parsers import JSONParser
+from rest_framework.exceptions import APIException
+from rest_framework.test import APIRequestFactory
+
+from baserow.api.v0.decorators import map_exceptions, validate_body
+
+
+class TemporaryException(Exception):
+    pass
+
+
+class TemporarySerializer(serializers.Serializer):
+    field_1 = serializers.CharField()
+    field_2 = serializers.ChoiceField(choices=('choice_1', 'choice_2'))
+
+
+def test_map_exceptions():
+    @map_exceptions({
+        TemporaryException: 'ERROR_TEMPORARY'
+    })
+    def test_1():
+        raise TemporaryException
+
+    with pytest.raises(APIException) as api_exception_1:
+        test_1()
+
+    assert api_exception_1.value.detail['error'] == 'ERROR_TEMPORARY'
+    assert api_exception_1.value.detail['detail'] == ''
+    assert api_exception_1.value.status_code == status.HTTP_400_BAD_REQUEST
+
+    @map_exceptions({
+        TemporaryException: ('ERROR_TEMPORARY_2', 404, 'Another message')
+    })
+    def test_2():
+        raise TemporaryException
+
+    with pytest.raises(APIException) as api_exception_2:
+        test_2()
+
+    assert api_exception_2.value.detail['error'] == 'ERROR_TEMPORARY_2'
+    assert api_exception_2.value.detail['detail'] == 'Another message'
+    assert api_exception_2.value.status_code == status.HTTP_404_NOT_FOUND
+
+    @map_exceptions({
+        TemporaryException: 'ERROR_TEMPORARY_3'
+    })
+    def test_3():
+        pass
+
+    test_3()
+
+
+def test_validate_body():
+    factory = APIRequestFactory()
+
+    request = Request(factory.post(
+        '/some-page/',
+        data=json.dumps({'field_1': 'test'}),
+        content_type='application/json'
+    ), parsers=[JSONParser()])
+    func = MagicMock()
+
+    with pytest.raises(APIException) as api_exception_1:
+        validate_body(TemporarySerializer)(func)(*[object, request])
+
+    assert api_exception_1.value.detail['error'] == 'ERROR_REQUEST_BODY_VALIDATION'
+    assert api_exception_1.value.detail['detail']['field_2'][0]['error'] == \
+           'This field is required.'
+    assert api_exception_1.value.detail['detail']['field_2'][0]['code'] == 'required'
+    assert api_exception_1.value.status_code == status.HTTP_400_BAD_REQUEST
+
+    request = Request(factory.post(
+        '/some-page/',
+        data=json.dumps({'field_1': 'test', 'field_2': 'wrong'}),
+        content_type='application/json'
+    ), parsers=[JSONParser()])
+    func = MagicMock()
+
+    with pytest.raises(APIException) as api_exception_1:
+        validate_body(TemporarySerializer)(func)(*[object, request])
+
+    assert api_exception_1.value.detail['error'] == 'ERROR_REQUEST_BODY_VALIDATION'
+    assert api_exception_1.value.detail['detail']['field_2'][0]['error'] == \
+        '"wrong" is not a valid choice.'
+    assert api_exception_1.value.detail['detail']['field_2'][0]['code'] == \
+        'invalid_choice'
+    assert api_exception_1.value.status_code == status.HTTP_400_BAD_REQUEST
+
+    request = Request(factory.post(
+        '/some-page/',
+        data=json.dumps({'field_1': 'test', 'field_2': 'choice_1'}),
+        content_type='application/json'
+    ), parsers=[JSONParser()])
+    func = MagicMock()
+
+    validate_body(TemporarySerializer)(func)(*[object, request])
+
diff --git a/backend/tests/baserow/api/v0/user/test_token_auth.py b/backend/tests/baserow/api/v0/users/test_token_auth.py
similarity index 100%
rename from backend/tests/baserow/api/v0/user/test_token_auth.py
rename to backend/tests/baserow/api/v0/users/test_token_auth.py
diff --git a/backend/tests/baserow/api/v0/user/test_user_views.py b/backend/tests/baserow/api/v0/users/test_user_views.py
similarity index 100%
rename from backend/tests/baserow/api/v0/user/test_user_views.py
rename to backend/tests/baserow/api/v0/users/test_user_views.py
diff --git a/backend/tests/baserow/core/test_core_applications.py b/backend/tests/baserow/core/test_core_applications.py
new file mode 100644
index 000000000..72cdc8073
--- /dev/null
+++ b/backend/tests/baserow/core/test_core_applications.py
@@ -0,0 +1,88 @@
+import pytest
+
+from baserow.core.applications import Application, ApplicationRegistry
+from baserow.core.exceptions import (
+    ApplicationAlreadyRegistered, ApplicationTypeDoesNotExist
+)
+
+
+class FakeModel(object):
+    pass
+
+
+class FakeModel2(object):
+    pass
+
+
+class TemporaryApplication1(Application):
+    type = 'temporary_1'
+    instance_model = FakeModel
+
+    def get_api_urls(self):
+        return ['url_1', 'url_2']
+
+
+class TemporaryApplication2(Application):
+    type = 'temporary_2'
+    instance_model = FakeModel2
+
+    def get_api_urls(self):
+        return ['url_3']
+
+
+def test_application_registry_register():
+    temporary_1 = TemporaryApplication1()
+    temporary_2 = TemporaryApplication2()
+
+    registry = ApplicationRegistry()
+    registry.register(temporary_1)
+    registry.register(temporary_2)
+
+    with pytest.raises(ValueError):
+        registry.register('NOT AN APPLICATION')
+
+    with pytest.raises(ApplicationAlreadyRegistered):
+        registry.register(temporary_1)
+
+    assert len(registry.registry.items()) == 2
+    assert registry.registry['temporary_1'] == temporary_1
+    assert registry.registry['temporary_2'] == temporary_2
+
+    registry.unregister(temporary_1)
+
+    assert len(registry.registry.items()) == 1
+
+    registry.unregister('temporary_2')
+
+    assert len(registry.registry.items()) == 0
+
+    with pytest.raises(ValueError):
+        registry.unregister(000)
+
+
+def test_application_registry_get():
+    temporary_1 = TemporaryApplication1()
+    registry = ApplicationRegistry()
+    registry.register(temporary_1)
+
+    assert registry.get('temporary_1') == temporary_1
+    with pytest.raises(ApplicationTypeDoesNotExist):
+        registry.get('something')
+
+    assert registry.get_by_model(FakeModel) == temporary_1
+    assert registry.get_by_model(FakeModel()) == temporary_1
+    with pytest.raises(ApplicationTypeDoesNotExist):
+        registry.get_by_model(FakeModel2)
+    with pytest.raises(ApplicationTypeDoesNotExist):
+        registry.get_by_model(FakeModel2())
+
+
+def test_application_get_api_urls():
+    temporary_1 = TemporaryApplication1()
+    temporary_2 = TemporaryApplication2()
+
+    registry = ApplicationRegistry()
+    registry.register(temporary_1)
+    registry.register(temporary_2)
+
+    assert registry.api_urls == ['url_1', 'url_2', 'url_3']
diff --git a/backend/tests/baserow/core/test_core_handler.py b/backend/tests/baserow/core/test_core_handler.py
index 8d69d57cc..99408d962 100644
--- a/backend/tests/baserow/core/test_core_handler.py
+++ b/backend/tests/baserow/core/test_core_handler.py
@@ -1,8 +1,9 @@
 import pytest
 
 from baserow.core.handler import CoreHandler
-from baserow.core.models import Group, GroupUser
-from baserow.core.exceptions import UserNotIngroupError
+from baserow.core.models import Group, GroupUser, Application
+from baserow.core.exceptions import UserNotIngroupError, ApplicationTypeDoesNotExist
+from baserow.contrib.database.models import Database
 
 
 @pytest.mark.django_db
@@ -42,6 +43,9 @@ def test_update_group(data_fixture):
     with pytest.raises(UserNotIngroupError):
         handler.update_group(user=user_2, group=group, name='New name')
 
+    with pytest.raises(ValueError):
+        handler.update_group(user=user_2, group=object(), name='New name')
+
 
 @pytest.mark.django_db
 def test_delete_group(data_fixture):
@@ -66,6 +70,9 @@ def test_delete_group(data_fixture):
     assert Group.objects.all().count() == 1
     assert GroupUser.objects.all().count() == 1
 
+    with pytest.raises(ValueError):
+        handler.delete_group(user=user_2, group=object())
+
 
 @pytest.mark.django_db
 def test_order_groups(data_fixture):
@@ -77,7 +84,7 @@ def test_order_groups(data_fixture):
     assert [1, 2, 3] == [ug_1.order, ug_2.order, ug_3.order]
 
     handler = CoreHandler()
-    handler.order_groups(user, [ug_3.id, ug_2.id, ug_1.id])
+    handler.order_groups(user, [ug_3.group.id, ug_2.group.id, ug_1.group.id])
 
     ug_1.refresh_from_db()
     ug_2.refresh_from_db()
@@ -85,10 +92,76 @@ def test_order_groups(data_fixture):
 
     assert [1, 2, 3] == [ug_3.order, ug_2.order, ug_1.order]
 
-    handler.order_groups(user, [ug_2.id, ug_1.id, ug_3.id])
+    handler.order_groups(user, [ug_2.group.id, ug_1.group.id, ug_3.group.id])
 
     ug_1.refresh_from_db()
     ug_2.refresh_from_db()
     ug_3.refresh_from_db()
 
     assert [1, 2, 3] == [ug_2.order, ug_1.order, ug_3.order]
+
+
+@pytest.mark.django_db
+def test_create_database_application(data_fixture):
+    user = data_fixture.create_user()
+    user_2 = data_fixture.create_user()
+    group = data_fixture.create_group(user=user)
+
+    handler = CoreHandler()
+    handler.create_application(user=user, group=group, type='database',
+                               name='Test database')
+
+    assert Application.objects.all().count() == 1
+    assert Database.objects.all().count() == 1
+
+    database = Database.objects.all().first()
+    assert database.name == 'Test database'
+    assert database.order == 1
+    assert database.group == group
+
+    with pytest.raises(UserNotIngroupError):
+        handler.create_application(user=user_2, group=group, type='database', name='')
+
+    with pytest.raises(ApplicationTypeDoesNotExist):
+        handler.create_application(user=user, group=group, type='UNKNOWN', name='')
+
+
+@pytest.mark.django_db
+def test_update_database_application(data_fixture):
+    user = data_fixture.create_user()
+    user_2 = data_fixture.create_user()
+    group = data_fixture.create_group(user=user)
+    database = data_fixture.create_database_application(group=group)
+
+    handler = CoreHandler()
+
+    with pytest.raises(UserNotIngroupError):
+        handler.update_application(user=user_2, application=database, name='Test 1')
+
+    with pytest.raises(ValueError):
+        handler.update_application(user=user_2, application=object(), name='Test 1')
+
+    handler.update_application(user=user, application=database, name='Test 1')
+
+    database.refresh_from_db()
+
+    assert database.name == 'Test 1'
+
+@pytest.mark.django_db
+def test_delete_database_application(data_fixture):
+    user = data_fixture.create_user()
+    user_2 = data_fixture.create_user()
+    group = data_fixture.create_group(user=user)
+    database = data_fixture.create_database_application(group=group)
+
+    handler = CoreHandler()
+
+    with pytest.raises(UserNotIngroupError):
+        handler.delete_application(user=user_2, application=database)
+
+    with pytest.raises(ValueError):
+        handler.delete_application(user=user_2, application=object())
+
+    assert Database.objects.all().count() == 1
+    handler.delete_application(user=user, application=database)
+    assert Database.objects.all().count() == 0
diff --git a/backend/tests/baserow/core/test_core_managers.py b/backend/tests/baserow/core/test_core_managers.py
index aeea1da2d..a5048297b 100644
--- a/backend/tests/baserow/core/test_core_managers.py
+++ b/backend/tests/baserow/core/test_core_managers.py
@@ -6,19 +6,20 @@ from baserow.core.models import Group
 @pytest.mark.django_db
 def test_groups_of_user(data_fixture):
     user_1 = data_fixture.create_user()
-    group_user_1 = data_fixture.create_user_group(user=user_1, order=1)
-    group_user_2 = data_fixture.create_user_group(user=user_1, order=2)
-    group_user_3 = data_fixture.create_user_group(user=user_1, order=0)
+    user_group_1 = data_fixture.create_user_group(user=user_1, order=1)
+    user_group_2 = data_fixture.create_user_group(user=user_1, order=2)
+    user_group_3 = data_fixture.create_user_group(user=user_1, order=0)
 
     user_2 = data_fixture.create_user()
-    group_user_4 = data_fixture.create_user_group(user=user_2, order=0)
+    user_group_4 = data_fixture.create_user_group(user=user_2, order=0)
 
     groups_user_1 = Group.objects.of_user(user=user_1)
     assert len(groups_user_1) == 3
-    assert groups_user_1[0].id == group_user_3.id
-    assert groups_user_1[1].id == group_user_1.id
-    assert groups_user_1[2].id == group_user_2.id
+
+    assert groups_user_1[0].id == user_group_3.group.id
+    assert groups_user_1[1].id == user_group_1.group.id
+    assert groups_user_1[2].id == user_group_2.group.id
 
     groups_user_2 = Group.objects.of_user(user=user_2)
     assert len(groups_user_2) == 1
-    assert groups_user_2[0].id == group_user_4.id
+    assert groups_user_2[0].id == user_group_4.group.id
diff --git a/backend/tests/baserow/core/test_core_models.py b/backend/tests/baserow/core/test_core_models.py
index 4abcf464d..f40e152db 100644
--- a/backend/tests/baserow/core/test_core_models.py
+++ b/backend/tests/baserow/core/test_core_models.py
@@ -1,6 +1,7 @@
 import pytest
 
 from baserow.core.models import GroupUser
+from baserow.contrib.database.models import Database
 
 
 @pytest.mark.django_db
@@ -24,3 +25,12 @@ def test_group_has_user(data_fixture):
 
     assert user_group.group.has_user(user_group.user)
     assert not user_group.group.has_user(user)
+
+
+@pytest.mark.django_db
+def test_application_content_type_init(data_fixture):
+    group = data_fixture.create_group()
+    database = Database.objects.create(name='Test 1', order=0, group=group)
+
+    assert database.content_type.app_label == 'database'
+    assert database.content_type.model == 'database'
diff --git a/backend/tests/baserow/core/test_core_utils.py b/backend/tests/baserow/core/test_core_utils.py
new file mode 100644
index 000000000..1728b15c5
--- /dev/null
+++ b/backend/tests/baserow/core/test_core_utils.py
@@ -0,0 +1,30 @@
+from baserow.core.utils import extract_allowed, set_allowed_attrs
+
+
+def test_extract_allowed():
+    assert extract_allowed({
+        'test_1': 'test_1',
+        'test_2': 'test_2'
+    }, ['test_1']) == {
+        'test_1': 'test_1'
+    }
+
+    assert extract_allowed({}, ['test_1']) == {}
+    assert extract_allowed({'test_1': 'test'}, ['test_2']) == {}
+    assert extract_allowed({'test_1': 'test'}, []) == {}
+
+
+def test_set_allowed_attrs():
+    class Tmp(object):
+        test_1 = None
+        test_2 = None
+
+    tmp1 = Tmp()
+    tmp1 = set_allowed_attrs(
+        {'test_1': 'test', 'test_2': 'test'},
+        ['test_1'],
+        tmp1
+    )
+
+    assert tmp1.test_1 == 'test'
+    assert tmp1.test_2 is None
diff --git a/backend/tests/fixtures/__init__.py b/backend/tests/fixtures/__init__.py
index 13a51aaef..a336658ad 100644
--- a/backend/tests/fixtures/__init__.py
+++ b/backend/tests/fixtures/__init__.py
@@ -2,7 +2,8 @@ from faker import Faker
 
 from .user import UserFixtures
 from .group import GroupFixtures
+from .application import ApplicationFixtures
 
 
-class Fixtures(UserFixtures, GroupFixtures):
+class Fixtures(UserFixtures, GroupFixtures, ApplicationFixtures):
     fake = Faker()
diff --git a/backend/tests/fixtures/application.py b/backend/tests/fixtures/application.py
new file mode 100644
index 000000000..142382d62
--- /dev/null
+++ b/backend/tests/fixtures/application.py
@@ -0,0 +1,15 @@
+from baserow.contrib.database.models import Database
+
+
+class ApplicationFixtures:
+    def create_database_application(self, **kwargs):
+        if 'group' not in kwargs:
+            kwargs['group'] = self.create_group()
+
+        if 'name' not in kwargs:
+            kwargs['name'] = self.fake.name()
+
+        if 'order' not in kwargs:
+            kwargs['order'] = 0
+
+        return Database.objects.create(**kwargs)