diff --git a/backend/src/baserow/config/settings/base.py b/backend/src/baserow/config/settings/base.py
index 55d556422..f1d547ddf 100644
--- a/backend/src/baserow/config/settings/base.py
+++ b/backend/src/baserow/config/settings/base.py
@@ -1184,6 +1184,11 @@ if POSTHOG_ENABLED:
 else:
     posthog.disabled = True
 
+BASEROW_BUILDER_DOMAINS = os.getenv("BASEROW_BUILDER_DOMAINS", None)
+BASEROW_BUILDER_DOMAINS = (
+    BASEROW_BUILDER_DOMAINS.split(",") if BASEROW_BUILDER_DOMAINS is not None else []
+)
+
 # Indicates whether we are running the tests or not. Set to True in the test.py settings
 # file used by pytest.ini
 TESTS = False
diff --git a/backend/src/baserow/contrib/builder/api/domains/errors.py b/backend/src/baserow/contrib/builder/api/domains/errors.py
index 3ab6e336a..dccc8906c 100644
--- a/backend/src/baserow/contrib/builder/api/domains/errors.py
+++ b/backend/src/baserow/contrib/builder/api/domains/errors.py
@@ -11,3 +11,16 @@ ERROR_DOMAIN_NOT_IN_BUILDER = (
     HTTP_400_BAD_REQUEST,
     "The domain id {e.domain_id} does not belong to the builder.",
 )
+
+ERROR_DOMAIN_NAME_NOT_UNIQUE = (
+    "ERROR_DOMAIN_NAME_NOT_UNIQUE",
+    HTTP_400_BAD_REQUEST,
+    "The domain name {e.domain_name} already exists.",
+)
+
+ERROR_SUB_DOMAIN_HAS_INVALID_DOMAIN_NAME = (
+    "ERROR_SUB_DOMAIN_HAS_INVALID_DOMAIN_NAME",
+    HTTP_400_BAD_REQUEST,
+    "The subdomain {e.domain_name} has an invalid domain name, "
+    "you can only use {e.available_domain_names}",
+)
diff --git a/backend/src/baserow/contrib/builder/api/domains/serializers.py b/backend/src/baserow/contrib/builder/api/domains/serializers.py
index beb7c85ec..7b5251232 100644
--- a/backend/src/baserow/contrib/builder/api/domains/serializers.py
+++ b/backend/src/baserow/contrib/builder/api/domains/serializers.py
@@ -1,5 +1,7 @@
 from typing import List
 
+from django.utils.functional import lazy
+
 from drf_spectacular.types import OpenApiTypes
 from drf_spectacular.utils import extend_schema_field
 from rest_framework import serializers
@@ -8,6 +10,7 @@ from baserow.api.services.serializers import PublicServiceSerializer
 from baserow.contrib.builder.api.pages.serializers import PathParamSerializer
 from baserow.contrib.builder.data_sources.models import DataSource
 from baserow.contrib.builder.domains.models import Domain
+from baserow.contrib.builder.domains.registries import domain_type_registry
 from baserow.contrib.builder.elements.models import Element
 from baserow.contrib.builder.elements.registries import element_type_registry
 from baserow.contrib.builder.models import Builder
@@ -16,9 +19,15 @@ from baserow.core.services.registries import service_type_registry
 
 
 class DomainSerializer(serializers.ModelSerializer):
+    type = serializers.SerializerMethodField(help_text="The type of the domain.")
+
+    @extend_schema_field(OpenApiTypes.STR)
+    def get_type(self, instance):
+        return domain_type_registry.get_by_model(instance.specific_class).type
+
     class Meta:
         model = Domain
-        fields = ("id", "domain_name", "order", "builder_id", "last_published")
+        fields = ("id", "type", "domain_name", "order", "builder_id", "last_published")
         extra_kwargs = {
             "id": {"read_only": True},
             "builder_id": {"read_only": True},
@@ -27,9 +36,37 @@ class DomainSerializer(serializers.ModelSerializer):
 
 
 class CreateDomainSerializer(serializers.ModelSerializer):
+    type = serializers.ChoiceField(
+        choices=lazy(domain_type_registry.get_types, list)(),
+        required=True,
+        help_text="The type of the domain.",
+    )
+
     class Meta:
         model = Domain
-        fields = ("domain_name",)
+        fields = (
+            "type",
+            "domain_name",
+        )
+
+
+class UpdateDomainSerializer(serializers.ModelSerializer):
+    type = serializers.ChoiceField(
+        choices=lazy(domain_type_registry.get_types, list)(),
+        required=False,
+        help_text="The type of the domain.",
+    )
+
+    domain_name = serializers.CharField(
+        required=False, help_text=Domain._meta.get_field("domain_name").help_text
+    )
+
+    class Meta:
+        model = Domain
+        fields = (
+            "type",
+            "domain_name",
+        )
 
 
 class OrderDomainsSerializer(serializers.Serializer):
diff --git a/backend/src/baserow/contrib/builder/api/domains/views.py b/backend/src/baserow/contrib/builder/api/domains/views.py
index beefe8194..24187320e 100644
--- a/backend/src/baserow/contrib/builder/api/domains/views.py
+++ b/backend/src/baserow/contrib/builder/api/domains/views.py
@@ -10,29 +10,43 @@ from rest_framework.status import HTTP_202_ACCEPTED
 from rest_framework.views import APIView
 
 from baserow.api.applications.errors import ERROR_APPLICATION_DOES_NOT_EXIST
-from baserow.api.decorators import map_exceptions, validate_body
+from baserow.api.decorators import (
+    map_exceptions,
+    validate_body,
+    validate_body_custom_fields,
+)
 from baserow.api.jobs.serializers import JobSerializer
 from baserow.api.schemas import CLIENT_SESSION_ID_SCHEMA_PARAMETER, get_error_schema
-from baserow.api.utils import DiscriminatorCustomFieldsMappingSerializer
+from baserow.api.utils import (
+    DiscriminatorCustomFieldsMappingSerializer,
+    type_from_data_or_registry,
+    validate_data_custom_fields,
+)
 from baserow.contrib.builder.api.data_sources.serializers import DataSourceSerializer
 from baserow.contrib.builder.api.domains.errors import (
     ERROR_DOMAIN_DOES_NOT_EXIST,
+    ERROR_DOMAIN_NAME_NOT_UNIQUE,
     ERROR_DOMAIN_NOT_IN_BUILDER,
+    ERROR_SUB_DOMAIN_HAS_INVALID_DOMAIN_NAME,
 )
 from baserow.contrib.builder.api.domains.serializers import (
     CreateDomainSerializer,
     DomainSerializer,
     OrderDomainsSerializer,
     PublicBuilderSerializer,
+    UpdateDomainSerializer,
 )
 from baserow.contrib.builder.api.pages.errors import ERROR_PAGE_DOES_NOT_EXIST
 from baserow.contrib.builder.data_sources.service import DataSourceService
 from baserow.contrib.builder.domains.exceptions import (
     DomainDoesNotExist,
+    DomainNameNotUniqueError,
     DomainNotInBuilder,
+    SubDomainHasInvalidDomainName,
 )
 from baserow.contrib.builder.domains.handler import DomainHandler
 from baserow.contrib.builder.domains.models import Domain
+from baserow.contrib.builder.domains.registries import domain_type_registry
 from baserow.contrib.builder.domains.service import DomainService
 from baserow.contrib.builder.elements.registries import element_type_registry
 from baserow.contrib.builder.elements.service import ElementService
@@ -66,9 +80,13 @@ class DomainsView(APIView):
         tags=["Builder domains"],
         operation_id="create_builder_domain",
         description="Creates a new domain for an application builder",
-        request=CreateDomainSerializer,
+        request=DiscriminatorCustomFieldsMappingSerializer(
+            domain_type_registry, CreateDomainSerializer, request=True
+        ),
         responses={
-            200: DomainSerializer,
+            200: DiscriminatorCustomFieldsMappingSerializer(
+                domain_type_registry, DomainSerializer
+            ),
             400: get_error_schema(
                 [
                     "ERROR_USER_NOT_IN_GROUP",
@@ -82,17 +100,22 @@ class DomainsView(APIView):
     @map_exceptions(
         {
             ApplicationDoesNotExist: ERROR_APPLICATION_DOES_NOT_EXIST,
+            SubDomainHasInvalidDomainName: ERROR_SUB_DOMAIN_HAS_INVALID_DOMAIN_NAME,
         }
     )
-    @validate_body(CreateDomainSerializer)
+    @validate_body_custom_fields(
+        domain_type_registry, base_serializer_class=CreateDomainSerializer
+    )
     def post(self, request, data: Dict, builder_id: int):
         builder = BuilderHandler().get_builder(builder_id)
+        type_name = data.pop("type")
 
+        domain_type = domain_type_registry.get(type_name)
         domain = DomainService().create_domain(
-            request.user, builder, data["domain_name"]
+            request.user, domain_type, builder, **data
         )
 
-        serializer = DomainSerializer(domain)
+        serializer = domain_type_registry.get_serializer(domain, DomainSerializer)
         return Response(serializer.data)
 
     @extend_schema(
@@ -109,7 +132,9 @@ class DomainsView(APIView):
         operation_id="get_builder_domains",
         description="Gets all the domains of a builder",
         responses={
-            200: DomainSerializer(many=True),
+            200: DiscriminatorCustomFieldsMappingSerializer(
+                domain_type_registry, DomainSerializer, many=True
+            ),
             400: get_error_schema(
                 [
                     "ERROR_USER_NOT_IN_GROUP",
@@ -132,9 +157,12 @@ class DomainsView(APIView):
             builder,
         )
 
-        serializer = DomainSerializer(domains, many=True)
+        data = [
+            domain_type_registry.get_serializer(domain, DomainSerializer).data
+            for domain in domains
+        ]
 
-        return Response(serializer.data)
+        return Response(data)
 
 
 class DomainView(APIView):
@@ -151,9 +179,11 @@ class DomainView(APIView):
         tags=["Builder domains"],
         operation_id="update_builder_domain",
         description="Updates an existing domain of an application builder",
-        request=CreateDomainSerializer,
+        request=UpdateDomainSerializer,
         responses={
-            200: DomainSerializer,
+            200: DiscriminatorCustomFieldsMappingSerializer(
+                domain_type_registry, DomainSerializer
+            ),
             400: get_error_schema(
                 [
                     "ERROR_USER_NOT_IN_GROUP",
@@ -167,19 +197,35 @@ class DomainView(APIView):
     @map_exceptions(
         {
             DomainDoesNotExist: ERROR_DOMAIN_DOES_NOT_EXIST,
+            DomainNameNotUniqueError: ERROR_DOMAIN_NAME_NOT_UNIQUE,
+            SubDomainHasInvalidDomainName: ERROR_SUB_DOMAIN_HAS_INVALID_DOMAIN_NAME,
         }
     )
-    @validate_body(CreateDomainSerializer)
-    def patch(self, request, data: Dict, domain_id: int):
-        base_queryset = Domain.objects.select_for_update(of=("self",))
+    def patch(self, request, domain_id: int):
+        base_queryset = Domain.objects
 
-        domain = DomainService().get_domain(
-            request.user, domain_id, base_queryset=base_queryset
+        domain = (
+            DomainService()
+            .get_domain(request.user, domain_id, base_queryset=base_queryset)
+            .specific
+        )
+        domain_type = type_from_data_or_registry(
+            request.data, domain_type_registry, domain
+        )
+
+        data = validate_data_custom_fields(
+            domain_type.type,
+            domain_type_registry,
+            request.data,
+            base_serializer_class=UpdateDomainSerializer,
+            partial=True,
         )
 
         domain_updated = DomainService().update_domain(request.user, domain, **data)
 
-        serializer = DomainSerializer(domain_updated)
+        serializer = domain_type_registry.get_serializer(
+            domain_updated, DomainSerializer
+        )
         return Response(serializer.data)
 
     @extend_schema(
diff --git a/backend/src/baserow/contrib/builder/apps.py b/backend/src/baserow/contrib/builder/apps.py
index f6aa0493e..8249c71fe 100644
--- a/backend/src/baserow/contrib/builder/apps.py
+++ b/backend/src/baserow/contrib/builder/apps.py
@@ -151,6 +151,12 @@ class BuilderConfig(AppConfig):
         element_type_registry.register(InputTextElementType())
         element_type_registry.register(ColumnElementType())
 
+        from .domains.domain_types import CustomDomainType, SubDomainType
+        from .domains.registries import domain_type_registry
+
+        domain_type_registry.register(CustomDomainType())
+        domain_type_registry.register(SubDomainType())
+
         from .domains.trash_types import DomainTrashableItemType
 
         trash_item_type_registry.register(DomainTrashableItemType())
diff --git a/backend/src/baserow/contrib/builder/domains/domain_types.py b/backend/src/baserow/contrib/builder/domains/domain_types.py
new file mode 100644
index 000000000..f694fe0f8
--- /dev/null
+++ b/backend/src/baserow/contrib/builder/domains/domain_types.py
@@ -0,0 +1,41 @@
+from typing import Dict
+
+from django.conf import settings
+
+from baserow.contrib.builder.domains.exceptions import SubDomainHasInvalidDomainName
+from baserow.contrib.builder.domains.models import CustomDomain, SubDomain
+from baserow.contrib.builder.domains.registries import DomainType
+
+
+class CustomDomainType(DomainType):
+    type = "custom"
+    model_class = CustomDomain
+
+
+class SubDomainType(DomainType):
+    type = "sub_domain"
+    model_class = SubDomain
+
+    def prepare_values(self, values: Dict) -> Dict:
+        domain_name = values.get("domain_name", None)
+
+        if domain_name is not None:
+            self._validate_domain_name(domain_name)
+
+        return values
+
+    def _validate_domain_name(self, domain_name: str):
+        """
+        Checks if the subdomain uses a domain name that is among the available domain
+        names defined in settings.
+
+        :param domain_name: The name that is being proposed
+        :raises SubDomainHasInvalidDomainName: If the domain name is not registered
+        """
+
+        for domain in settings.BASEROW_BUILDER_DOMAINS:
+            if domain_name.endswith(f".{domain}"):
+                # The domain suffix is valid
+                return
+
+        raise SubDomainHasInvalidDomainName(domain_name)
diff --git a/backend/src/baserow/contrib/builder/domains/exceptions.py b/backend/src/baserow/contrib/builder/domains/exceptions.py
index 07e86d121..43779284e 100644
--- a/backend/src/baserow/contrib/builder/domains/exceptions.py
+++ b/backend/src/baserow/contrib/builder/domains/exceptions.py
@@ -1,3 +1,6 @@
+from django.conf import settings
+
+
 class DomainDoesNotExist(Exception):
     """Raised when trying to get a domain that doesn't exist."""
 
@@ -12,3 +15,27 @@ class DomainNotInBuilder(Exception):
             *args,
             **kwargs,
         )
+
+
+class DomainNameNotUniqueError(Exception):
+    """Raised when trying to set a domain name that already exists"""
+
+    def __init__(self, domain_name, *args, **kwargs):
+        self.domain_name = domain_name
+        super().__init__(
+            f"The domain name {domain_name} already exists", *args, **kwargs
+        )
+
+
+class SubDomainHasInvalidDomainName(Exception):
+    """Raised when a subdomain is using an invalid domain name"""
+
+    def __init__(self, domain_name, *args, **kwargs):
+        self.domain_name = domain_name
+        self.available_domain_names = settings.BASEROW_BUILDER_DOMAINS
+        super().__init__(
+            f"The subdomain {domain_name} has an invalid domain name, you can only use "
+            f"{self.available_domain_names}",
+            *args,
+            **kwargs,
+        )
diff --git a/backend/src/baserow/contrib/builder/domains/handler.py b/backend/src/baserow/contrib/builder/domains/handler.py
index 73479b806..03d25f5c2 100644
--- a/backend/src/baserow/contrib/builder/domains/handler.py
+++ b/backend/src/baserow/contrib/builder/domains/handler.py
@@ -1,23 +1,30 @@
-from typing import List
+from typing import Iterable, List, cast
 
 from django.core.files.storage import default_storage
 from django.db.models import QuerySet
+from django.db.utils import IntegrityError
 from django.utils import timezone
 
 from baserow.contrib.builder.domains.exceptions import (
     DomainDoesNotExist,
+    DomainNameNotUniqueError,
     DomainNotInBuilder,
 )
 from baserow.contrib.builder.domains.models import Domain
+from baserow.contrib.builder.domains.registries import DomainType
 from baserow.contrib.builder.exceptions import BuilderDoesNotExist
 from baserow.contrib.builder.models import Builder
+from baserow.core.db import specific_iterator
 from baserow.core.exceptions import IdDoesNotExist
 from baserow.core.registries import ImportExportConfig, application_type_registry
 from baserow.core.trash.handler import TrashHandler
-from baserow.core.utils import Progress
+from baserow.core.utils import Progress, extract_allowed
 
 
 class DomainHandler:
+    allowed_fields_create = ["domain_name"]
+    allowed_fields_update = ["domain_name", "last_published"]
+
     def get_domain(self, domain_id: int, base_queryset: QuerySet = None) -> Domain:
         """
         Gets a domain by ID
@@ -39,20 +46,20 @@ class DomainHandler:
 
     def get_domains(
         self, builder: Builder, base_queryset: QuerySet = None
-    ) -> QuerySet[Domain]:
+    ) -> Iterable[Domain]:
         """
         Gets all the domains of a builder.
 
         :param builder: The builder we are trying to get all domains for
         :param base_queryset: Can be provided to already filter or apply performance
             improvements to the queryset when it's being executed
-        :return: A queryset with all the domains
+        :return: An iterable of all the specific domains
         """
 
         if base_queryset is None:
             base_queryset = Domain.objects
 
-        return base_queryset.filter(builder=builder)
+        return specific_iterator(base_queryset.filter(builder=builder))
 
     def get_public_builder_by_domain_name(self, domain_name: str) -> Builder:
         """
@@ -78,20 +85,31 @@ class DomainHandler:
 
         return domain.published_to
 
-    def create_domain(self, builder: Builder, domain_name: str) -> Domain:
+    def create_domain(
+        self, domain_type: DomainType, builder: Builder, **kwargs
+    ) -> Domain:
         """
         Creates a new domain
 
+        :param domain_type: The type of domain that's being created
         :param builder: The builder the domain belongs to
-        :param domain_name: The name of the domain
+        :param kwargs: Additional attributes of the domain
         :return: The newly created domain instance
         """
 
         last_order = Domain.get_last_order(builder)
-        domain = Domain.objects.create(
-            builder=builder, domain_name=domain_name, order=last_order
+
+        model_class = cast(Domain, domain_type.model_class)
+
+        allowed_values = extract_allowed(
+            kwargs, self.allowed_fields_create + domain_type.allowed_fields
         )
 
+        prepared_values = domain_type.prepare_values(allowed_values)
+
+        domain = model_class(builder=builder, order=last_order, **prepared_values)
+        domain.save()
+
         return domain
 
     def delete_domain(self, domain: Domain):
@@ -112,10 +130,23 @@ class DomainHandler:
         :return: The updated domain
         """
 
-        for key, value in kwargs.items():
+        domain_type = domain.get_type()
+
+        allowed_values = extract_allowed(
+            kwargs, self.allowed_fields_update + domain_type.allowed_fields
+        )
+
+        prepared_values = domain_type.prepare_values(allowed_values)
+
+        for key, value in prepared_values.items():
             setattr(domain, key, value)
 
-        domain.save()
+        try:
+            domain.save()
+        except IntegrityError as error:
+            if "unique" in str(error) and "domain_name" in prepared_values:
+                raise DomainNameNotUniqueError(prepared_values["domain_name"])
+            raise error
 
         return domain
 
diff --git a/backend/src/baserow/contrib/builder/domains/models.py b/backend/src/baserow/contrib/builder/domains/models.py
index b9180422c..164c2eda0 100644
--- a/backend/src/baserow/contrib/builder/domains/models.py
+++ b/backend/src/baserow/contrib/builder/domains/models.py
@@ -1,16 +1,21 @@
+from django.contrib.contenttypes.models import ContentType
 from django.db import models
 from django.db.models import CASCADE, SET_NULL
 
 import validators
 from rest_framework.exceptions import ValidationError
 
+from baserow.contrib.builder.domains.registries import domain_type_registry
 from baserow.core.jobs.mixins import JobWithUserIpAddress
 from baserow.core.jobs.models import Job
 from baserow.core.mixins import (
     HierarchicalModelMixin,
     OrderableMixin,
+    PolymorphicContentTypeMixin,
     TrashableModelMixin,
+    WithRegistry,
 )
+from baserow.core.registry import ModelRegistryMixin
 
 
 def validate_domain(value: str):
@@ -29,7 +34,24 @@ def validate_domain(value: str):
         raise ValidationError("Invalid domain syntax")
 
 
-class Domain(HierarchicalModelMixin, TrashableModelMixin, OrderableMixin, models.Model):
+def get_default_domain_content_type():
+    return ContentType.objects.get_for_model(CustomDomain)
+
+
+class Domain(
+    HierarchicalModelMixin,
+    TrashableModelMixin,
+    OrderableMixin,
+    WithRegistry,
+    PolymorphicContentTypeMixin,
+    models.Model,
+):
+    content_type = models.ForeignKey(
+        ContentType,
+        verbose_name="content type",
+        related_name="builder_domains",
+        on_delete=models.SET(get_default_domain_content_type),
+    )
     builder = models.ForeignKey(
         "builder.Builder",
         on_delete=CASCADE,
@@ -63,6 +85,18 @@ class Domain(HierarchicalModelMixin, TrashableModelMixin, OrderableMixin, models
         queryset = Domain.objects.filter(builder=builder)
         return cls.get_highest_order_of_queryset(queryset) + 1
 
+    @staticmethod
+    def get_type_registry() -> ModelRegistryMixin:
+        return domain_type_registry
+
+
+class CustomDomain(Domain):
+    pass
+
+
+class SubDomain(Domain):
+    pass
+
 
 class PublishDomainJob(JobWithUserIpAddress, Job):
     domain: Domain = models.ForeignKey(Domain, null=True, on_delete=models.SET_NULL)
diff --git a/backend/src/baserow/contrib/builder/domains/registries.py b/backend/src/baserow/contrib/builder/domains/registries.py
new file mode 100644
index 000000000..912dd3222
--- /dev/null
+++ b/backend/src/baserow/contrib/builder/domains/registries.py
@@ -0,0 +1,34 @@
+from abc import ABC
+from typing import Dict
+
+from baserow.core.registry import (
+    CustomFieldsInstanceMixin,
+    CustomFieldsRegistryMixin,
+    Instance,
+    ModelInstanceMixin,
+    ModelRegistryMixin,
+    Registry,
+)
+
+
+class DomainType(Instance, ModelInstanceMixin, CustomFieldsInstanceMixin, ABC):
+    def prepare_values(self, values: Dict) -> Dict:
+        """
+        Called before a domain is saved/updates to validate the data or transform the
+        data before it is being saved.
+        :param values: The values that are about to be saved
+        :return: The values after validation/transformation
+        """
+
+        return values
+
+
+class DomainTypeRegistry(Registry, ModelRegistryMixin, CustomFieldsRegistryMixin):
+    """
+    Contains all the registered domain types.
+    """
+
+    name = "domain_type"
+
+
+domain_type_registry = DomainTypeRegistry()
diff --git a/backend/src/baserow/contrib/builder/domains/service.py b/backend/src/baserow/contrib/builder/domains/service.py
index be61b21dd..fe0280c57 100644
--- a/backend/src/baserow/contrib/builder/domains/service.py
+++ b/backend/src/baserow/contrib/builder/domains/service.py
@@ -29,6 +29,7 @@ from baserow.core.utils import Progress, extract_allowed
 
 from .job_types import PublishDomainJobType
 from .operations import PublishDomainOperationType
+from .registries import DomainType
 
 
 class DomainService:
@@ -121,14 +122,19 @@ class DomainService:
         return builder
 
     def create_domain(
-        self, user: AbstractUser, builder: Builder, domain_name: str
+        self,
+        user: AbstractUser,
+        domain_type: DomainType,
+        builder: Builder,
+        **kwargs,
     ) -> Domain:
         """
         Creates a new domain
 
         :param user: The user trying to create the domain
+        :param domain_type: The type of domain that's being created
         :param builder: The builder the domain belongs to
-        :param domain_name: The name of the domain
+        :param kwargs: Additional attributes of the domain
         :return: The newly created domain instance
         """
 
@@ -139,7 +145,7 @@ class DomainService:
             context=builder,
         )
 
-        domain = self.handler.create_domain(builder, domain_name)
+        domain = self.handler.create_domain(domain_type, builder, **kwargs)
 
         domain_created.send(self, domain=domain, user=user)
 
diff --git a/backend/src/baserow/contrib/builder/migrations/0018_sub_domains.py b/backend/src/baserow/contrib/builder/migrations/0018_sub_domains.py
new file mode 100644
index 000000000..e8aeb5447
--- /dev/null
+++ b/backend/src/baserow/contrib/builder/migrations/0018_sub_domains.py
@@ -0,0 +1,73 @@
+# Generated by Django 3.2.21 on 2023-09-14 13:34
+
+import django.db.models.deletion
+from django.db import migrations, models
+
+import baserow.contrib.builder.domains.models
+
+
+def get_default_domain_content_type_id():
+    return baserow.contrib.builder.domains.models.get_default_domain_content_type().id
+
+
+class Migration(migrations.Migration):
+    dependencies = [
+        ("contenttypes", "0002_remove_content_type_name"),
+        ("builder", "0017_mainthemeconfigblock"),
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name="CustomDomain",
+            fields=[
+                (
+                    "domain_ptr",
+                    models.OneToOneField(
+                        auto_created=True,
+                        on_delete=django.db.models.deletion.CASCADE,
+                        parent_link=True,
+                        primary_key=True,
+                        serialize=False,
+                        to="builder.domain",
+                    ),
+                ),
+            ],
+            options={
+                "abstract": False,
+            },
+            bases=("builder.domain",),
+        ),
+        migrations.CreateModel(
+            name="SubDomain",
+            fields=[
+                (
+                    "domain_ptr",
+                    models.OneToOneField(
+                        auto_created=True,
+                        on_delete=django.db.models.deletion.CASCADE,
+                        parent_link=True,
+                        primary_key=True,
+                        serialize=False,
+                        to="builder.domain",
+                    ),
+                ),
+            ],
+            options={
+                "abstract": False,
+            },
+            bases=("builder.domain",),
+        ),
+        migrations.AddField(
+            model_name="domain",
+            name="content_type",
+            field=models.ForeignKey(
+                default=get_default_domain_content_type_id,
+                on_delete=models.SET(
+                    baserow.contrib.builder.domains.models.get_default_domain_content_type
+                ),
+                related_name="builder_domains",
+                to="contenttypes.contenttype",
+                verbose_name="content type",
+            ),
+        ),
+    ]
diff --git a/backend/src/baserow/contrib/builder/migrations/0019_migrate_existing_domains.py b/backend/src/baserow/contrib/builder/migrations/0019_migrate_existing_domains.py
new file mode 100644
index 000000000..639804e64
--- /dev/null
+++ b/backend/src/baserow/contrib/builder/migrations/0019_migrate_existing_domains.py
@@ -0,0 +1,34 @@
+from django.db import migrations, transaction
+
+from baserow.contrib.builder.domains.models import CustomDomain, Domain
+
+
+def forward(apps, schema_editor):
+    """
+    This migration introduces polymorphism, so we need to decide which type of
+    domain all the existing domains now become. In this case we are turning
+    all the existing domains into custom domains.
+    """
+
+    with transaction.atomic():
+        domains = Domain.objects.all()
+
+        for domain in domains:
+            domain.delete()
+            CustomDomain.objects.create(
+                domain_name=domain.domain_name,
+                order=domain.order,
+                builder=domain.builder,
+            )
+
+
+def reverse(apps, schema_editor):
+    ...
+
+
+class Migration(migrations.Migration):
+    dependencies = [("builder", "0018_sub_domains")]
+
+    operations = [
+        migrations.RunPython(forward, reverse),
+    ]
diff --git a/backend/src/baserow/contrib/builder/populate.py b/backend/src/baserow/contrib/builder/populate.py
index 9d021558f..3b7acc2da 100644
--- a/backend/src/baserow/contrib/builder/populate.py
+++ b/backend/src/baserow/contrib/builder/populate.py
@@ -4,7 +4,7 @@ from django.db import transaction
 from faker import Faker
 
 from baserow.contrib.builder.data_sources.handler import DataSourceHandler
-from baserow.contrib.builder.domains.models import Domain
+from baserow.contrib.builder.domains.models import CustomDomain
 from baserow.contrib.builder.elements.handler import ElementHandler
 from baserow.contrib.builder.elements.registries import element_type_registry
 from baserow.contrib.builder.models import Builder
@@ -38,12 +38,18 @@ def load_test_data():
             user, workspace, "builder", name="Back to local website"
         )
 
-    Domain.objects.filter(domain_name="test1.getbaserow.io").delete()
-    Domain.objects.filter(domain_name="test2.getbaserow.io").delete()
-    Domain.objects.filter(domain_name="test3.getbaserow.io").delete()
-    Domain.objects.create(builder=builder, domain_name="test1.getbaserow.io", order=1)
-    Domain.objects.create(builder=builder, domain_name="test2.getbaserow.io", order=2)
-    Domain.objects.create(builder=builder, domain_name="test3.getbaserow.io", order=3)
+    CustomDomain.objects.filter(domain_name="test1.getbaserow.io").delete()
+    CustomDomain.objects.filter(domain_name="test2.getbaserow.io").delete()
+    CustomDomain.objects.filter(domain_name="test3.getbaserow.io").delete()
+    CustomDomain.objects.create(
+        builder=builder, domain_name="test1.getbaserow.io", order=1
+    )
+    CustomDomain.objects.create(
+        builder=builder, domain_name="test2.getbaserow.io", order=2
+    )
+    CustomDomain.objects.create(
+        builder=builder, domain_name="test3.getbaserow.io", order=3
+    )
 
     integration_type = integration_type_registry.get("local_baserow")
 
diff --git a/backend/src/baserow/test_utils/fixtures/domain.py b/backend/src/baserow/test_utils/fixtures/domain.py
index ef72f3082..b1fe601d2 100644
--- a/backend/src/baserow/test_utils/fixtures/domain.py
+++ b/backend/src/baserow/test_utils/fixtures/domain.py
@@ -1,8 +1,14 @@
-from baserow.contrib.builder.domains.models import Domain
+from baserow.contrib.builder.domains.models import CustomDomain, SubDomain
 
 
 class DomainFixtures:
-    def create_builder_domain(self, user=None, **kwargs):
+    def create_builder_custom_domain(self, user=None, **kwargs):
+        return self.create_builder_domain(CustomDomain, user, **kwargs)
+
+    def create_builder_sub_domain(self, user=None, **kwargs):
+        return self.create_builder_domain(SubDomain, user, **kwargs)
+
+    def create_builder_domain(self, model_class, user=None, **kwargs):
         if user is None:
             user = self.create_user()
 
@@ -15,6 +21,6 @@ class DomainFixtures:
         if "order" not in kwargs:
             kwargs["order"] = 0
 
-        domain = Domain.objects.create(**kwargs)
+        domain = model_class.objects.create(**kwargs)
 
         return domain
diff --git a/backend/tests/baserow/contrib/builder/api/domains/test_domain_public_views.py b/backend/tests/baserow/contrib/builder/api/domains/test_domain_public_views.py
index a5e2401d0..f56cd11ed 100644
--- a/backend/tests/baserow/contrib/builder/api/domains/test_domain_public_views.py
+++ b/backend/tests/baserow/contrib/builder/api/domains/test_domain_public_views.py
@@ -18,7 +18,7 @@ def test_get_public_builder_by_domain_name(api_client, data_fixture):
     page = data_fixture.create_builder_page(user=user, builder=builder_to)
     page2 = data_fixture.create_builder_page(user=user, builder=builder_to)
 
-    domain = data_fixture.create_builder_domain(
+    domain = data_fixture.create_builder_custom_domain(
         domain_name="test.getbaserow.io", published_to=builder_to
     )
 
@@ -56,7 +56,7 @@ def test_get_builder_missing_domain_name(api_client, data_fixture):
     page = data_fixture.create_builder_page(user=user)
     page2 = data_fixture.create_builder_page(builder=page.builder, user=user)
 
-    domain = data_fixture.create_builder_domain(
+    domain = data_fixture.create_builder_custom_domain(
         domain_name="test.getbaserow.io", published_to=page.builder
     )
 
@@ -78,7 +78,7 @@ def test_get_non_public_builder(api_client, data_fixture):
     user, token = data_fixture.create_user_and_token()
     page = data_fixture.create_builder_page(user=user)
     page2 = data_fixture.create_builder_page(builder=page.builder, user=user)
-    domain = data_fixture.create_builder_domain(
+    domain = data_fixture.create_builder_custom_domain(
         domain_name="test.getbaserow.io", builder=page.builder
     )
 
@@ -148,7 +148,7 @@ def test_publish_builder(mock_run_async_job, api_client, data_fixture):
     page = data_fixture.create_builder_page(builder=builder_from, user=user)
     page2 = data_fixture.create_builder_page(builder=builder_from, user=user)
 
-    domain = data_fixture.create_builder_domain(
+    domain = data_fixture.create_builder_custom_domain(
         domain_name="test.getbaserow.io", builder=builder_from
     )
 
@@ -182,7 +182,7 @@ def test_get_elements_of_public_builder(api_client, data_fixture):
     element2 = data_fixture.create_builder_heading_element(page=page)
     element3 = data_fixture.create_builder_paragraph_element(page=page)
 
-    domain = data_fixture.create_builder_domain(
+    domain = data_fixture.create_builder_custom_domain(
         domain_name="test.getbaserow.io",
         published_to=page.builder,
         builder=builder_from,
@@ -238,7 +238,7 @@ def test_get_data_source_of_public_builder(api_client, data_fixture):
         page=page
     )
 
-    domain = data_fixture.create_builder_domain(
+    domain = data_fixture.create_builder_custom_domain(
         domain_name="test.getbaserow.io",
         published_to=page.builder,
         builder=builder_from,
diff --git a/backend/tests/baserow/contrib/builder/api/domains/test_domain_views.py b/backend/tests/baserow/contrib/builder/api/domains/test_domain_views.py
index 9c06e4799..b4986fc87 100644
--- a/backend/tests/baserow/contrib/builder/api/domains/test_domain_views.py
+++ b/backend/tests/baserow/contrib/builder/api/domains/test_domain_views.py
@@ -1,3 +1,4 @@
+from django.test.utils import override_settings
 from django.urls import reverse
 
 import pytest
@@ -9,13 +10,15 @@ from rest_framework.status import (
     HTTP_404_NOT_FOUND,
 )
 
+from baserow.contrib.builder.domains.domain_types import CustomDomainType, SubDomainType
+
 
 @pytest.mark.django_db
 def test_get_domains(api_client, data_fixture):
     user, token = data_fixture.create_user_and_token()
     builder = data_fixture.create_builder_application(user=user)
-    domain_one = data_fixture.create_builder_domain(builder=builder)
-    domain_two = data_fixture.create_builder_domain(builder=builder)
+    domain_one = data_fixture.create_builder_custom_domain(builder=builder)
+    domain_two = data_fixture.create_builder_custom_domain(builder=builder)
 
     url = reverse(
         "api:builder:builder_id:domains:list", kwargs={"builder_id": builder.id}
@@ -62,7 +65,7 @@ def test_create_domain(api_client, data_fixture):
     )
     response = api_client.post(
         url,
-        {"domain_name": domain_name},
+        {"type": CustomDomainType.type, "domain_name": domain_name},
         format="json",
         HTTP_AUTHORIZATION=f"JWT {token}",
     )
@@ -82,7 +85,7 @@ def test_create_domain_user_not_in_workspace(api_client, data_fixture):
     )
     response = api_client.post(
         url,
-        {"domain_name": "test.com"},
+        {"type": CustomDomainType.type, "domain_name": "test.com"},
         format="json",
         HTTP_AUTHORIZATION=f"JWT {token}",
     )
@@ -98,7 +101,7 @@ def test_create_domain_application_does_not_exist(api_client, data_fixture):
     url = reverse("api:builder:builder_id:domains:list", kwargs={"builder_id": 9999})
     response = api_client.post(
         url,
-        {"domain_name": "test.com"},
+        {"type": CustomDomainType.type, "domain_name": "test.com"},
         format="json",
         HTTP_AUTHORIZATION=f"JWT {token}",
     )
@@ -114,14 +117,14 @@ def test_create_domain_domain_already_exists(api_client, data_fixture):
 
     domain_name = "test.com"
 
-    data_fixture.create_builder_domain(builder=builder, domain_name=domain_name)
+    data_fixture.create_builder_custom_domain(builder=builder, domain_name=domain_name)
 
     url = reverse(
         "api:builder:builder_id:domains:list", kwargs={"builder_id": builder.id}
     )
     response = api_client.post(
         url,
-        {"domain_name": domain_name},
+        {"type": CustomDomainType.type, "domain_name": domain_name},
         format="json",
         HTTP_AUTHORIZATION=f"JWT {token}",
     )
@@ -142,7 +145,7 @@ def test_create_domain_invalid_domain_name(api_client, data_fixture):
     )
     response = api_client.post(
         url,
-        {"domain_name": domain_name},
+        {"type": CustomDomainType.type, "domain_name": domain_name},
         format="json",
         HTTP_AUTHORIZATION=f"JWT {token}",
     )
@@ -151,11 +154,36 @@ def test_create_domain_invalid_domain_name(api_client, data_fixture):
     assert response.json()["error"] == "ERROR_REQUEST_BODY_VALIDATION"
 
 
+@pytest.mark.django_db
+@override_settings(BASEROW_BUILDER_DOMAINS=["test.com"])
+def test_create_invalid_sub_domain(api_client, data_fixture):
+    user, token = data_fixture.create_user_and_token()
+    builder = data_fixture.create_builder_application(user=user)
+
+    domain_name = "hello.nottest.com"
+
+    url = reverse(
+        "api:builder:builder_id:domains:list", kwargs={"builder_id": builder.id}
+    )
+    response = api_client.post(
+        url,
+        {"type": SubDomainType.type, "domain_name": domain_name},
+        format="json",
+        HTTP_AUTHORIZATION=f"JWT {token}",
+    )
+
+    response_json = response.json()
+    assert response.status_code == HTTP_400_BAD_REQUEST
+    assert response_json["error"] == "ERROR_SUB_DOMAIN_HAS_INVALID_DOMAIN_NAME"
+    assert "test.com" in response_json["detail"]
+    assert "nottest.com" in response_json["detail"]
+
+
 @pytest.mark.django_db
 def test_update_domain(api_client, data_fixture):
     user, token = data_fixture.create_user_and_token()
     builder = data_fixture.create_builder_application(user=user)
-    domain = data_fixture.create_builder_domain(
+    domain = data_fixture.create_builder_custom_domain(
         builder=builder, domain_name="something.com"
     )
 
@@ -187,12 +215,45 @@ def test_update_domain_domain_does_not_exist(api_client, data_fixture):
     assert response.json()["error"] == "ERROR_DOMAIN_DOES_NOT_EXIST"
 
 
+@pytest.mark.django_db
+def test_update_domain_with_same_name(api_client, data_fixture):
+    user, token = data_fixture.create_user_and_token()
+    domain = data_fixture.create_builder_custom_domain(user=user)
+
+    url = reverse("api:builder:domains:item", kwargs={"domain_id": domain.id})
+    response = api_client.patch(
+        url,
+        {"domain_name": domain.domain_name},
+        format="json",
+        HTTP_AUTHORIZATION=f"JWT {token}",
+    )
+
+    assert response.status_code == HTTP_200_OK
+
+
+@pytest.mark.django_db
+def test_update_domain_name_uniqueness(api_client, data_fixture):
+    user, token = data_fixture.create_user_and_token()
+    domain = data_fixture.create_builder_custom_domain(user=user)
+    domain_2 = data_fixture.create_builder_custom_domain(user=user)
+
+    url = reverse("api:builder:domains:item", kwargs={"domain_id": domain.id})
+    response = api_client.patch(
+        url,
+        {"domain_name": domain_2.domain_name},
+        format="json",
+        HTTP_AUTHORIZATION=f"JWT {token}",
+    )
+
+    assert response.status_code == HTTP_400_BAD_REQUEST, response.json()
+
+
 @pytest.mark.django_db
 def test_order_domains(api_client, data_fixture):
     user, token = data_fixture.create_user_and_token()
     builder = data_fixture.create_builder_application(user=user)
-    domain_one = data_fixture.create_builder_domain(builder=builder, order=1)
-    domain_two = data_fixture.create_builder_domain(builder=builder, order=2)
+    domain_one = data_fixture.create_builder_custom_domain(builder=builder, order=1)
+    domain_two = data_fixture.create_builder_custom_domain(builder=builder, order=2)
 
     url = reverse(
         "api:builder:builder_id:domains:order", kwargs={"builder_id": builder.id}
@@ -211,8 +272,8 @@ def test_order_domains(api_client, data_fixture):
 def test_order_domains_user_not_in_workspace(api_client, data_fixture):
     user, token = data_fixture.create_user_and_token()
     builder = data_fixture.create_builder_application()
-    domain_one = data_fixture.create_builder_domain(builder=builder, order=1)
-    domain_two = data_fixture.create_builder_domain(builder=builder, order=2)
+    domain_one = data_fixture.create_builder_custom_domain(builder=builder, order=1)
+    domain_two = data_fixture.create_builder_custom_domain(builder=builder, order=2)
 
     url = reverse(
         "api:builder:builder_id:domains:order", kwargs={"builder_id": builder.id}
@@ -232,8 +293,8 @@ def test_order_domains_user_not_in_workspace(api_client, data_fixture):
 def test_order_domains_domain_not_in_builder(api_client, data_fixture):
     user, token = data_fixture.create_user_and_token()
     builder = data_fixture.create_builder_application(user=user)
-    domain_one = data_fixture.create_builder_domain(builder=builder, order=1)
-    domain_two = data_fixture.create_builder_domain(order=2)
+    domain_one = data_fixture.create_builder_custom_domain(builder=builder, order=1)
+    domain_two = data_fixture.create_builder_custom_domain(order=2)
 
     url = reverse(
         "api:builder:builder_id:domains:order", kwargs={"builder_id": builder.id}
@@ -253,8 +314,8 @@ def test_order_domains_domain_not_in_builder(api_client, data_fixture):
 def test_order_domains_application_does_not_exist(api_client, data_fixture):
     user, token = data_fixture.create_user_and_token()
     builder = data_fixture.create_builder_application(user=user)
-    domain_one = data_fixture.create_builder_domain(builder=builder, order=1)
-    domain_two = data_fixture.create_builder_domain(builder=builder, order=2)
+    domain_one = data_fixture.create_builder_custom_domain(builder=builder, order=1)
+    domain_two = data_fixture.create_builder_custom_domain(builder=builder, order=2)
 
     url = reverse("api:builder:builder_id:domains:order", kwargs={"builder_id": 99999})
     response = api_client.post(
@@ -272,7 +333,7 @@ def test_order_domains_application_does_not_exist(api_client, data_fixture):
 def test_delete_domain(api_client, data_fixture):
     user, token = data_fixture.create_user_and_token()
     builder = data_fixture.create_builder_application(user=user)
-    domain = data_fixture.create_builder_domain(builder=builder, order=1)
+    domain = data_fixture.create_builder_custom_domain(builder=builder, order=1)
 
     url = reverse("api:builder:domains:item", kwargs={"domain_id": domain.id})
     response = api_client.delete(
@@ -288,7 +349,7 @@ def test_delete_domain(api_client, data_fixture):
 def test_delete_domain_user_not_in_workspace(api_client, data_fixture):
     user, token = data_fixture.create_user_and_token()
     builder = data_fixture.create_builder_application()
-    domain = data_fixture.create_builder_domain(builder=builder, order=1)
+    domain = data_fixture.create_builder_custom_domain(builder=builder, order=1)
 
     url = reverse("api:builder:domains:item", kwargs={"domain_id": domain.id})
     response = api_client.delete(
diff --git a/backend/tests/baserow/contrib/builder/domains/test_domain_handler.py b/backend/tests/baserow/contrib/builder/domains/test_domain_handler.py
index 4e868c75e..de8f8b52a 100644
--- a/backend/tests/baserow/contrib/builder/domains/test_domain_handler.py
+++ b/backend/tests/baserow/contrib/builder/domains/test_domain_handler.py
@@ -1,5 +1,6 @@
 import pytest
 
+from baserow.contrib.builder.domains.domain_types import CustomDomainType
 from baserow.contrib.builder.domains.exceptions import (
     DomainDoesNotExist,
     DomainNotInBuilder,
@@ -13,7 +14,7 @@ from baserow.core.utils import Progress
 
 @pytest.mark.django_db
 def test_get_domain(data_fixture):
-    domain = data_fixture.create_builder_domain()
+    domain = data_fixture.create_builder_custom_domain()
     assert DomainHandler().get_domain(domain.id).id == domain.id
 
 
@@ -25,7 +26,7 @@ def test_get_domain_domain_does_not_exist(data_fixture):
 
 @pytest.mark.django_db
 def test_get_domain_base_queryset(data_fixture, django_assert_num_queries):
-    domain = data_fixture.create_builder_domain()
+    domain = data_fixture.create_builder_custom_domain()
 
     # Without selecting related
     domain = DomainHandler().get_domain(domain.id)
@@ -42,8 +43,8 @@ def test_get_domain_base_queryset(data_fixture, django_assert_num_queries):
 @pytest.mark.django_db
 def test_get_domains(data_fixture):
     builder = data_fixture.create_builder_application()
-    domain_one = data_fixture.create_builder_domain(builder=builder)
-    domain_two = data_fixture.create_builder_domain(builder=builder)
+    domain_one = data_fixture.create_builder_custom_domain(builder=builder)
+    domain_two = data_fixture.create_builder_custom_domain(builder=builder)
 
     domains = list(DomainHandler().get_domains(builder))
     domain_ids = [domain.id for domain in domains]
@@ -58,7 +59,9 @@ def test_create_domain(data_fixture):
     builder = data_fixture.create_builder_application()
     expected_order = Domain.get_last_order(builder)
 
-    domain = DomainHandler().create_domain(builder, "test.com")
+    domain = DomainHandler().create_domain(
+        CustomDomainType(), builder, domain_name="test.com"
+    )
 
     assert domain.order == expected_order
     assert domain.domain_name == "test.com"
@@ -66,7 +69,7 @@ def test_create_domain(data_fixture):
 
 @pytest.mark.django_db
 def test_delete_domain(data_fixture):
-    domain = data_fixture.create_builder_domain()
+    domain = data_fixture.create_builder_custom_domain()
 
     DomainHandler().delete_domain(domain)
 
@@ -75,7 +78,7 @@ def test_delete_domain(data_fixture):
 
 @pytest.mark.django_db
 def test_update_domain(data_fixture):
-    domain = data_fixture.create_builder_domain(domain_name="test.com")
+    domain = data_fixture.create_builder_custom_domain(domain_name="test.com")
 
     DomainHandler().update_domain(domain, domain_name="new.com")
 
@@ -87,8 +90,8 @@ def test_update_domain(data_fixture):
 @pytest.mark.django_db
 def test_order_domains(data_fixture):
     builder = data_fixture.create_builder_application()
-    domain_one = data_fixture.create_builder_domain(builder=builder, order=1)
-    domain_two = data_fixture.create_builder_domain(builder=builder, order=2)
+    domain_one = data_fixture.create_builder_custom_domain(builder=builder, order=1)
+    domain_two = data_fixture.create_builder_custom_domain(builder=builder, order=2)
 
     assert DomainHandler().order_domains(builder, [domain_two.id, domain_one.id]) == [
         domain_two.id,
@@ -105,8 +108,8 @@ def test_order_domains(data_fixture):
 @pytest.mark.django_db
 def test_order_domains_domain_not_in_builder(data_fixture):
     builder = data_fixture.create_builder_application()
-    domain_one = data_fixture.create_builder_domain(builder=builder, order=1)
-    domain_two = data_fixture.create_builder_domain(builder=builder, order=2)
+    domain_one = data_fixture.create_builder_custom_domain(builder=builder, order=1)
+    domain_two = data_fixture.create_builder_custom_domain(builder=builder, order=2)
 
     base_qs = Domain.objects.filter(id=domain_two.id)
 
@@ -120,7 +123,7 @@ def test_order_domains_domain_not_in_builder(data_fixture):
 def test_get_public_builder_by_name(data_fixture):
     builder = data_fixture.create_builder_application()
     builder_to = data_fixture.create_builder_application(workspace=None)
-    domain1 = data_fixture.create_builder_domain(
+    domain1 = data_fixture.create_builder_custom_domain(
         builder=builder, published_to=builder_to
     )
 
@@ -132,7 +135,7 @@ def test_get_public_builder_by_name(data_fixture):
 @pytest.mark.django_db
 def test_get_published_builder_by_missing_domain_name(data_fixture):
     builder = data_fixture.create_builder_application()
-    domain1 = data_fixture.create_builder_domain(builder=builder)
+    domain1 = data_fixture.create_builder_custom_domain(builder=builder)
 
     with pytest.raises(BuilderDoesNotExist):
         DomainHandler().get_public_builder_by_domain_name(domain1.domain_name)
@@ -142,7 +145,7 @@ def test_get_published_builder_by_missing_domain_name(data_fixture):
 def test_get_published_builder_for_trashed_builder(data_fixture):
     builder = data_fixture.create_builder_application(trashed=True)
     builder_to = data_fixture.create_builder_application(workspace=None)
-    domain1 = data_fixture.create_builder_domain(
+    domain1 = data_fixture.create_builder_custom_domain(
         builder=builder, published_to=builder_to
     )
 
@@ -151,7 +154,7 @@ def test_get_published_builder_for_trashed_builder(data_fixture):
 
     builder = data_fixture.create_builder_application()
     builder_to = data_fixture.create_builder_application(workspace=None)
-    domain1 = data_fixture.create_builder_domain(
+    domain1 = data_fixture.create_builder_custom_domain(
         builder=builder, published_to=builder_to, trashed=True
     )
 
@@ -163,7 +166,7 @@ def test_get_published_builder_for_trashed_builder(data_fixture):
 def test_domain_publishing(data_fixture):
     builder = data_fixture.create_builder_application()
 
-    domain1 = data_fixture.create_builder_domain(builder=builder)
+    domain1 = data_fixture.create_builder_custom_domain(builder=builder)
 
     page1 = data_fixture.create_builder_page(builder=builder)
     page2 = data_fixture.create_builder_page(builder=builder)
diff --git a/backend/tests/baserow/contrib/builder/domains/test_domain_permission_manager.py b/backend/tests/baserow/contrib/builder/domains/test_domain_permission_manager.py
index 0a2535e3d..3f94f652e 100644
--- a/backend/tests/baserow/contrib/builder/domains/test_domain_permission_manager.py
+++ b/backend/tests/baserow/contrib/builder/domains/test_domain_permission_manager.py
@@ -22,10 +22,10 @@ def test_allow_public_builder_manager_type(data_fixture):
     user = data_fixture.create_user()
     builder = data_fixture.create_builder_application(user=user)
     builder_to = data_fixture.create_builder_application(workspace=None)
-    domain1 = data_fixture.create_builder_domain(
+    domain1 = data_fixture.create_builder_custom_domain(
         builder=builder, published_to=builder_to
     )
-    domain2 = data_fixture.create_builder_domain(builder=builder)
+    domain2 = data_fixture.create_builder_custom_domain(builder=builder)
 
     public_page = data_fixture.create_builder_page(builder=builder_to)
     non_public_page = data_fixture.create_builder_page(builder=builder)
diff --git a/backend/tests/baserow/contrib/builder/domains/test_domain_service.py b/backend/tests/baserow/contrib/builder/domains/test_domain_service.py
index 9f30a0efa..4b3017141 100644
--- a/backend/tests/baserow/contrib/builder/domains/test_domain_service.py
+++ b/backend/tests/baserow/contrib/builder/domains/test_domain_service.py
@@ -2,6 +2,7 @@ from unittest.mock import patch
 
 import pytest
 
+from baserow.contrib.builder.domains.domain_types import CustomDomainType
 from baserow.contrib.builder.domains.exceptions import DomainNotInBuilder
 from baserow.contrib.builder.domains.models import Domain
 from baserow.contrib.builder.domains.service import DomainService
@@ -15,7 +16,9 @@ def test_domain_created_signal_sent(domain_created_mock, data_fixture):
     user = data_fixture.create_user()
     builder = data_fixture.create_builder_application(user=user)
 
-    domain = DomainService().create_domain(user, builder, "test")
+    domain = DomainService().create_domain(
+        user, CustomDomainType(), builder, domain_name="test"
+    )
 
     assert domain_created_mock.called_with(domain=domain, user=user)
 
@@ -26,7 +29,9 @@ def test_create_domain_user_not_in_workspace(data_fixture):
     builder = data_fixture.create_builder_application()
 
     with pytest.raises(UserNotInWorkspace):
-        DomainService().create_domain(user, builder, "test")
+        DomainService().create_domain(
+            user, CustomDomainType, builder, domain_name="test"
+        )
 
 
 @patch("baserow.contrib.builder.domains.service.domain_deleted")
@@ -34,7 +39,7 @@ def test_create_domain_user_not_in_workspace(data_fixture):
 def test_domain_deleted_signal_sent(domain_deleted_mock, data_fixture):
     user = data_fixture.create_user()
     builder = data_fixture.create_builder_application(user=user)
-    domain = data_fixture.create_builder_domain(builder=builder)
+    domain = data_fixture.create_builder_custom_domain(builder=builder)
 
     DomainService().delete_domain(user, domain)
 
@@ -49,7 +54,9 @@ def test_delete_domain_user_not_in_workspace(data_fixture):
     user_unrelated = data_fixture.create_user()
     builder = data_fixture.create_builder_application(user=user)
 
-    domain = DomainService().create_domain(user, builder, "test")
+    domain = DomainService().create_domain(
+        user, CustomDomainType(), builder, domain_name="test"
+    )
 
     with pytest.raises(UserNotInWorkspace):
         DomainService().delete_domain(user_unrelated, domain)
@@ -61,7 +68,7 @@ def test_delete_domain_user_not_in_workspace(data_fixture):
 def test_get_domain_user_not_in_workspace(data_fixture):
     user = data_fixture.create_user()
     builder = data_fixture.create_builder_application()
-    domain = data_fixture.create_builder_domain(builder=builder)
+    domain = data_fixture.create_builder_custom_domain(builder=builder)
 
     with pytest.raises(UserNotInWorkspace):
         DomainService().get_domain(user, domain.id)
@@ -71,7 +78,7 @@ def test_get_domain_user_not_in_workspace(data_fixture):
 def test_get_domains_user_not_in_workspace(data_fixture):
     user = data_fixture.create_user()
     builder = data_fixture.create_builder_application()
-    domain = data_fixture.create_builder_domain(builder=builder)
+    domain = data_fixture.create_builder_custom_domain(builder=builder)
 
     with pytest.raises(UserNotInWorkspace):
         DomainService().get_domains(user, builder)
@@ -81,8 +88,8 @@ def test_get_domains_user_not_in_workspace(data_fixture):
 def test_get_domains_partial_permissions(data_fixture, stub_check_permissions):
     user = data_fixture.create_user()
     builder = data_fixture.create_builder_application(user=user)
-    domain_with_access = data_fixture.create_builder_domain(builder=builder)
-    domain_without_access = data_fixture.create_builder_domain(builder=builder)
+    domain_with_access = data_fixture.create_builder_custom_domain(builder=builder)
+    domain_without_access = data_fixture.create_builder_custom_domain(builder=builder)
 
     def exclude_domain_without_access(
         actor,
@@ -107,7 +114,7 @@ def test_get_domains_partial_permissions(data_fixture, stub_check_permissions):
 def test_domain_updated_signal_sent(domain_updated_mock, data_fixture):
     user = data_fixture.create_user()
     builder = data_fixture.create_builder_application(user=user)
-    domain = data_fixture.create_builder_domain(builder=builder)
+    domain = data_fixture.create_builder_custom_domain(builder=builder)
 
     DomainService().update_domain(user, domain, domain_name="new.com")
 
@@ -118,7 +125,7 @@ def test_domain_updated_signal_sent(domain_updated_mock, data_fixture):
 def test_update_domain_user_not_in_workspace(data_fixture):
     user = data_fixture.create_user()
     builder = data_fixture.create_builder_application()
-    domain = data_fixture.create_builder_domain(builder=builder)
+    domain = data_fixture.create_builder_custom_domain(builder=builder)
 
     with pytest.raises(UserNotInWorkspace):
         DomainService().update_domain(user, domain, domain_name="test.com")
@@ -128,7 +135,7 @@ def test_update_domain_user_not_in_workspace(data_fixture):
 def test_update_domain_invalid_values(data_fixture):
     user = data_fixture.create_user()
     builder = data_fixture.create_builder_application(user=user)
-    domain = data_fixture.create_builder_domain(builder=builder)
+    domain = data_fixture.create_builder_custom_domain(builder=builder)
 
     domain_updated = DomainService().update_domain(user, domain, nonsense="hello")
 
@@ -140,8 +147,8 @@ def test_update_domain_invalid_values(data_fixture):
 def test_domains_reordered_signal_sent(domains_reordered_mock, data_fixture):
     user = data_fixture.create_user()
     builder = data_fixture.create_builder_application(user=user)
-    domain_one = data_fixture.create_builder_domain(builder=builder, order=1)
-    domain_two = data_fixture.create_builder_domain(builder=builder, order=2)
+    domain_one = data_fixture.create_builder_custom_domain(builder=builder, order=1)
+    domain_two = data_fixture.create_builder_custom_domain(builder=builder, order=2)
 
     full_order = DomainService().order_domains(
         user, builder, [domain_two.id, domain_one.id]
@@ -156,8 +163,8 @@ def test_domains_reordered_signal_sent(domains_reordered_mock, data_fixture):
 def test_order_domains_user_not_in_workspace(data_fixture):
     user = data_fixture.create_user()
     builder = data_fixture.create_builder_application()
-    domain_one = data_fixture.create_builder_domain(builder=builder, order=1)
-    domain_two = data_fixture.create_builder_domain(builder=builder, order=2)
+    domain_one = data_fixture.create_builder_custom_domain(builder=builder, order=1)
+    domain_two = data_fixture.create_builder_custom_domain(builder=builder, order=2)
 
     with pytest.raises(UserNotInWorkspace):
         DomainService().order_domains(user, builder, [domain_two.id, domain_one.id])
@@ -167,8 +174,8 @@ def test_order_domains_user_not_in_workspace(data_fixture):
 def test_order_domains_domain_not_in_builder(data_fixture):
     user = data_fixture.create_user()
     builder = data_fixture.create_builder_application(user=user)
-    domain_one = data_fixture.create_builder_domain(builder=builder, order=1)
-    domain_two = data_fixture.create_builder_domain(order=2)
+    domain_one = data_fixture.create_builder_custom_domain(builder=builder, order=1)
+    domain_two = data_fixture.create_builder_custom_domain(order=2)
 
     with pytest.raises(DomainNotInBuilder):
         DomainService().order_domains(user, builder, [domain_two.id, domain_one.id])
@@ -179,7 +186,7 @@ def test_get_published_builder_by_domain_name(data_fixture):
     user = data_fixture.create_user()
     builder = data_fixture.create_builder_application(user=user)
     builder_to = data_fixture.create_builder_application(workspace=None)
-    domain1 = data_fixture.create_builder_domain(
+    domain1 = data_fixture.create_builder_custom_domain(
         builder=builder, published_to=builder_to
     )
 
@@ -197,7 +204,7 @@ def test_get_published_builder_by_domain_name_unauthorized(
     user = data_fixture.create_user()
     builder = data_fixture.create_builder_application(user=user)
     builder_to = data_fixture.create_builder_application(workspace=None)
-    domain1 = data_fixture.create_builder_domain(
+    domain1 = data_fixture.create_builder_custom_domain(
         builder=builder, published_to=builder_to
     )
 
@@ -213,7 +220,7 @@ def test_async_publish_domain(mock_run_async_job, data_fixture):
     user = data_fixture.create_user()
     builder = data_fixture.create_builder_application(user=user)
 
-    domain1 = data_fixture.create_builder_domain(builder=builder)
+    domain1 = data_fixture.create_builder_custom_domain(builder=builder)
 
     job = DomainService().async_publish(user, domain1)
 
@@ -225,7 +232,7 @@ def test_async_publish_domain(mock_run_async_job, data_fixture):
 @pytest.mark.django_db(transaction=True)
 def test_async_publish_domain_no_permission(data_fixture, stub_check_permissions):
     user = data_fixture.create_user()
-    domain1 = data_fixture.create_builder_domain()
+    domain1 = data_fixture.create_builder_custom_domain()
 
     with stub_check_permissions(raise_permission_denied=True), pytest.raises(
         PermissionException
@@ -239,7 +246,7 @@ def test_publish_domain(domain_updated_mock, data_fixture):
     user = data_fixture.create_user()
     builder = data_fixture.create_builder_application(user=user)
 
-    domain1 = data_fixture.create_builder_domain(builder=builder)
+    domain1 = data_fixture.create_builder_custom_domain(builder=builder)
 
     progress = Progress(100)
     domain = DomainService().publish(user, domain1, progress)
@@ -252,7 +259,7 @@ def test_publish_domain_unauthorized(data_fixture, stub_check_permissions):
     user = data_fixture.create_user()
     builder = data_fixture.create_builder_application(user=user)
 
-    domain1 = data_fixture.create_builder_domain(builder=builder)
+    domain1 = data_fixture.create_builder_custom_domain(builder=builder)
 
     progress = Progress(100)
 
diff --git a/backend/tests/baserow/contrib/builder/domains/test_publish_domain_job_type.py b/backend/tests/baserow/contrib/builder/domains/test_publish_domain_job_type.py
index ad662b89e..090d46209 100644
--- a/backend/tests/baserow/contrib/builder/domains/test_publish_domain_job_type.py
+++ b/backend/tests/baserow/contrib/builder/domains/test_publish_domain_job_type.py
@@ -9,7 +9,7 @@ from baserow.core.jobs.handler import JobHandler
 def test_publish_domain_job_type(data_fixture):
     user = data_fixture.create_user()
     builder = data_fixture.create_builder_application(user=user)
-    domain1 = data_fixture.create_builder_domain(builder=builder)
+    domain1 = data_fixture.create_builder_custom_domain(builder=builder)
 
     publish_domain_job = JobHandler().create_and_start_job(
         user,
diff --git a/backend/tests/baserow/contrib/builder/test_builder_application_type.py b/backend/tests/baserow/contrib/builder/test_builder_application_type.py
index b852d443a..862567ccb 100644
--- a/backend/tests/baserow/contrib/builder/test_builder_application_type.py
+++ b/backend/tests/baserow/contrib/builder/test_builder_application_type.py
@@ -410,7 +410,7 @@ def test_builder_application_import(data_fixture):
 def test_delete_builder_application_with_published_builder(data_fixture):
     builder = data_fixture.create_builder_application()
     builder_to = data_fixture.create_builder_application(workspace=None)
-    domain1 = data_fixture.create_builder_domain(
+    domain1 = data_fixture.create_builder_custom_domain(
         builder=builder, published_to=builder_to
     )
 
diff --git a/docker-compose.local-build.yml b/docker-compose.local-build.yml
index 523c3e181..ca90bd07a 100644
--- a/docker-compose.local-build.yml
+++ b/docker-compose.local-build.yml
@@ -139,6 +139,7 @@ x-backend-variables: &backend-variables
   BASEROW_DISABLE_LOCKED_MIGRATIONS:
   BASEROW_USE_PG_FULLTEXT_SEARCH:
   BASEROW_AUTO_VACUUM:
+  BASEROW_BUILDER_DOMAINS:
 
 services:
   # A caddy reverse proxy sitting in-front of all the services. Responsible for routing
@@ -206,6 +207,7 @@ services:
       BASEROW_USE_PG_FULLTEXT_SEARCH:
       BASEROW_UNIQUE_ROW_VALUES_SIZE_LIMIT:
       BASEROW_ROW_PAGE_SIZE_LIMIT:
+      BASEROW_BUILDER_DOMAINS:
     depends_on:
       - backend
     networks:
diff --git a/docker-compose.no-caddy.yml b/docker-compose.no-caddy.yml
index d31cd8d4d..3c7f3a505 100644
--- a/docker-compose.no-caddy.yml
+++ b/docker-compose.no-caddy.yml
@@ -158,6 +158,7 @@ x-backend-variables: &backend-variables
   BASEROW_DISABLE_LOCKED_MIGRATIONS:
   BASEROW_USE_PG_FULLTEXT_SEARCH:
   BASEROW_AUTO_VACUUM:
+  BASEROW_BUILDER_DOMAINS:
 
 services:
   backend:
@@ -201,6 +202,7 @@ services:
       BASEROW_USE_PG_FULLTEXT_SEARCH:
       BASEROW_UNIQUE_ROW_VALUES_SIZE_LIMIT:
       BASEROW_ROW_PAGE_SIZE_LIMIT:
+      BASEROW_BUILDER_DOMAINS:
     depends_on:
       - backend
     networks:
diff --git a/docker-compose.yml b/docker-compose.yml
index 7866bffb1..5081a1765 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -157,6 +157,7 @@ x-backend-variables: &backend-variables
   BASEROW_DISABLE_LOCKED_MIGRATIONS:
   BASEROW_USE_PG_FULLTEXT_SEARCH:
   BASEROW_AUTO_VACUUM:
+  BASEROW_BUILDER_DOMAINS:
 
 services:
   # A caddy reverse proxy sitting in-front of all the services. Responsible for routing
@@ -220,6 +221,7 @@ services:
       POSTHOG_HOST:
       BASEROW_UNIQUE_ROW_VALUES_SIZE_LIMIT:
       BASEROW_ROW_PAGE_SIZE_LIMIT:
+      BASEROW_BUILDER_DOMAINS:
     depends_on:
       - backend
     networks:
diff --git a/docs/installation/configuration.md b/docs/installation/configuration.md
index 9e36d1324..320662bae 100644
--- a/docs/installation/configuration.md
+++ b/docs/installation/configuration.md
@@ -139,6 +139,12 @@ The installation methods referred to in the variable descriptions are:
 | BASEROW\_INITIAL\_CREATE\_SYNC\_TABLE\_DATA\_LIMIT | The maximum number of rows you can import in a synchronous way                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     | 5000                   |
 | BASEROW\_MAX\_ROW\_REPORT\_ERROR\_COUNT            | The maximum row error count tolerated before a file import fails. Before this max error count the import will continue and the non failing rows will be imported and after it, no rows are imported at all.                                                                                                                                                                                                                                                                                                                                                                                                                                                        | 30                     |
 
+### Backend Application Builder Configuration
+| Name                      | Description                                                                                                              | Defaults               |
+|---------------------------|--------------------------------------------------------------------------------------------------------------------------|------------------------|
+| BASEROW\_BUILDER\_DOMAINS | A comma separated list of domain names that can be used as the domains to create sub domains in the application builder. |                        |
+
+
 ### User file upload Configuration
 
 Baserow needs somewhere to store the following types of files:
@@ -242,6 +248,8 @@ domain than your Baserow, you need to make sure CORS is configured correctly.
 | BASEROW_MAX_SNAPSHOTS_PER_GROUP               | Controls how many application snapshots can be created per workspace.                                                                                                                                                                                                                                            | -1 (unlimited) |
 | BASEROW\_USE\_PG\_FULLTEXT\_SEARCH            | By default, Baserow will use Postgres full-text as its search backend. If the product is installed on a system with limited disk space, and less accurate results / degraded search performance is acceptable, then switch this setting off by setting it to false.                                                                     | true           |
 | BASEROW\_UNIQUE\_ROW\_VALUES\_SIZE\_LIMIT             | Sets the limit for the automatic detection of multiselect options when converting a text field to a multiselect field. Increase the value to detect more options automatically, but consider performance implications.                                                                                       | 100             |
+| BASEROW\_BUILDER\_DOMAINS | A comma separated list of domain names that can be used as the domains to create sub domains in the application builder. |                        |
+
 
 ### SSO Configuration
 | Name                                                         | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    | Defaults |
diff --git a/web-frontend/modules/builder/components/domain/CustomDomainForm.vue b/web-frontend/modules/builder/components/domain/CustomDomainForm.vue
index 530cd8b33..4dea4dfb6 100644
--- a/web-frontend/modules/builder/components/domain/CustomDomainForm.vue
+++ b/web-frontend/modules/builder/components/domain/CustomDomainForm.vue
@@ -29,26 +29,25 @@
       </div>
       <template v-if="serverErrors.domain_name">
         <div v-if="serverErrors.domain_name.code === 'invalid'" class="error">
-          {{ $t('customDomainForm.invalidDomain') }}
+          {{ $t('domainForm.invalidDomain') }}
         </div>
         <div v-if="serverErrors.domain_name.code === 'unique'" class="error">
-          {{ $t('customDomainForm.notUniqueDomain') }}
-        </div></template
-      >
+          {{ $t('domainForm.notUniqueDomain') }}
+        </div>
+      </template>
     </FormElement>
   </form>
 </template>
 
 <script>
-import form from '@baserow/modules/core/mixins/form'
 import { required, maxLength } from 'vuelidate/lib/validators'
+import domainForm from '@baserow/modules/builder/mixins/domainForm'
 
 export default {
-  name: 'DomainForm',
-  mixins: [form],
+  name: 'CustomDomainForm',
+  mixins: [domainForm],
   data() {
     return {
-      serverErrors: { domain_name: null },
       values: {
         domain_name: '',
       },
@@ -64,22 +63,5 @@ export default {
       },
     }
   },
-  methods: {
-    handleServerError(error) {
-      if (error.handler.code !== 'ERROR_REQUEST_BODY_VALIDATION') return false
-
-      this.serverErrors = Object.fromEntries(
-        Object.entries(error.handler.detail || {}).map(([key, value]) => [
-          key,
-          value[0],
-        ])
-      )
-
-      return true
-    },
-    hasError() {
-      return !this.isFormValid() || this.serverErrors.domain_name !== null
-    },
-  },
 }
 </script>
diff --git a/web-frontend/modules/builder/components/domain/DomainForm.vue b/web-frontend/modules/builder/components/domain/DomainForm.vue
index 99fc6ae00..36cbf7c50 100644
--- a/web-frontend/modules/builder/components/domain/DomainForm.vue
+++ b/web-frontend/modules/builder/components/domain/DomainForm.vue
@@ -4,15 +4,15 @@
     <Error :error="error"></Error>
     <FormElement class="control">
       <Dropdown
-        v-model="selectedDomainType"
+        v-model="selectedDomain"
         :show-search="false"
         class="domain-settings__domain-type"
       >
         <DropdownItem
-          v-for="domainType in domainTypes"
-          :key="domainType.getType()"
-          :name="domainType.name"
-          :value="domainType.getType()"
+          v-for="option in options"
+          :key="option.value.domain"
+          :name="option.name"
+          :value="option.value"
         />
       </Dropdown>
     </FormElement>
@@ -20,7 +20,9 @@
       :is="currentDomainType.formComponent"
       ref="domainForm"
       :builder="builder"
+      :domain="selectedDomain.domain"
       @submitted="createDomain($event)"
+      @error="formHasError = $event"
     />
     <div class="actions">
       <a @click="hideForm">
@@ -29,7 +31,7 @@
       </a>
       <button
         class="button button--large"
-        :disabled="createLoading || formHasError()"
+        :disabled="createLoading || formHasError"
         :class="{ 'button--loading': createLoading }"
         @click="onSubmit"
       >
@@ -58,17 +60,29 @@ export default {
   },
   data() {
     return {
-      selectedDomainType: 'custom',
+      selectedDomain: { type: 'custom', domain: 'custom' },
       createLoading: false,
+      formHasError: false,
     }
   },
   computed: {
     domainTypes() {
-      return this.$registry.getAll('domain')
+      return Object.values(this.$registry.getAll('domain')) || []
+    },
+    selectedDomainType() {
+      return this.selectedDomain.type
     },
     currentDomainType() {
       return this.$registry.get('domain', this.selectedDomainType)
     },
+    options() {
+      return this.domainTypes.map((domainType) => domainType.options).flat()
+    },
+  },
+  watch: {
+    selectedDomainType() {
+      this.$refs?.domainForm?.reset()
+    },
   },
   methods: {
     ...mapActions({
@@ -92,13 +106,6 @@ export default {
       }
       this.createLoading = false
     },
-    formHasError() {
-      if (this.$refs.domainForm) {
-        return this.$refs.domainForm.hasError()
-      } else {
-        return false
-      }
-    },
     handleAnyError(error) {
       if (
         !this.$refs.domainForm ||
diff --git a/web-frontend/modules/builder/components/domain/SubDomainDetails.vue b/web-frontend/modules/builder/components/domain/SubDomainDetails.vue
new file mode 100644
index 000000000..58128815b
--- /dev/null
+++ b/web-frontend/modules/builder/components/domain/SubDomainDetails.vue
@@ -0,0 +1,28 @@
+<template>
+  <div>
+    <p>
+      {{ $t('subDomainDetails.text') }}
+    </p>
+    <div class="actions actions--right">
+      <a
+        class="button button--error"
+        :class="{ 'button--loading': domain._.loading }"
+        @click="$emit('delete')"
+      >
+        {{ $t('action.delete') }}
+      </a>
+    </div>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'SubDomainDetails',
+  props: {
+    domain: {
+      type: Object,
+      required: true,
+    },
+  },
+}
+</script>
diff --git a/web-frontend/modules/builder/components/domain/SubDomainForm.vue b/web-frontend/modules/builder/components/domain/SubDomainForm.vue
new file mode 100644
index 000000000..155e2441b
--- /dev/null
+++ b/web-frontend/modules/builder/components/domain/SubDomainForm.vue
@@ -0,0 +1,59 @@
+<template>
+  <form @submit.prevent="submit">
+    <FormElement :error="fieldHasErrors('domain_name')" class="control">
+      <FormInput
+        v-model="domainPrefix"
+        :label="$t('subDomainForm.domainNameLabel')"
+        :error="
+          $v.values.domain_name.$dirty && !$v.values.domain_name.required
+            ? $t('error.requiredField')
+            : $v.values.domain_name.$dirty && !$v.values.domain_name.maxLength
+            ? $t('error.maxLength', { max: 255 })
+            : serverErrors.domain_name &&
+              serverErrors.domain_name.code === 'invalid'
+            ? $t('domainForm.invalidDomain')
+            : serverErrors.domain_name &&
+              serverErrors.domain_name.code === 'unique'
+            ? $t('domainForm.notUniqueDomain')
+            : ''
+        "
+        @input="serverErrors.domain_name = null"
+      >
+        <template #suffix> .{{ domain }} </template>
+      </FormInput>
+    </FormElement>
+  </form>
+</template>
+
+<script>
+import { maxLength, required } from 'vuelidate/lib/validators'
+import domainForm from '@baserow/modules/builder/mixins/domainForm'
+
+export default {
+  name: 'SubDomainForm',
+  mixins: [domainForm],
+  data() {
+    return {
+      domainPrefix: '',
+      values: {
+        domain_name: '',
+      },
+    }
+  },
+  watch: {
+    domainPrefix(value) {
+      this.values.domain_name = `${value}.${this.domain}`
+    },
+  },
+  validations() {
+    return {
+      values: {
+        domain_name: {
+          required,
+          maxLength: maxLength(255),
+        },
+      },
+    }
+  },
+}
+</script>
diff --git a/web-frontend/modules/builder/domainTypes.js b/web-frontend/modules/builder/domainTypes.js
index f4471be1e..e9a694565 100644
--- a/web-frontend/modules/builder/domainTypes.js
+++ b/web-frontend/modules/builder/domainTypes.js
@@ -1,6 +1,8 @@
 import { Registerable } from '@baserow/modules/core/registry'
 import CustomDomainDetails from '@baserow/modules/builder/components/domain/CustomDomainDetails'
 import CustomDomainForm from '@baserow/modules/builder/components/domain/CustomDomainForm'
+import SubDomainForm from '@baserow/modules/builder/components/domain/SubDomainForm'
+import SubDomainDetails from '@baserow/modules/builder/components/domain/SubDomainDetails'
 
 export class DomainType extends Registerable {
   get name() {
@@ -10,6 +12,14 @@ export class DomainType extends Registerable {
   get detailsComponent() {
     return null
   }
+
+  get formComponent() {
+    return null
+  }
+
+  get options() {
+    return []
+  }
 }
 
 export class CustomDomainType extends DomainType {
@@ -28,4 +38,44 @@ export class CustomDomainType extends DomainType {
   get formComponent() {
     return CustomDomainForm
   }
+
+  get options() {
+    return [
+      {
+        name: this.app.i18n.t('domainTypes.customName'),
+        value: {
+          type: this.getType(),
+          domain: 'custom',
+        },
+      },
+    ]
+  }
+}
+
+export class SubDomainType extends DomainType {
+  static getType() {
+    return 'sub_domain'
+  }
+
+  get name() {
+    return this.app.i18n.t('domainTypes.subDomainName')
+  }
+
+  get options() {
+    return this.app.$config.BASEROW_BUILDER_DOMAINS.map((domain) => ({
+      name: this.app.i18n.t('domainTypes.subDomain', { domain }),
+      value: {
+        type: this.getType(),
+        domain,
+      },
+    }))
+  }
+
+  get detailsComponent() {
+    return SubDomainDetails
+  }
+
+  get formComponent() {
+    return SubDomainForm
+  }
 }
diff --git a/web-frontend/modules/builder/locales/en.json b/web-frontend/modules/builder/locales/en.json
index 18ffe9dbc..6917a9b1f 100644
--- a/web-frontend/modules/builder/locales/en.json
+++ b/web-frontend/modules/builder/locales/en.json
@@ -155,17 +155,27 @@
         "hostHeader": "Host",
         "valueHeader": "Value"
     },
+    "domainForm": {
+      "invalidDomain": "The provided domain name is invalid",
+      "notUniqueDomain": "The provided domain is already used"
+    },
     "customDomainForm": {
-        "domainNameLabel": "Domain name",
-        "invalidDomain": "The provided domain name is invalid",
-        "notUniqueDomain": "The provided domain is already used"
+        "domainNameLabel": "Domain name"
+    },
+    "subDomainForm": {
+        "domainNameLabel": "Domain name"
+    },
+    "subDomainDetails": {
+      "text": "The DNS settings of the domain have already been configured and checked. It works without making any additional changes."
     },
     "domainCard": {
         "refresh": "Refresh settings",
         "detailLabel": "Show details"
     },
     "domainTypes": {
-        "customName": "Custom domain"
+        "customName": "Custom domain",
+        "subDomainName": "Subdomain",
+        "subDomain": "Subdomain of {domain}"
     },
     "linkElement": {
         "noValue": "Unnamed..."
diff --git a/web-frontend/modules/builder/mixins/domainForm.js b/web-frontend/modules/builder/mixins/domainForm.js
new file mode 100644
index 000000000..78777d6e9
--- /dev/null
+++ b/web-frontend/modules/builder/mixins/domainForm.js
@@ -0,0 +1,46 @@
+import form from '@baserow/modules/core/mixins/form'
+
+export default {
+  mixins: [form],
+  props: {
+    domain: {
+      type: String,
+      required: true,
+    },
+  },
+  data() {
+    return {
+      serverErrors: {},
+    }
+  },
+  computed: {
+    hasServerErrors() {
+      return Object.values(this.serverErrors).some((value) => value !== null)
+    },
+    hasError() {
+      return !this.isFormValid() || this.hasServerErrors
+    },
+  },
+  watch: {
+    hasError: {
+      handler(value) {
+        this.$emit('error', value)
+      },
+      immediate: true,
+    },
+  },
+  methods: {
+    handleServerError(error) {
+      if (error.handler.code !== 'ERROR_REQUEST_BODY_VALIDATION') return false
+
+      this.serverErrors = Object.fromEntries(
+        Object.entries(error.handler.detail || {}).map(([key, value]) => [
+          key,
+          value[0],
+        ])
+      )
+
+      return true
+    },
+  },
+}
diff --git a/web-frontend/modules/builder/plugin.js b/web-frontend/modules/builder/plugin.js
index cd27559ef..ce4b519d9 100644
--- a/web-frontend/modules/builder/plugin.js
+++ b/web-frontend/modules/builder/plugin.js
@@ -49,7 +49,10 @@ import {
   VisibilityPageSidePanelType,
   StylePageSidePanelType,
 } from '@baserow/modules/builder/pageSidePanelTypes'
-import { CustomDomainType } from '@baserow/modules/builder/domainTypes'
+import {
+  CustomDomainType,
+  SubDomainType,
+} from '@baserow/modules/builder/domainTypes'
 import { PagePageSettingsType } from '@baserow/modules/builder/pageSettingsTypes'
 import {
   TextPathParamType,
@@ -158,6 +161,7 @@ export default (context) => {
   app.$registry.register('pageSidePanel', new EventsPageSidePanelType(context))
 
   app.$registry.register('domain', new CustomDomainType(context))
+  app.$registry.register('domain', new SubDomainType(context))
 
   app.$registry.register('pageSettings', new PagePageSettingsType(context))
 
diff --git a/web-frontend/modules/builder/services/domain.js b/web-frontend/modules/builder/services/domain.js
index 86431c783..4c351adbe 100644
--- a/web-frontend/modules/builder/services/domain.js
+++ b/web-frontend/modules/builder/services/domain.js
@@ -1,16 +1,10 @@
 export default (client) => {
   return {
-    async fetchAll(builderId) {
-      // TODO Manually add the domain type while the backend doesn't support it.
-      const result = await client.get(`builder/${builderId}/domains/`)
-      result.data = result.data.map((domain) => ({ type: 'custom', ...domain }))
-      return result
+    fetchAll(builderId) {
+      return client.get(`builder/${builderId}/domains/`)
     },
-    async create(builderId, { type, ...data }) {
-      // TODO For now we manage the type manually.
-      const result = await client.post(`builder/${builderId}/domains/`, data)
-      result.data.type = 'custom'
-      return result
+    create(builderId, data) {
+      return client.post(`builder/${builderId}/domains/`, data)
     },
     delete(domainId) {
       return client.delete(`builder/domains/${domainId}/`)
diff --git a/web-frontend/modules/builder/store/domain.js b/web-frontend/modules/builder/store/domain.js
index 3f725507a..0e8c44d54 100644
--- a/web-frontend/modules/builder/store/domain.js
+++ b/web-frontend/modules/builder/store/domain.js
@@ -54,10 +54,10 @@ const actions = {
 
     commit('SET_ITEMS', { domains })
   },
-  async create({ commit }, { builderId, ...data }) {
+  async create({ commit }, { builderId, type, ...data }) {
     const { data: domain } = await DomainService(this.$client).create(
       builderId,
-      data
+      { type, ...data }
     )
 
     commit('ADD_ITEM', { domain })
diff --git a/web-frontend/modules/core/assets/scss/components/builder/domains_settings.scss b/web-frontend/modules/core/assets/scss/components/builder/domains_settings.scss
index 4b3421701..05c7d9fc6 100644
--- a/web-frontend/modules/core/assets/scss/components/builder/domains_settings.scss
+++ b/web-frontend/modules/core/assets/scss/components/builder/domains_settings.scss
@@ -1,6 +1,10 @@
 .domain-settings__domain-type {
   display: inline-block;
-  width: 30%;
+  width: fit-content;
+
+  .dropdown__items {
+    width: min-content;
+  }
 }
 
 .domains-settings__loading {
diff --git a/web-frontend/modules/core/assets/scss/components/form_input.scss b/web-frontend/modules/core/assets/scss/components/form_input.scss
index 4600ab417..e88fca185 100644
--- a/web-frontend/modules/core/assets/scss/components/form_input.scss
+++ b/web-frontend/modules/core/assets/scss/components/form_input.scss
@@ -1,5 +1,5 @@
 .form-input {
-  display: block;
+  display: flex;
   border: 1px solid $color-neutral-400;
   position: relative;
   @include rounded($rounded);
@@ -96,3 +96,14 @@
     display: none;
   }
 }
+
+.form-input__suffix {
+  display: flex;
+  align-items: center;
+  width: fit-content;
+  border-left: 1px solid $color-neutral-400;
+  appearance: none;
+  padding: 0 12px;
+  outline: none;
+  line-height: 100%;
+}
diff --git a/web-frontend/modules/core/components/FormInput.vue b/web-frontend/modules/core/components/FormInput.vue
index bc3857430..018579076 100644
--- a/web-frontend/modules/core/components/FormInput.vue
+++ b/web-frontend/modules/core/components/FormInput.vue
@@ -43,6 +43,11 @@
           class="form-input__icon fas"
           :class="[`fa-${icon}`]"
         />
+        <div class="form-input__suffix disabled">
+          <div>
+            <slot name="suffix"></slot>
+          </div>
+        </div>
       </div>
       <div v-if="hasError" class="error">
         {{ error }}
diff --git a/web-frontend/modules/core/mixins/dropdown.js b/web-frontend/modules/core/mixins/dropdown.js
index ceae0b051..e0dc059d8 100644
--- a/web-frontend/modules/core/mixins/dropdown.js
+++ b/web-frontend/modules/core/mixins/dropdown.js
@@ -5,6 +5,7 @@ import {
 } from '@baserow/modules/core/utils/dom'
 
 import dropdownHelpers from './dropdownHelpers'
+import _ from 'lodash'
 
 export default {
   mixins: [dropdownHelpers],
@@ -325,7 +326,7 @@ export default {
     getSelectedProperty(value, property) {
       for (const i in this.getDropdownItemComponents()) {
         const item = this.getDropdownItemComponents()[i]
-        if (item.value === value) {
+        if (_.isEqual(item.value, value)) {
           return item[property]
         }
       }
@@ -338,7 +339,7 @@ export default {
     hasValue() {
       for (const i in this.getDropdownItemComponents()) {
         const item = this.getDropdownItemComponents()[i]
-        if (item.value === this.value) {
+        if (_.isEqual(item.value, this.value)) {
           return true
         }
       }
@@ -366,7 +367,9 @@ export default {
       )
 
       const isArrowUp = event.key === 'ArrowUp'
-      let index = children.findIndex((item) => item.value === this.hover)
+      let index = children.findIndex((item) =>
+        _.isEqual(item.value, this.hover)
+      )
       index = isArrowUp ? index - 1 : index + 1
 
       // Check if the new index is within the allowed range.
diff --git a/web-frontend/modules/core/module.js b/web-frontend/modules/core/module.js
index a924479b6..86851d2bc 100644
--- a/web-frontend/modules/core/module.js
+++ b/web-frontend/modules/core/module.js
@@ -91,6 +91,9 @@ export default function CoreModule(options) {
       process.env.BASEROW_UNIQUE_ROW_VALUES_SIZE_LIMIT ?? 100,
     BASEROW_ROW_PAGE_SIZE_LIMIT:
       parseInt(process.env.BASEROW_ROW_PAGE_SIZE_LIMIT) ?? 200,
+    BASEROW_BUILDER_DOMAINS: process.env.BASEROW_BUILDER_DOMAINS
+      ? process.env.BASEROW_BUILDER_DOMAINS.split(',')
+      : [],
   }
 
   const locales = [