diff --git a/aleksis/core/mixins.py b/aleksis/core/mixins.py
index e44caa020bdb6fb1814a3b7e10de95d72915b26b..c6608a6255d8e0680d6d608dce2b373e71fbcbb8 100644
--- a/aleksis/core/mixins.py
+++ b/aleksis/core/mixins.py
@@ -47,6 +47,28 @@ class _ExtensibleModelBase(models.base.ModelBase):
         return mcls
 
 
+def _generate_one_to_one_proxy_property(field, subfield):
+    def getter(self):
+        if hasattr(self, field.name):
+            related = getattr(self, field.name)
+            return getattr(related, subfield.name)
+        # Related instane does not exist
+        return None
+
+    def setter(self, val):
+        if hasattr(self, field.name):
+            related = getattr(self, field.name)
+        else:
+            # Auto-create related instance (but do not save)
+            related = field.related_model()
+            setattr(related, field.remote_field.name, self)
+            # Ensure the related model is saved later
+            self._save_reverse = getattr(self, "_save_reverse", []) + [related]
+        setattr(related, subfield.name, val)
+
+    return property(getter, setter)
+
+
 class ExtensibleModel(models.Model, metaclass=_ExtensibleModelBase):
     """Base model for all objects in AlekSIS apps.
 
@@ -248,13 +270,47 @@ class ExtensibleModel(models.Model, metaclass=_ExtensibleModelBase):
             to.property_(_virtual_related, related_name)
 
     @classmethod
-    def syncable_fields(cls) -> List[models.Field]:
-        """Collect all fields that can be synced on a model."""
-        return [
-            field
-            for field in cls._meta.fields
-            if (field.editable and not field.auto_created and not field.is_relation)
-        ]
+    def syncable_fields(
+        cls, recursive: bool = True, exclude_remotes: List = []
+    ) -> List[models.Field]:
+        """Collect all fields that can be synced on a model.
+
+        If recursive is True, it recurses into related models and generates virtual
+        proxy fields to access fields in related models."""
+        fields = []
+        for field in cls._meta.get_fields():
+            if field.is_relation and field.one_to_one and recursive:
+                if ExtensibleModel not in field.related_model.__mro__:
+                    # Related model is not extensible and thus has no syncable fields
+                    continue
+                if field.related_model in exclude_remotes:
+                    # Remote is excluded, probably to avoid recursion
+                    continue
+
+                # Recurse into related model to get its fields as well
+                for subfield in field.related_model.syncable_fields(
+                    recursive, exclude_remotes + [cls]
+                ):
+                    # generate virtual field names for proxy access
+                    name = f"_{field.name}__{subfield.name}"
+                    verbose_name = f"{field.name} ({field.related_model._meta.verbose_name}) → {subfield.verbose_name}"
+
+                    if not hasattr(cls, name):
+                        # Add proxy properties to handle access to related model
+                        setattr(cls, name, _generate_one_to_one_proxy_property(field, subfield))
+
+                    # Generate a fake field class with enough API to detect attribute names
+                    fields.append(
+                        type(
+                            "FakeRelatedProxyField",
+                            (),
+                            {"name": name, "verbose_name": verbose_name},
+                        )
+                    )
+            elif field.editable and not field.auto_created:
+                fields.append(field)
+
+        return fields
 
     @classmethod
     def syncable_fields_choices(cls) -> Tuple[Tuple[str, str]]:
@@ -273,6 +329,16 @@ class ExtensibleModel(models.Model, metaclass=_ExtensibleModelBase):
         """Dynamically add a new permission to a model."""
         cls.extra_permissions.append((name, verbose_name))
 
+    def save(self, *args, **kwargs):
+        """Ensure all functionality of our extensions that needs saving gets it."""
+        # For auto-created remote syncable fields
+        if hasattr(self, "_save_reverse"):
+            for related in self._save_reverse:
+                related.save()
+            del self._save_reverse
+
+        super().save(*args, **kwargs)
+
     class Meta:
         abstract = True