diff --git a/backend/src/baserow/api/v0/applications/serializers.py b/backend/src/baserow/api/v0/applications/serializers.py
index 1f992c925..7c40978bd 100644
--- a/backend/src/baserow/api/v0/applications/serializers.py
+++ b/backend/src/baserow/api/v0/applications/serializers.py
@@ -1,3 +1,5 @@
+from django.utils.functional import lazy
+
 from rest_framework import serializers
 
 from baserow.api.v0.groups.serializers import GroupSerializer
@@ -19,12 +21,17 @@ class ApplicationSerializer(serializers.ModelSerializer):
         }
 
     def get_type(self, instance):
-        application = registry.get_by_model(instance.specific_class)
+        # It could be that the application related to the instance is already in the
+        # context else we can call the specific_class property to find it.
+        application = self.context.get('application')
+        if not application:
+            application = registry.get_by_model(instance.specific_class)
+
         return application.type
 
 
 class ApplicationCreateSerializer(serializers.ModelSerializer):
-    type = serializers.ChoiceField(choices=registry.get_types())
+    type = serializers.ChoiceField(choices=lazy(registry.get_types, list)())
 
     class Meta:
         model = Application
@@ -35,3 +42,23 @@ class ApplicationUpdateSerializer(serializers.ModelSerializer):
     class Meta:
         model = Application
         fields = ('name',)
+
+
+def get_application_serializer(instance, **kwargs):
+    """
+    Returns an instantiated serialized based on the instance class type. Custom
+    serializers can be defined per application. This function will return that one is
+    set else it will return the default one.
+
+    :param instance: The instance where a serializer is needed for.
+    :type instance: Application
+    :return: An instantiated serializer for the instance.
+    :rtype: ApplicationSerializer
+    """
+    application = registry.get_by_model(instance.specific_class)
+    serializer_class = application.instance_serializer
+
+    if not serializer_class:
+        serializer_class = ApplicationSerializer
+
+    return serializer_class(instance, context={'application': application}, **kwargs)
diff --git a/backend/src/baserow/api/v0/applications/views.py b/backend/src/baserow/api/v0/applications/views.py
index f44a76926..5cf35bc9e 100644
--- a/backend/src/baserow/api/v0/applications/views.py
+++ b/backend/src/baserow/api/v0/applications/views.py
@@ -6,21 +6,22 @@ from rest_framework.response import Response
 from rest_framework.permissions import IsAuthenticated
 
 from baserow.api.v0.decorators import validate_body, map_exceptions
+from baserow.api.v0.errors import ERROR_USER_NOT_IN_GROUP
 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
+    ApplicationCreateSerializer, ApplicationUpdateSerializer, get_application_serializer
 )
-from .errors import ERROR_USER_NOT_IN_GROUP
 
 
 class ApplicationsView(APIView):
     permission_classes = (IsAuthenticated,)
     core_handler = CoreHandler()
 
-    def load_group(self, request, group_id):
+    @staticmethod
+    def get_group(request, group_id):
         return get_object_or_404(
             GroupUser.objects.select_related('group'),
             group_id=group_id,
@@ -29,27 +30,30 @@ class ApplicationsView(APIView):
 
     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.
+        Responds with a list of serialized applications that belong to the group if the
+        user has access to that group.
         """
 
-        group_user = self.load_group(request, group_id)
+        group_user = self.get_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)
+        data = [
+            get_application_serializer(application).data
+            for application in applications
+        ]
+        return Response(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)
+        group_user = self.get_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)
+        return Response(get_application_serializer(application).data)
 
 
 class ApplicationView(APIView):
@@ -63,7 +67,7 @@ class ApplicationView(APIView):
             pk=application_id, group__users__in=[request.user]
         )
 
-        return Response(ApplicationSerializer(application).data)
+        return Response(get_application_serializer(application).data)
 
     @transaction.atomic
     @validate_body(ApplicationUpdateSerializer)
@@ -80,7 +84,7 @@ class ApplicationView(APIView):
         application = self.core_handler.update_application(
             request.user, application, name=data['name'])
 
-        return Response(ApplicationSerializer(application).data)
+        return Response(get_application_serializer(application).data)
 
     @transaction.atomic
     @map_exceptions({
diff --git a/backend/src/baserow/api/v0/decorators.py b/backend/src/baserow/api/v0/decorators.py
index d909decfd..f8bbd9cbb 100644
--- a/backend/src/baserow/api/v0/decorators.py
+++ b/backend/src/baserow/api/v0/decorators.py
@@ -40,7 +40,7 @@ def map_exceptions(exceptions):
             except tuple(exceptions.keys()) as e:
                 value = exceptions.get(e.__class__)
                 status_code = status.HTTP_400_BAD_REQUEST
-                detail = str(e)
+                detail = ''
 
                 if isinstance(value, str):
                     error = value
diff --git a/backend/src/baserow/api/v0/applications/errors.py b/backend/src/baserow/api/v0/errors.py
similarity index 100%
rename from backend/src/baserow/api/v0/applications/errors.py
rename to backend/src/baserow/api/v0/errors.py
diff --git a/backend/src/baserow/contrib/database/api/__init__.py b/backend/src/baserow/contrib/database/api/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/backend/src/baserow/contrib/database/api/v0/__init__.py b/backend/src/baserow/contrib/database/api/v0/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/backend/src/baserow/contrib/database/api/v0/serializers.py b/backend/src/baserow/contrib/database/api/v0/serializers.py
new file mode 100644
index 000000000..b2d102b48
--- /dev/null
+++ b/backend/src/baserow/contrib/database/api/v0/serializers.py
@@ -0,0 +1,26 @@
+from rest_framework import serializers
+
+from baserow.api.v0.applications.serializers import ApplicationSerializer
+from baserow.contrib.database.table.models import Table
+from baserow.contrib.database.api.v0.tables.serializers import TableSerializer
+
+
+class DatabaseSerializer(ApplicationSerializer):
+    tables = serializers.SerializerMethodField()
+
+    class Meta(ApplicationSerializer.Meta):
+        fields = ApplicationSerializer.Meta.fields + ('tables',)
+
+    def get_tables(self, instance):
+        """
+        Because the the instance doesn't know at this point it is a Database we have to
+        select the related tables this way.
+
+        :param instance: The database application instance.
+        :type instance: Application
+        :return: A list of serialized tables that belong to this instance.
+        :rtype: list
+        """
+
+        tables = Table.objects.filter(database_id=instance.pk)
+        return TableSerializer(tables, many=True).data
diff --git a/backend/src/baserow/contrib/database/api/v0/tables/__init__.py b/backend/src/baserow/contrib/database/api/v0/tables/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/backend/src/baserow/contrib/database/api/v0/tables/serializers.py b/backend/src/baserow/contrib/database/api/v0/tables/serializers.py
new file mode 100644
index 000000000..5c397cf03
--- /dev/null
+++ b/backend/src/baserow/contrib/database/api/v0/tables/serializers.py
@@ -0,0 +1,20 @@
+from rest_framework import serializers
+
+from baserow.contrib.database.table.models import Table
+
+
+class TableSerializer(serializers.ModelSerializer):
+    class Meta:
+        model = Table
+        fields = ('id', 'name', 'order',)
+        extra_kwargs = {
+            'id': {
+                'read_only': True
+            }
+        }
+
+
+class TableCreateUpdateSerializer(serializers.ModelSerializer):
+    class Meta:
+        model = Table
+        fields = ('name',)
diff --git a/backend/src/baserow/contrib/database/api/v0/tables/urls.py b/backend/src/baserow/contrib/database/api/v0/tables/urls.py
new file mode 100644
index 000000000..8e769becb
--- /dev/null
+++ b/backend/src/baserow/contrib/database/api/v0/tables/urls.py
@@ -0,0 +1,11 @@
+from django.conf.urls import url
+
+from .views import TablesView, TableView
+
+
+app_name = 'baserow.contrib.api.v0.tables'
+
+urlpatterns = [
+    url(r'database/(?P<database_id>[0-9]+)/$', TablesView.as_view(), name='list'),
+    url(r'(?P<table_id>[0-9]+)/$', TableView.as_view(), name='item'),
+]
diff --git a/backend/src/baserow/contrib/database/api/v0/tables/views.py b/backend/src/baserow/contrib/database/api/v0/tables/views.py
new file mode 100644
index 000000000..048011a02
--- /dev/null
+++ b/backend/src/baserow/contrib/database/api/v0/tables/views.py
@@ -0,0 +1,115 @@
+from django.db import transaction
+from django.shortcuts import get_object_or_404
+
+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.api.v0.errors import ERROR_USER_NOT_IN_GROUP
+from baserow.core.exceptions import UserNotIngroupError
+from baserow.contrib.database.models import Database
+from baserow.contrib.database.table.models import Table
+from baserow.contrib.database.table.handler import TableHandler
+
+from .serializers import TableSerializer, TableCreateUpdateSerializer
+
+
+class TablesView(APIView):
+    permission_classes = (IsAuthenticated,)
+    table_handler = TableHandler()
+
+    @staticmethod
+    def get_database(user, database_id):
+        database = get_object_or_404(
+            Database.objects.select_related('group'),
+            pk=database_id
+        )
+
+        if not database.group.has_user(user):
+            raise UserNotIngroupError(f'User {user} doesn\'t belong to group '
+                                      f'{database.group}.')
+
+        return database
+
+    @map_exceptions({
+        UserNotIngroupError: ERROR_USER_NOT_IN_GROUP
+    })
+    def get(self, request, database_id):
+        """Lists all the tables of a database."""
+
+        database = self.get_database(request.user, database_id)
+        tables = Table.objects.filter(database=database)
+        serializer = TableSerializer(tables, many=True)
+        return Response(serializer.data)
+
+    @transaction.atomic
+    @map_exceptions({
+        UserNotIngroupError: ERROR_USER_NOT_IN_GROUP
+    })
+    @validate_body(TableCreateUpdateSerializer)
+    def post(self, request, data, database_id):
+        """Creates a new table in a database."""
+
+        database = self.get_database(request.user, database_id)
+        table = self.table_handler.create_table(
+            request.user, database, name=data['name'])
+        serializer = TableSerializer(table)
+        return Response(serializer.data)
+
+
+class TableView(APIView):
+    permission_classes = (IsAuthenticated,)
+    table_handler = TableHandler()
+
+    @staticmethod
+    def get_table(user, table_id):
+        table = get_object_or_404(
+            Table.objects.select_related('database__group'),
+            pk=table_id
+        )
+
+        if not table.database.group.has_user(user):
+            raise UserNotIngroupError(f'User {user} doesn\'t belong to group '
+                                      f'{table.database.group}.')
+
+        return table
+
+    @map_exceptions({
+        UserNotIngroupError: ERROR_USER_NOT_IN_GROUP
+    })
+    def get(self, request, table_id):
+        """Responds with a serialized table instance."""
+
+        table = self.get_table(request.user, table_id)
+        serializer = TableSerializer(table)
+        return Response(serializer.data)
+
+    @transaction.atomic
+    @map_exceptions({
+        UserNotIngroupError: ERROR_USER_NOT_IN_GROUP
+    })
+    @validate_body(TableCreateUpdateSerializer)
+    def patch(self, request, data, table_id):
+        """Updates the values a table instance."""
+
+        table = self.table_handler.update_table(
+            request.user,
+            self.get_table(request.user, table_id),
+            name=data['name']
+        )
+        serializer = TableSerializer(table)
+        return Response(serializer.data)
+
+    @transaction.atomic
+    @map_exceptions({
+        UserNotIngroupError: ERROR_USER_NOT_IN_GROUP
+    })
+    def delete(self, request, table_id):
+        """Deletes an existing table."""
+
+        self.table_handler.delete_table(
+            request.user,
+            self.get_table(request.user, table_id)
+        )
+        return Response(status=204)
diff --git a/backend/src/baserow/contrib/database/api_urls.py b/backend/src/baserow/contrib/database/api_urls.py
index 916b115e0..357961847 100644
--- a/backend/src/baserow/contrib/database/api_urls.py
+++ b/backend/src/baserow/contrib/database/api_urls.py
@@ -1,5 +1,9 @@
+from django.urls import path, include
+
+from .api.v0.tables import urls as table_urls
+
 app_name = 'baserow.contrib.database'
 
 urlpatterns = [
-
+    path('tables/', include(table_urls, namespace='tables')),
 ]
diff --git a/backend/src/baserow/contrib/database/applications.py b/backend/src/baserow/contrib/database/applications.py
index 327f4deae..3ccd99c8b 100644
--- a/backend/src/baserow/contrib/database/applications.py
+++ b/backend/src/baserow/contrib/database/applications.py
@@ -4,11 +4,13 @@ from baserow.core.applications import Application
 
 from . import api_urls
 from .models import Database
+from .api.v0.serializers import DatabaseSerializer
 
 
 class DatabaseApplication(Application):
     type = 'database'
     instance_model = Database
+    instance_serializer = DatabaseSerializer
 
     def get_api_urls(self):
         return [
diff --git a/backend/src/baserow/contrib/database/migrations/0002_auto_20191018_0740.py b/backend/src/baserow/contrib/database/migrations/0002_auto_20191018_0740.py
new file mode 100644
index 000000000..6eb53c199
--- /dev/null
+++ b/backend/src/baserow/contrib/database/migrations/0002_auto_20191018_0740.py
@@ -0,0 +1,24 @@
+# Generated by Django 2.2.2 on 2019-10-18 07:40
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('database', '0001_initial'),
+    ]
+
+    operations = [
+        migrations.RenameField(
+            model_name='table',
+            old_name='group',
+            new_name='database',
+        ),
+        migrations.AddField(
+            model_name='table',
+            name='name',
+            field=models.CharField(default='', max_length=255),
+            preserve_default=False,
+        ),
+    ]
diff --git a/backend/src/baserow/contrib/database/models.py b/backend/src/baserow/contrib/database/models.py
index 344526c13..c34e677c8 100644
--- a/backend/src/baserow/contrib/database/models.py
+++ b/backend/src/baserow/contrib/database/models.py
@@ -1,12 +1,11 @@
-from django.db import models
-
 from baserow.core.models import Application
 
+from .table.models import Table
+
+__all__ = [
+    'Table'
+]
+
 
 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/contrib/database/table/__init__.py b/backend/src/baserow/contrib/database/table/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/backend/src/baserow/contrib/database/table/handler.py b/backend/src/baserow/contrib/database/table/handler.py
new file mode 100644
index 000000000..6bc21101b
--- /dev/null
+++ b/backend/src/baserow/contrib/database/table/handler.py
@@ -0,0 +1,76 @@
+from baserow.core.exceptions import UserNotIngroupError
+from baserow.core.utils import extract_allowed, set_allowed_attrs
+
+from .models import Table
+
+
+class TableHandler:
+    def create_table(self, user, database, **kwargs):
+        """
+        Creates a new table.
+
+        :param user: The user on whose behalf the table is created.
+        :type user: User
+        :param database: The database that the table instance belongs to.
+        :type database: Database
+        :param kwargs: The fields that need to be set upon creation.
+        :type kwargs: object
+        :return: The created table instance.
+        :rtype: Table
+        """
+
+        if not database.group.has_user(user):
+            raise UserNotIngroupError(f'The user {user} does not belong to the group '
+                                      f'{database.group}.')
+
+        table_values = extract_allowed(kwargs, ['name'])
+        last_order = Table.get_last_order(database)
+        table = Table.objects.create(database=database, order=last_order,
+                                     **table_values)
+
+        return table
+
+    def update_table(self, user, table, **kwargs):
+        """
+        Updates an existing table instance.
+
+        :param user: The user on whose behalf the table is updated.
+        :type user: User
+        :param table: The table instance that needs to be updated.
+        :type table: Table
+        :param kwargs: The fields that need to be updated.
+        :type kwargs: object
+        :return: The updated table instance.
+        :rtype: Table
+        """
+
+        if not isinstance(table, Table):
+            raise ValueError('The table is not an instance of Table')
+
+        if not table.database.group.has_user(user):
+            raise UserNotIngroupError(f'The user {user} does not belong to the group '
+                                      f'{table.database.group}.')
+
+        table = set_allowed_attrs(kwargs, ['name'], table)
+        table.save()
+
+        return table
+
+    def delete_table(self, user, table):
+        """
+        Deletes an existing table instance.
+
+        :param user: The user on whose behalf the table is deleted.
+        :type user: User
+        :param table: The table instance that needs to be deleted.
+        :type table: Table
+        """
+
+        if not isinstance(table, Table):
+            raise ValueError('The table is not an instance of Table')
+
+        if not table.database.group.has_user(user):
+            raise UserNotIngroupError(f'The user {user} does not belong to the group '
+                                      f'{table.database.group}.')
+
+        table.delete()
diff --git a/backend/src/baserow/contrib/database/table/models.py b/backend/src/baserow/contrib/database/table/models.py
new file mode 100644
index 000000000..04b9a0c78
--- /dev/null
+++ b/backend/src/baserow/contrib/database/table/models.py
@@ -0,0 +1,17 @@
+from django.db import models
+
+from baserow.core.mixins import OrderableMixin
+
+
+class Table(OrderableMixin, models.Model):
+    database = models.ForeignKey('database.Database', on_delete=models.CASCADE)
+    order = models.PositiveIntegerField()
+    name = models.CharField(max_length=255)
+
+    class Meta:
+        ordering = ('order',)
+
+    @classmethod
+    def get_last_order(cls, database):
+        queryset = Table.objects.filter(database=database)
+        return cls.get_highest_order_of_queryset(queryset) + 1
diff --git a/backend/src/baserow/core/applications.py b/backend/src/baserow/core/applications.py
index 221689d42..6b2bf7697 100644
--- a/backend/src/baserow/core/applications.py
+++ b/backend/src/baserow/core/applications.py
@@ -27,7 +27,13 @@ class Application(object):
     """
 
     type = None
+    """A unique string that identifies the application."""
+
     instance_model = None
+    """The model instance that is created when adding an application."""
+
+    instance_serializer = None
+    """This serializer that is used to serialize the instance model."""
 
     def __init__(self):
         if not self.type:
@@ -92,7 +98,8 @@ class ApplicationRegistry(object):
         return self.registry[type]
 
     def get_by_model(self, instance):
-        """Returns the application instance of a model or model 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.
diff --git a/backend/src/baserow/core/handler.py b/backend/src/baserow/core/handler.py
index 7e3b853a2..0fef89666 100644
--- a/backend/src/baserow/core/handler.py
+++ b/backend/src/baserow/core/handler.py
@@ -106,11 +106,10 @@ class CoreHandler:
         application = registry.get(type)
         model = application.instance_model
         application_values = extract_allowed(kwargs, ['name'])
+        last_order = model.get_last_order(group)
 
-        if 'order' not in application_values:
-            application_values['order'] = model.get_last_order(group)
-
-        instance = model.objects.create(group=group, **application_values)
+        instance = model.objects.create(group=group, order=last_order,
+                                        **application_values)
 
         return instance
 
diff --git a/backend/src/baserow/core/models.py b/backend/src/baserow/core/models.py
index 7d0299c20..38f92bc2b 100644
--- a/backend/src/baserow/core/models.py
+++ b/backend/src/baserow/core/models.py
@@ -39,7 +39,8 @@ class GroupUser(OrderableMixin, models.Model):
 
     @classmethod
     def get_last_order(cls, user):
-        return cls.get_highest_order_of_queryset(cls.objects.filter(user=user)) + 1
+        queryset = cls.objects.filter(user=user)
+        return cls.get_highest_order_of_queryset(queryset) + 1
 
 
 class Application(OrderableMixin, models.Model):
@@ -88,5 +89,5 @@ class Application(OrderableMixin, models.Model):
 
     @classmethod
     def get_last_order(cls, group):
-        return cls.get_highest_order_of_queryset(
-            Application.objects.filter(group=group)) + 1
+        queryset = Application.objects.filter(group=group)
+        return cls.get_highest_order_of_queryset(queryset) + 1
diff --git a/backend/tests/baserow/contrib/database/api/v0/tables/test_table_views.py b/backend/tests/baserow/contrib/database/api/v0/tables/test_table_views.py
new file mode 100644
index 000000000..631dfb1ad
--- /dev/null
+++ b/backend/tests/baserow/contrib/database/api/v0/tables/test_table_views.py
@@ -0,0 +1,210 @@
+import pytest
+
+from django.shortcuts import reverse
+
+from baserow.contrib.database.table.models import Table
+
+
+@pytest.mark.django_db
+def test_list_tables(api_client, data_fixture):
+    user, token = data_fixture.create_user_and_token()
+    database = data_fixture.create_database_application(user=user)
+    database_2 = data_fixture.create_database_application()
+    table_1 = data_fixture.create_database_table(database=database, order=2)
+    table_2 = data_fixture.create_database_table(database=database, order=1)
+    table_3 = data_fixture.create_database_table(database=database_2)
+
+    url = reverse('api_v0:database:tables:list', kwargs={'database_id': database.id})
+    response = api_client.get(url, HTTP_AUTHORIZATION=f'JWT {token}')
+    response_json = response.json()
+    assert response.status_code == 200
+    assert len(response_json) == 2
+    assert response_json[0]['id'] == table_2.id
+    assert response_json[1]['id'] == table_1.id
+
+    url = reverse('api_v0:database:tables:list', kwargs={'database_id': database_2.id})
+    response = api_client.get(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:database:tables:list', kwargs={'database_id': 9999})
+    response = api_client.get(url, **{
+        'HTTP_AUTHORIZATION': f'JWT {token}'
+    })
+    assert response.status_code == 404
+
+
+@pytest.mark.django_db
+def test_create_table(api_client, data_fixture):
+    user, token = data_fixture.create_user_and_token()
+    database = data_fixture.create_database_application(user=user)
+    database_2 = data_fixture.create_database_application()
+
+    url = reverse('api_v0:database:tables:list', kwargs={'database_id': database.id})
+    response = api_client.post(
+        url,
+        {'name': 'Test 1'},
+        format='json',
+        HTTP_AUTHORIZATION=f'JWT {token}'
+    )
+    assert response.status_code == 200
+    json_response = response.json()
+
+    Table.objects.all().count() == 1
+    table = Table.objects.filter(database=database).first()
+
+    assert table.order == json_response['order'] == 1
+    assert table.name == json_response['name']
+    assert table.id == json_response['id']
+
+    url = reverse('api_v0:database:tables:list', kwargs={'database_id': database_2.id})
+    response = api_client.post(
+        url,
+        {'name': 'Test 1'},
+        format='json',
+        HTTP_AUTHORIZATION=f'JWT {token}'
+    )
+    assert response.status_code == 400
+    assert response.json()['error'] == 'ERROR_USER_NOT_IN_GROUP'
+
+    url = reverse('api_v0:database:tables:list', kwargs={'database_id': database.id})
+    response = api_client.post(
+        url,
+        {'not_a_name': 'Test 1'},
+        format='json',
+        HTTP_AUTHORIZATION=f'JWT {token}'
+    )
+    assert response.status_code == 400
+    assert response.json()['error'] == 'ERROR_REQUEST_BODY_VALIDATION'
+
+    url = reverse('api_v0:database:tables:list', kwargs={'database_id': 9999})
+    response = api_client.post(
+        url,
+        {'name': 'Test 1'},
+        format='json',
+        HTTP_AUTHORIZATION=f'JWT {token}'
+    )
+    assert response.status_code == 404
+
+
+@pytest.mark.django_db
+def test_get_table(api_client, data_fixture):
+    user, token = data_fixture.create_user_and_token()
+    table_1 = data_fixture.create_database_table(user=user)
+    table_2 = data_fixture.create_database_table()
+
+    url = reverse('api_v0:database:tables:item', kwargs={'table_id': table_1.id})
+    response = api_client.get(url, HTTP_AUTHORIZATION=f'JWT {token}')
+    assert response.status_code == 200
+    json_response = response.json()
+    assert json_response['id'] == table_1.id
+    assert json_response['name'] == table_1.name
+    assert json_response['order'] == table_1.order
+
+    url = reverse('api_v0:database:tables:item', kwargs={'table_id': table_2.id})
+    response = api_client.get(url, HTTP_AUTHORIZATION=f'JWT {token}')
+    json_response = response.json()
+    assert response.status_code == 400
+    assert json_response['error'] == 'ERROR_USER_NOT_IN_GROUP'
+
+    url = reverse('api_v0:database:tables:item', kwargs={'table_id': 9999})
+    response = api_client.get(url, HTTP_AUTHORIZATION=f'JWT {token}')
+    assert response.status_code == 404
+
+
+@pytest.mark.django_db
+def test_update_table(api_client, data_fixture):
+    user, token = data_fixture.create_user_and_token()
+    table_1 = data_fixture.create_database_table(user=user)
+    table_2 = data_fixture.create_database_table()
+
+    url = reverse('api_v0:database:tables:item', kwargs={'table_id': table_1.id})
+    response = api_client.patch(
+        url,
+        {'name': 'New name'},
+        format='json',
+        HTTP_AUTHORIZATION=f'JWT {token}'
+    )
+    assert response.status_code == 200
+    response_json = response.json()
+
+    table_1.refresh_from_db()
+
+    assert response_json['id'] == table_1.id
+    assert response_json['name'] == table_1.name == 'New name'
+
+    url = reverse('api_v0:database:tables:item', kwargs={'table_id': table_2.id})
+    response = api_client.patch(
+        url,
+        {'name': 'New name'},
+        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:database:tables:item', kwargs={'table_id': table_1.id})
+    response = api_client.patch(
+        url,
+        {'not_a_name': 'New name'},
+        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:database:tables:item', kwargs={'table_id': 999})
+    response = api_client.patch(
+        url,
+        {'name': 'New name'},
+        format='json',
+        HTTP_AUTHORIZATION=f'JWT {token}'
+    )
+    assert response.status_code == 404
+
+
+@pytest.mark.django_db
+def test_delete_group(api_client, data_fixture):
+    user, token = data_fixture.create_user_and_token()
+    table_1 = data_fixture.create_database_table(user=user)
+    table_2 = data_fixture.create_database_table()
+
+    assert Table.objects.all().count() == 2
+    url = reverse('api_v0:database:tables:item', kwargs={'table_id': table_1.id})
+    response = api_client.delete(url, HTTP_AUTHORIZATION=f'JWT {token}')
+    assert response.status_code == 204
+    assert Table.objects.all().count() == 1
+
+    url = reverse('api_v0:database:tables:item', kwargs={'table_id': table_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:database:tables:item', kwargs={'table_id': 9999})
+    response = api_client.delete(url, HTTP_AUTHORIZATION=f'JWT {token}')
+    assert response.status_code == 404
+
+
+@pytest.mark.django_db
+def test_get_database_application_with_tables(api_client, data_fixture):
+    user, token = data_fixture.create_user_and_token()
+    database = data_fixture.create_database_application(user=user)
+    table_1 = data_fixture.create_database_table(database=database, order=0)
+    table_2 = data_fixture.create_database_table(database=database, order=1)
+    table_3 = data_fixture.create_database_table()
+
+    url = reverse('api_v0:applications:item', kwargs={'application_id': database.id})
+    response = api_client.get(
+        url,
+        format='json',
+        HTTP_AUTHORIZATION=f'JWT {token}'
+    )
+    response_json = response.json()
+    assert response.status_code == 200
+    assert len(response_json['tables']) == 2
+    assert response_json['tables'][0]['id'] == table_1.id
+    assert response_json['tables'][1]['id'] == table_2.id
diff --git a/backend/tests/baserow/contrib/database/table/test_handler.py b/backend/tests/baserow/contrib/database/table/test_handler.py
new file mode 100644
index 000000000..b8a0c0344
--- /dev/null
+++ b/backend/tests/baserow/contrib/database/table/test_handler.py
@@ -0,0 +1,62 @@
+import pytest
+
+from baserow.core.exceptions import UserNotIngroupError
+from baserow.contrib.database.table.models import Table
+from baserow.contrib.database.table.handler import TableHandler
+
+
+@pytest.mark.django_db
+def test_create_database_table(data_fixture):
+    user = data_fixture.create_user()
+    user_2 = data_fixture.create_user()
+    database = data_fixture.create_database_application(user=user)
+
+    handler = TableHandler()
+    handler.create_table(user=user, database=database, name='Test table')
+
+    assert Table.objects.all().count() == 1
+
+    table = Table.objects.all().first()
+    assert table.name == 'Test table'
+    assert table.order == 1
+    assert table.database == database
+
+    with pytest.raises(UserNotIngroupError):
+        handler.create_table(user=user_2, database=database, name='')
+
+
+@pytest.mark.django_db
+def test_update_database_table(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)
+    table = data_fixture.create_database_table(database=database)
+
+    handler = TableHandler()
+
+    with pytest.raises(UserNotIngroupError):
+        handler.update_table(user=user_2, table=table, name='Test 1')
+
+    handler.update_table(user=user, table=table, name='Test 1')
+
+    table.refresh_from_db()
+
+    assert table.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)
+    table = data_fixture.create_database_table(database=database)
+
+    handler = TableHandler()
+
+    with pytest.raises(UserNotIngroupError):
+        handler.delete_table(user=user_2, table=table)
+
+    assert Table.objects.all().count() == 1
+    handler.delete_table(user=user, table=table)
+    assert Table.objects.all().count() == 0
diff --git a/backend/tests/baserow/contrib/database/table/test_models.py b/backend/tests/baserow/contrib/database/table/test_models.py
new file mode 100644
index 000000000..18249749b
--- /dev/null
+++ b/backend/tests/baserow/contrib/database/table/test_models.py
@@ -0,0 +1,15 @@
+import pytest
+
+from baserow.contrib.database.table.models import Table
+
+
+@pytest.mark.django_db
+def test_group_user_get_next_order(data_fixture):
+    database = data_fixture.create_database_application()
+    database_2 = data_fixture.create_database_application()
+    data_fixture.create_database_table(order=1, database=database)
+    data_fixture.create_database_table(order=2, database=database)
+    data_fixture.create_database_table(order=10, database=database_2)
+
+    assert Table.get_last_order(database) == 3
+    assert Table.get_last_order(database_2) == 11
diff --git a/backend/tests/fixtures/__init__.py b/backend/tests/fixtures/__init__.py
index a336658ad..7a566c582 100644
--- a/backend/tests/fixtures/__init__.py
+++ b/backend/tests/fixtures/__init__.py
@@ -3,7 +3,8 @@ from faker import Faker
 from .user import UserFixtures
 from .group import GroupFixtures
 from .application import ApplicationFixtures
+from .table import TableFixtures
 
 
-class Fixtures(UserFixtures, GroupFixtures, ApplicationFixtures):
+class Fixtures(UserFixtures, GroupFixtures, ApplicationFixtures, TableFixtures):
     fake = Faker()
diff --git a/backend/tests/fixtures/application.py b/backend/tests/fixtures/application.py
index 142382d62..01cf8160f 100644
--- a/backend/tests/fixtures/application.py
+++ b/backend/tests/fixtures/application.py
@@ -2,9 +2,9 @@ from baserow.contrib.database.models import Database
 
 
 class ApplicationFixtures:
-    def create_database_application(self, **kwargs):
+    def create_database_application(self, user=None, **kwargs):
         if 'group' not in kwargs:
-            kwargs['group'] = self.create_group()
+            kwargs['group'] = self.create_group(user=user)
 
         if 'name' not in kwargs:
             kwargs['name'] = self.fake.name()
diff --git a/backend/tests/fixtures/table.py b/backend/tests/fixtures/table.py
new file mode 100644
index 000000000..ad1502d25
--- /dev/null
+++ b/backend/tests/fixtures/table.py
@@ -0,0 +1,15 @@
+from baserow.contrib.database.table.models import Table
+
+
+class TableFixtures:
+    def create_database_table(self, user=None, **kwargs):
+        if 'database' not in kwargs:
+            kwargs['database'] = self.create_database_application(user=user)
+
+        if 'name' not in kwargs:
+            kwargs['name'] = self.fake.name()
+
+        if 'order' not in kwargs:
+            kwargs['order'] = 0
+
+        return Table.objects.create(**kwargs)
diff --git a/web-frontend/components/sidebar/SidebarApplication.vue b/web-frontend/components/sidebar/SidebarApplication.vue
index 8a2a63e17..eb8a04871 100644
--- a/web-frontend/components/sidebar/SidebarApplication.vue
+++ b/web-frontend/components/sidebar/SidebarApplication.vue
@@ -95,6 +95,15 @@ export default {
         })
     },
     selectApplication(application) {
+      // If there is no route associated with the application we just change the
+      // selected state.
+      if (application._.type.routeName === null) {
+        this.$store.dispatch('application/select', application)
+        return
+      }
+
+      // If we do have a route, this the related component with that route has to do
+      // the state change.
       this.setLoading(application, true)
 
       this.$nuxt.$router.push(
diff --git a/web-frontend/core/applications.js b/web-frontend/core/applications.js
index 8e7380eff..99b6f2165 100644
--- a/web-frontend/core/applications.js
+++ b/web-frontend/core/applications.js
@@ -72,9 +72,6 @@ export class Application {
     if (this.name === null) {
       throw new Error('The name of an application must be set.')
     }
-    if (this.routeName === null) {
-      throw new Error('The route name of an application must be set.')
-    }
   }
 
   /**
@@ -89,4 +86,14 @@ export class Application {
       hasSelectedSidebarComponent: this.getSelectedSidebarComponent() !== null
     }
   }
+
+  /**
+   * Every time a fresh application object is fetched from the backend, it will
+   * be populated, this is the moment to update some values. Because each
+   * application can have unique properties, they might need to be populated.
+   * This method can be overwritten in order the populate the correct values.
+   */
+  populate(application) {
+    return application
+  }
 }
diff --git a/web-frontend/mixins/application.js b/web-frontend/mixins/application.js
deleted file mode 100644
index d98aa23ba..000000000
--- a/web-frontend/mixins/application.js
+++ /dev/null
@@ -1,30 +0,0 @@
-import { notify404 } from '@/utils/error'
-
-/**
- * This mixin can be used in combination with the component where the
- * application routes to when selected. It will make sure that the application
- * preSelect action is called so that the all the depending information is
- * loaded. If something goes wrong while loading this information it will show a
- * standard error.
- */
-export default {
-  props: {
-    id: {
-      type: Number,
-      required: true
-    }
-  },
-  mounted() {
-    this.$store.dispatch('application/preSelect', this.id).catch(error => {
-      notify404(
-        this.$store.dispatch,
-        error,
-        'Application not found.',
-        "The application with the provided id doesn't exist or you " +
-          "don't have access to it."
-      )
-
-      this.$nuxt.$router.push({ name: 'app' })
-    })
-  }
-}
diff --git a/web-frontend/modules/database/application.js b/web-frontend/modules/database/application.js
index 16a60c2b9..aa70fd5e9 100644
--- a/web-frontend/modules/database/application.js
+++ b/web-frontend/modules/database/application.js
@@ -1,11 +1,16 @@
 import { Application } from '@/core/applications'
 import Sidebar from '@/modules/database/components/Sidebar'
+import { populateTable } from '@/modules/database/store/table'
 
 export class DatabaseApplication extends Application {
-  getType() {
+  static getType() {
     return 'database'
   }
 
+  getType() {
+    return DatabaseApplication.getType()
+  }
+
   getIconClass() {
     return 'database'
   }
@@ -14,11 +19,13 @@ export class DatabaseApplication extends Application {
     return 'Database'
   }
 
-  getRouteName() {
-    return 'application-database'
-  }
-
   getSelectedSidebarComponent() {
     return Sidebar
   }
+
+  populate(application) {
+    const values = super.populate(application)
+    values.tables.forEach((object, index, tables) => populateTable(object))
+    return values
+  }
 }
diff --git a/web-frontend/modules/database/components/Sidebar.vue b/web-frontend/modules/database/components/Sidebar.vue
index 0145d5cf5..e0341fdb5 100644
--- a/web-frontend/modules/database/components/Sidebar.vue
+++ b/web-frontend/modules/database/components/Sidebar.vue
@@ -1,38 +1,36 @@
 <template>
   <div>
     <ul class="tree-subs">
-      <li class="tree-sub active">
-        <a href="#" class="tree-sub-link">@TODO</a>
-        <a
-          class="tree-options"
-          @click="
-            $refs.context.toggle($event.currentTarget, 'bottom', 'right', 0)
-          "
-        >
-          <i class="fas fa-ellipsis-v"></i>
-        </a>
-        <Context ref="context">
-          <div class="context-menu-title">@TODO</div>
-          <ul class="context-menu">
-            <li>
-              <a>
-                <i class="context-menu-icon fas fa-fw fa-pen"></i>
-                Rename
-              </a>
-            </li>
-            <li>
-              <a>
-                <i class="context-menu-icon fas fa-fw fa-trash"></i>
-                Delete
-              </a>
-            </li>
-          </ul>
-        </Context>
-      </li>
+      <SidebarItem
+        v-for="table in application.tables"
+        :key="table.id"
+        :database="application"
+        :table="table"
+      ></SidebarItem>
     </ul>
-    <a href="#" class="tree-sub-add">
+    <a class="tree-sub-add" @click="$refs.createTableModal.show()">
       <i class="fas fa-plus"></i>
       Create table
     </a>
+    <CreateTableModal
+      ref="createTableModal"
+      :application="application"
+    ></CreateTableModal>
   </div>
 </template>
+
+<script>
+import SidebarItem from '@/modules/database/components/table/SidebarItem'
+import CreateTableModal from '@/modules/database/components/table/CreateTableModal'
+
+export default {
+  name: 'Sidebar',
+  components: { SidebarItem, CreateTableModal },
+  props: {
+    application: {
+      type: Object,
+      required: true
+    }
+  }
+}
+</script>
diff --git a/web-frontend/modules/database/components/table/CreateTableModal.vue b/web-frontend/modules/database/components/table/CreateTableModal.vue
new file mode 100644
index 000000000..1de81ec4b
--- /dev/null
+++ b/web-frontend/modules/database/components/table/CreateTableModal.vue
@@ -0,0 +1,55 @@
+<template>
+  <Modal>
+    <h2 class="box-title">Create new table</h2>
+    <TableForm ref="tableForm" @submitted="submitted">
+      <div class="actions">
+        <div class="align-right">
+          <button
+            class="button button-large"
+            :class="{ 'button-loading': loading }"
+            :disabled="loading"
+          >
+            Add table
+          </button>
+        </div>
+      </div>
+    </TableForm>
+  </Modal>
+</template>
+
+<script>
+import TableForm from './TableForm'
+
+import modal from '@/mixins/modal'
+
+export default {
+  name: 'CreateTableModal',
+  components: { TableForm },
+  mixins: [modal],
+  props: {
+    application: {
+      type: Object,
+      required: true
+    }
+  },
+  data() {
+    return {
+      loading: false
+    }
+  },
+  methods: {
+    submitted(values) {
+      this.loading = true
+      this.$store
+        .dispatch('table/create', { database: this.application, values })
+        .then(() => {
+          this.loading = false
+          this.hide()
+        })
+        .catch(() => {
+          this.loading = false
+        })
+    }
+  }
+}
+</script>
diff --git a/web-frontend/modules/database/components/table/SidebarItem.vue b/web-frontend/modules/database/components/table/SidebarItem.vue
new file mode 100644
index 000000000..8b7ebbccb
--- /dev/null
+++ b/web-frontend/modules/database/components/table/SidebarItem.vue
@@ -0,0 +1,99 @@
+<template>
+  <li class="tree-sub" :class="{ active: table._.selected }">
+    <nuxt-link
+      :to="{
+        name: 'database-table',
+        params: {
+          id: database.id,
+          tableId: table.id
+        }
+      }"
+      class="tree-sub-link"
+    >
+      <Editable
+        ref="rename"
+        :value="table.name"
+        @change="renameTable(database, table, $event)"
+      ></Editable>
+    </nuxt-link>
+    <a
+      v-show="!database._.loading"
+      class="tree-options"
+      @click="$refs.context.toggle($event.currentTarget, 'bottom', 'right', 0)"
+    >
+      <i class="fas fa-ellipsis-v"></i>
+    </a>
+    <Context ref="context">
+      <div class="context-menu-title">{{ table.name }}</div>
+      <ul class="context-menu">
+        <li>
+          <a @click="enableRename()">
+            <i class="context-menu-icon fas fa-fw fa-pen"></i>
+            Rename
+          </a>
+        </li>
+        <li>
+          <a @click="deleteTable(database, table)">
+            <i class="context-menu-icon fas fa-fw fa-trash"></i>
+            Delete
+          </a>
+        </li>
+      </ul>
+    </Context>
+  </li>
+</template>
+
+<script>
+export default {
+  name: 'SidebarItem',
+  props: {
+    database: {
+      type: Object,
+      required: true
+    },
+    table: {
+      type: Object,
+      required: true
+    }
+  },
+  methods: {
+    setLoading(database, value) {
+      this.$store.dispatch('application/setItemLoading', {
+        application: database,
+        value
+      })
+    },
+    deleteTable(database, table) {
+      this.$refs.context.hide()
+      this.setLoading(database, true)
+
+      this.$store.dispatch('table/delete', { database, table }).then(() => {
+        this.setLoading(database, false)
+      })
+    },
+    enableRename() {
+      this.$refs.context.hide()
+      this.$refs.rename.edit()
+    },
+    renameTable(database, table, event) {
+      this.setLoading(database, true)
+
+      this.$store
+        .dispatch('table/update', {
+          database,
+          table,
+          values: {
+            name: event.value
+          }
+        })
+        .catch(() => {
+          // If something is going wrong we will reset the original value.
+          this.$refs.rename.set(event.oldValue)
+        })
+        .then(() => {
+          this.setLoading(database, false)
+        })
+    }
+  }
+}
+</script>
diff --git a/web-frontend/modules/database/components/table/TableForm.vue b/web-frontend/modules/database/components/table/TableForm.vue
new file mode 100644
index 000000000..6daeb553c
--- /dev/null
+++ b/web-frontend/modules/database/components/table/TableForm.vue
@@ -0,0 +1,50 @@
+<template>
+  <form @submit.prevent="submit">
+    <div class="control">
+      <label class="control-label">
+        <i class="fas fa-font"></i>
+        Name
+      </label>
+      <div class="control-elements">
+        <input
+          ref="name"
+          v-model="values.name"
+          :class="{ 'input-error': $v.values.name.$error }"
+          type="text"
+          class="input input-large"
+          @blur="$v.values.name.$touch()"
+        />
+        <div v-if="$v.values.name.$error" class="error">
+          This field is required.
+        </div>
+      </div>
+    </div>
+    <slot></slot>
+  </form>
+</template>
+
+<script>
+import { required } from 'vuelidate/lib/validators'
+
+import form from '@/mixins/form'
+
+export default {
+  name: 'TableForm',
+  mixins: [form],
+  data() {
+    return {
+      values: {
+        name: ''
+      }
+    }
+  },
+  validations: {
+    values: {
+      name: { required }
+    }
+  },
+  mounted() {
+    this.$refs.name.focus()
+  }
+}
+</script>
diff --git a/web-frontend/modules/database/module.js b/web-frontend/modules/database/module.js
index ab867ce18..4bc5deb47 100644
--- a/web-frontend/modules/database/module.js
+++ b/web-frontend/modules/database/module.js
@@ -9,6 +9,7 @@ export default function DatabaseModule(options) {
     filename: 'plugin.js'
   })
 
+  // Add all the related routes.
   this.extendRoutes(routes => {
     routes.push(...databaseRoutes)
   })
diff --git a/web-frontend/modules/database/pages/Database.vue b/web-frontend/modules/database/pages/Database.vue
deleted file mode 100644
index a1017bf21..000000000
--- a/web-frontend/modules/database/pages/Database.vue
+++ /dev/null
@@ -1,29 +0,0 @@
-<template>
-  <div>
-    <header class="layout-col-3-1 header">
-      <ul class="header-filter">
-        <li class="header-filter-item">&nbsp;</li>
-      </ul>
-      <ul class="header-info">
-        <li>{{ selectedApplication.name }}</li>
-        <li>@TODO table name</li>
-      </ul>
-    </header>
-  </div>
-</template>
-
-<script>
-import { mapState } from 'vuex'
-
-import application from '@/mixins/application'
-
-export default {
-  layout: 'app',
-  mixins: [application],
-  computed: {
-    ...mapState({
-      selectedApplication: state => state.application.selected
-    })
-  }
-}
-</script>
diff --git a/web-frontend/modules/database/pages/Table.vue b/web-frontend/modules/database/pages/Table.vue
new file mode 100644
index 000000000..9555a3c45
--- /dev/null
+++ b/web-frontend/modules/database/pages/Table.vue
@@ -0,0 +1,47 @@
+<template>
+  <div>
+    <header class="layout-col-3-1 header">
+      <ul class="header-filter">
+        <li class="header-filter-item">&nbsp;</li>
+      </ul>
+      <ul class="header-info">
+        <li>{{ database }}</li>
+        <li>{{ table }}</li>
+      </ul>
+    </header>
+  </div>
+</template>
+
+<script>
+// import { notify404 } from '@/utils/error'
+
+export default {
+  layout: 'app',
+  props: {
+    id: {
+      type: Number,
+      required: true
+    },
+    tableId: {
+      type: Number,
+      required: true
+    }
+  },
+  asyncData({ store, params, redirect }) {
+    // @TODO figure out why the id's aren't converted to an int in the route.
+    const databaseId = parseInt(params.id)
+    const tableId = parseInt(params.tableId)
+
+    return store
+      .dispatch('table/preSelect', { databaseId, tableId })
+      .then(data => {
+        return { database: data.database, table: data.table }
+      })
+      .catch(() => {
+        // If something went wrong this will probably mean that the user doesn't have
+        // access to the database so we will need to redirect back to the index page.
+        redirect({ name: 'app' })
+      })
+  }
+}
+</script>
diff --git a/web-frontend/modules/database/plugin.js b/web-frontend/modules/database/plugin.js
index 784b13c82..fdbd108ab 100644
--- a/web-frontend/modules/database/plugin.js
+++ b/web-frontend/modules/database/plugin.js
@@ -1,5 +1,7 @@
 import { DatabaseApplication } from '@/modules/database/application'
+import tableStore from '@/modules/database/store/table'
 
 export default ({ store }) => {
+  store.registerModule('table', tableStore)
   store.dispatch('application/register', new DatabaseApplication())
 }
diff --git a/web-frontend/modules/database/routes.js b/web-frontend/modules/database/routes.js
index cc110db65..7a66831b9 100644
--- a/web-frontend/modules/database/routes.js
+++ b/web-frontend/modules/database/routes.js
@@ -2,13 +2,15 @@ import path from 'path'
 
 export const databaseRoutes = [
   {
-    name: 'application-database',
-    path: '/database/:id',
-    component: path.resolve(__dirname, 'pages/Database.vue'),
+    name: 'database-table',
+    path: '/database/:id/table/:tableId',
+    component: path.resolve(__dirname, 'pages/Table.vue'),
     props(route) {
-      const props = { ...route.params }
-      props.id = parseInt(props.id)
-      return props
+      // @TODO figure out why the route param is empty on the server side.
+      const p = { ...route.params }
+      p.id = parseInt(p.id)
+      p.tableId = parseInt(p.tableId)
+      return p
     }
   }
 ]
diff --git a/web-frontend/modules/database/services/table.js b/web-frontend/modules/database/services/table.js
new file mode 100644
index 000000000..892e0c47d
--- /dev/null
+++ b/web-frontend/modules/database/services/table.js
@@ -0,0 +1,19 @@
+import { client } from '@/services/client'
+
+export default {
+  fetchAll(databaseId) {
+    return client.get(`/database/tables/database/${databaseId}/`)
+  },
+  create(databaseId, values) {
+    return client.post(`/database/tables/database/${databaseId}/`, values)
+  },
+  get(tableId) {
+    return client.get(`/database/tables/${tableId}/`)
+  },
+  update(tableId, values) {
+    return client.patch(`/database/tables/${tableId}/`, values)
+  },
+  delete(tableId) {
+    return client.delete(`/database/tables/${tableId}/`)
+  }
+}
diff --git a/web-frontend/modules/database/store/table.js b/web-frontend/modules/database/store/table.js
new file mode 100644
index 000000000..be54751c2
--- /dev/null
+++ b/web-frontend/modules/database/store/table.js
@@ -0,0 +1,190 @@
+import TableService from '@/modules/database/services/table'
+import { DatabaseApplication } from '@/modules/database/application'
+import { notify404, notifyError } from '@/utils/error'
+
+export function populateTable(table) {
+  table._ = {
+    disabled: false,
+    selected: false
+  }
+  return table
+}
+
+export const state = () => ({
+  selected: {}
+})
+
+export const mutations = {
+  ADD_ITEM(state, { database, table }) {
+    populateTable(table)
+    database.tables.push(table)
+  },
+  UPDATE_ITEM(state, { table, values }) {
+    Object.assign(table, table, values)
+  },
+  SET_SELECTED(state, { database, table }) {
+    Object.values(database.tables).forEach(item => {
+      item._.selected = false
+    })
+    table._.selected = true
+    state.selected = table
+  },
+  DELETE_ITEM(state, { database, id }) {
+    const index = database.tables.findIndex(item => item.id === id)
+    database.tables.splice(index, 1)
+  }
+}
+
+export const actions = {
+  /**
+   * Create a new table based on the provided values and add it to the tables
+   * of the provided database.
+   */
+  create({ commit, dispatch }, { database, values }) {
+    const type = DatabaseApplication.getType()
+
+    // Check if the provided database (application) has the correct type.
+    if (database.type !== type) {
+      throw new Error(
+        `The provided database application doesn't have the required type 
+        "${type}".`
+      )
+    }
+
+    return TableService.create(database.id, values)
+      .then(({ data }) => {
+        commit('ADD_ITEM', { database, table: data })
+      })
+      .catch(error => {
+        notify404(
+          dispatch,
+          error,
+          'Could not create table',
+          "You're unable to create a new table for the selected database. " +
+            ". This is because the database doesn't exist."
+        )
+
+        notifyError(
+          dispatch,
+          error,
+          'ERROR_USER_NOT_IN_GROUP',
+          'Could not create table',
+          "You're unable to create a new table for the selected database. " +
+            ". This is because you're not part of the group."
+        )
+
+        throw error
+      })
+  },
+  /**
+   * Update an existing table of the provided database with the provided tables.
+   */
+  update({ commit, dispatch }, { database, table, values }) {
+    return TableService.update(table.id, values)
+      .then(({ data }) => {
+        commit('UPDATE_ITEM', { database, table, values: data })
+      })
+      .catch(error => {
+        notify404(
+          dispatch,
+          error,
+          'Could not update table',
+          "You're unable to update the table. This is because the " +
+            "table doesn't exist."
+        )
+
+        notifyError(
+          dispatch,
+          error,
+          'ERROR_USER_NOT_IN_GROUP',
+          'Could not update table',
+          "You're unable to update the table. This is because you're " +
+            'not part of the group.'
+        )
+
+        throw error
+      })
+  },
+  /**
+   * Deletes an existing application.
+   */
+  delete({ commit, dispatch }, { database, table }) {
+    return TableService.delete(table.id)
+      .then(() => {
+        commit('DELETE_ITEM', { database, id: table.id })
+      })
+      .catch(error => {
+        notify404(
+          dispatch,
+          error,
+          'Could not delete table',
+          "You're unable to delete the table for the selected database. " +
+            ". This is because the database doesn't exist."
+        )
+
+        notifyError(
+          dispatch,
+          error,
+          'ERROR_USER_NOT_IN_GROUP',
+          'Unable to delete',
+          "You're not allowed to delete the table because you're" +
+            ' not part of the group where the application is in.'
+        )
+
+        throw error
+      })
+  },
+  /**
+   * Select a table of a database.
+   */
+  select({ commit }, { database, table }) {
+    commit('SET_SELECTED', { database, table })
+    return { database, table }
+  },
+  /**
+   * First it will preSelect the application to make sure the groups are
+   * fetched, the correct group is selected, the related applications are
+   * selected and the provided application id is selected. After that is will
+   * check if the application has the correct type which is a database and makes
+   * sure that the table actually belongs to that database. If so it will select
+   * the table and return the database and table so it can be used.
+   */
+  preSelect({ dispatch, getters, rootGetters }, { databaseId, tableId }) {
+    // Preselect the application
+    return dispatch('application/preSelect', databaseId, { root: true }).then(
+      database => {
+        const type = DatabaseApplication.getType()
+
+        // Check if the just selected application has the correct type because
+        // it needs to have tables.
+        if (database.type !== type) {
+          throw new Error(
+            `The application doesn't have the required ${type} type.`
+          )
+        }
+
+        // Check if the provided table id is found is the just selected
+        // database.
+        const index = database.tables.findIndex(item => item.id === tableId)
+        if (index === -1) {
+          throw new Error('The table is not found in the selected application.')
+        }
+        const table = database.tables[index]
+
+        // Select the table table and return the database and table instance
+        // when done.
+        return dispatch('select', { database, table })
+      }
+    )
+  }
+}
+
+export const getters = {}
+
+export default {
+  namespaced: true,
+  state,
+  getters,
+  actions,
+  mutations
+}
diff --git a/web-frontend/pages/app/index.vue b/web-frontend/pages/app/index.vue
index 01d71e138..658b62921 100644
--- a/web-frontend/pages/app/index.vue
+++ b/web-frontend/pages/app/index.vue
@@ -10,7 +10,9 @@
       <br /><br />
       {{ groupApplications }}
       <br /><br />
-      <nuxt-link :to="{ name: 'application-database', params: { id: 1 } }">
+      <nuxt-link
+        :to="{ name: 'database-table', params: { id: 1, tableId: 1 } }"
+      >
         <i class="fas fa-arrow-left"></i>
         App
       </nuxt-link>
diff --git a/web-frontend/store/application.js b/web-frontend/store/application.js
index 0dfeb8552..0edaaeeb3 100644
--- a/web-frontend/store/application.js
+++ b/web-frontend/store/application.js
@@ -10,7 +10,7 @@ function populateApplication(application, getters) {
     loading: false,
     selected: false
   }
-  return application
+  return type.populate(application)
 }
 
 export const state = () => ({
@@ -143,6 +143,8 @@ export const actions = {
           "You're unable to create a new application for the selected " +
             "group. This could be because you're not part of the group."
         )
+
+        throw error
       })
   },
   /**
@@ -158,10 +160,12 @@ export const actions = {
           dispatch,
           error,
           'ERROR_USER_NOT_IN_GROUP',
-          'Rename not allowed',
-          "You're not allowed to rename the application because you're " +
+          'Change not allowed',
+          "You're not allowed to change the application because you're " +
             'not part of the group where the application is in.'
         )
+
+        throw error
       })
   },
   /**
@@ -181,6 +185,8 @@ export const actions = {
           "You're not allowed to rename the application because you're" +
             ' not part of the group where the application is in.'
         )
+
+        throw error
       })
   },
   /**
@@ -188,6 +194,7 @@ export const actions = {
    */
   select({ commit }, application) {
     commit('SET_SELECTED', application)
+    return application
   },
   /**
    * Select an application by a given application id.
@@ -213,17 +220,17 @@ export const actions = {
    * to date. In short it will make sure that the depending state of the given
    * application will be there.
    */
-  preSelect({ dispatch, getters, rootGetters }, id) {
+  preSelect({ dispatch, getters, rootGetters, state }, id) {
     // First we will check if the application is already in the items.
     const application = getters.get(id)
 
     // If the application is already selected we don't have to do anything.
     if (application !== undefined && application._.selected) {
-      return
+      return application
     }
 
     // This function will select a group by its id which will then automatically
-    // fetch the applications related to that group. When done it will select
+    // fetches the applications related to that group. When done it will select
     // the provided application id.
     const selectGroupAndApplication = (groupId, applicationId) => {
       return dispatch('group/selectById', groupId, {
@@ -237,19 +244,20 @@ export const actions = {
       // If the application is already in the selected groups, which means that
       // the groups and applications are already loaded, we can just select that
       // application.
-      dispatch('select', application)
+      return dispatch('select', application)
     } else {
       // The application is not in the selected group so we need to figure out
       // in which he is by fetching the application.
-      return ApplicationService.get(id).then(data => {
+      return ApplicationService.get(id).then(response => {
         if (!rootGetters['group/isLoaded']) {
           // If the groups are not already loaded we need to load them first.
           return dispatch('group/fetchAll', {}, { root: true }).then(() => {
-            return selectGroupAndApplication(data.data.group.id, id)
+            return selectGroupAndApplication(response.data.group.id, id)
           })
         } else {
-          // The groups are already loaded so we
-          return selectGroupAndApplication(data.data.group.id, id)
+          // The groups are already loaded so we need to select the group and
+          // application.
+          return selectGroupAndApplication(response.data.group.id, id)
         }
       })
     }
diff --git a/web-frontend/store/group.js b/web-frontend/store/group.js
index d19eab34c..4d7b70093 100644
--- a/web-frontend/store/group.js
+++ b/web-frontend/store/group.js
@@ -122,10 +122,12 @@ export const actions = {
         notify404(
           dispatch,
           error,
-          'Unable to rename',
-          "You're unable to rename the group. This could be because " +
+          'Unable to update',
+          "You're unable to update the group. This could be because " +
             "you're not part of the group."
         )
+
+        throw error
       })
   },
   /**
@@ -148,6 +150,8 @@ export const actions = {
           "You're unable to delete the group. This could be because " +
             "you're not part of the group."
         )
+
+        throw error
       })
   },
   /**