diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index bdb8cf803c2f5129b43387b48b3dd2eb8f234cf4..760fe0af70242cb26200bef74122e564eb1ce187 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -153,7 +153,7 @@ review_smoketest:
 review_destroy:
   stage: destroy
   image:
-    name: 'europe-north1-docker.pkg.dev/holi-shared/docker-hub-remote/hashicorp/terraform:1.11.1'
+    name: 'europe-north1-docker.pkg.dev/holi-shared/docker-hub-remote/hashicorp/terraform:1.11.2'
     # default entrypoint is terraform command, but we want to run shell scripts
     entrypoint: ['/bin/sh', '-c']
   variables:
diff --git a/.terraform-version b/.terraform-version
index 720c7384c6195b916c795feaddb08bbe023d183c..ca7176690dd6f501842f3ef4b70bb32118edb489 100644
--- a/.terraform-version
+++ b/.terraform-version
@@ -1 +1 @@
-1.11.1
+1.11.2
diff --git a/feed_posts/admin.py b/feed_posts/admin.py
index b7c79759eb78706791521c01fa1318f2d6050956..27c266e4d52616708be70358e2b32499962e92ee 100644
--- a/feed_posts/admin.py
+++ b/feed_posts/admin.py
@@ -16,6 +16,7 @@ class FeedPostAdminModel(admin.ModelAdmin):
         "image",
         "visibility",
         "display_topics",
+        "display_topics_v2",
         "language_code",
         "creator",
         "display_category",
@@ -31,6 +32,9 @@ class FeedPostAdminModel(admin.ModelAdmin):
     def display_topics(self, obj):
         return ", ".join([topic.title for topic in obj.topics.all()])
 
+    def display_topics_v2(self, obj):
+        return ", ".join(obj.topics_v2)
+
     def display_category(self, obj):
         return obj.category.title if obj.category else ""
 
@@ -38,6 +42,7 @@ class FeedPostAdminModel(admin.ModelAdmin):
         return obj.creator.role == UserRole.CO_FOUNDER
 
     display_topics.short_description = "Topics"
+    display_topics_v2.short_description = "Topics V2"
     display_category.short_description = "Category"
     display_is_co_founder.short_description = "Co-founder's Post"
 
diff --git a/feed_posts/migrations/0018_feedpost_topics_v2.py b/feed_posts/migrations/0018_feedpost_topics_v2.py
new file mode 100644
index 0000000000000000000000000000000000000000..ae424501e0f940d96a70fb93072a7f6a627e3093
--- /dev/null
+++ b/feed_posts/migrations/0018_feedpost_topics_v2.py
@@ -0,0 +1,34 @@
+# Generated by Django 5.0.12 on 2025-02-28 10:00
+
+import django.contrib.postgres.fields
+from django.db import migrations, models
+
+from openbook_terms.helpers import topics_to_topic_v2_slugs
+
+
+def migrate_feed_post_topics(apps, schema_editor):
+    FeedPost = apps.get_model("feed_posts", "FeedPost")
+
+    for post in FeedPost.objects.all():
+        old_topics = post.topics.all()
+        new_topics = topics_to_topic_v2_slugs(old_topics)
+
+        post.topics_v2 = new_topics
+        post.save()
+
+
+class Migration(migrations.Migration):
+    dependencies = [
+        ("feed_posts", "0017_feedconfig"),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name="feedpost",
+            name="topics_v2",
+            field=django.contrib.postgres.fields.ArrayField(
+                base_field=models.CharField(max_length=128), blank=True, default=list, size=None
+            ),
+        ),
+        migrations.RunPython(migrate_feed_post_topics),
+    ]
diff --git a/feed_posts/migrations/0019_feedpost_feed_posts__topics__9ffb13_gin.py b/feed_posts/migrations/0019_feedpost_feed_posts__topics__9ffb13_gin.py
new file mode 100644
index 0000000000000000000000000000000000000000..be6eda3d131e514b19e476ee7a37a9884640b7af
--- /dev/null
+++ b/feed_posts/migrations/0019_feedpost_feed_posts__topics__9ffb13_gin.py
@@ -0,0 +1,24 @@
+# Generated by Django 5.0.12 on 2025-02-28 15:49
+
+import django.contrib.postgres.indexes
+from django.conf import settings
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ("feed_posts", "0018_feedpost_topics_v2"),
+        ("openbook_common", "0011_delete_featuretoggle"),
+        ("openbook_terms", "0010_delete_inspirationpostcategory"),
+        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+    ]
+
+    operations = [
+        migrations.AddIndex(
+            model_name="feedpost",
+            index=django.contrib.postgres.indexes.GinIndex(
+                fields=["topics_v2"], name="feed_posts__topics__9ffb13_gin"
+            ),
+        ),
+    ]
diff --git a/feed_posts/migrations/tests/test_0018_feedpost_topics_v2.py b/feed_posts/migrations/tests/test_0018_feedpost_topics_v2.py
new file mode 100644
index 0000000000000000000000000000000000000000..e661fee6ea71aa454fa6e91081144af1ef7f4603
--- /dev/null
+++ b/feed_posts/migrations/tests/test_0018_feedpost_topics_v2.py
@@ -0,0 +1,25 @@
+from importlib import import_module
+import pytest
+from django.apps import apps
+from django.db import connection
+from feed_posts.models import FeedPost
+from openbook_common.tests.helpers import make_feed_post, make_topic
+
+# Required because of file name starting with a number
+migration = import_module("feed_posts.migrations.0018_feedpost_topics_v2")
+
+
+@pytest.mark.django_db
+class TestMigrateFeedPostTopic:
+    def test_migrate_feedpost_topics(self):
+        # GIVEN a feed post with only V1 topics
+        topics_v1 = [make_topic(slug="eco-tourism"), make_topic(slug="elderly-people")]
+        feed_post = make_feed_post()
+        feed_post.topics.set(topics_v1)
+
+        # WHEN migrating feed post topics from V1 to V2
+        migration.migrate_feed_post_topics(apps, connection.schema_editor())
+
+        # THEN the feed post has mapped V2 topics
+        updated_feed_post = FeedPost.objects.get(id=feed_post.id)
+        assert sorted(updated_feed_post.topics_v2) == sorted(["lifestyle-consumption", "elderly-care"])
diff --git a/feed_posts/models.py b/feed_posts/models.py
index 8b5f6f3c6dd991264f770cabf0d0c89823a3223c..8621461375e0bc0046684fc4e166773dd8c35505 100644
--- a/feed_posts/models.py
+++ b/feed_posts/models.py
@@ -3,6 +3,8 @@ from datetime import timedelta
 from typing import Optional
 
 from django.contrib.gis.db import models
+from django.contrib.postgres.fields import ArrayField
+from django.contrib.postgres.indexes import GinIndex
 from django.core.files.storage import default_storage
 from django.utils import timezone
 from django.utils.text import slugify
@@ -36,6 +38,8 @@ class FeedPost(CommentNotificationMixin, ModelWithUUID):
 
     topics = models.ManyToManyField(Topic, related_name="feed_posts", blank=True, db_index=True)
 
+    topics_v2 = ArrayField(models.CharField(max_length=settings.TERM_SLUG_MAX_LENGTH), blank=True, default=list)
+
     category = models.ForeignKey(FeedPostCategory, on_delete=models.PROTECT, null=True)
 
     creator = models.ForeignKey(
@@ -86,6 +90,9 @@ class FeedPost(CommentNotificationMixin, ModelWithUUID):
         help_text="Specifies how many days the post will remain at the top of the feed.",
     )
 
+    class Meta:
+        indexes = [GinIndex(fields=["topics_v2"])]
+
     @property
     def has_priority(self):
         if (
diff --git a/feed_posts/repositories.py b/feed_posts/repositories.py
index 367bad2259f68935ea360115b9069f82f820353e..2e36301316ceec2329a12316789e8b0bc72e6403 100644
--- a/feed_posts/repositories.py
+++ b/feed_posts/repositories.py
@@ -8,6 +8,8 @@ from openbook_auth.models import User, get_reactions, UserRole
 from openbook_common.enums import ReactionType
 from openbook_common.schema.types import GeolocationPointInput
 from openbook_common.utils.helpers import is_uuid
+from openbook_terms.helpers import topics_to_topic_v2_slugs
+from openbook_terms.models import Topic
 
 
 class FeedPostRepository:
@@ -16,7 +18,8 @@ class FeedPostRepository:
         title,
         visibility,
         description,
-        topics,
+        topic_ids,
+        topics_v2,
         category,
         creator,
         image,
@@ -38,6 +41,9 @@ class FeedPostRepository:
         elif images is not None and len(images) > 0:
             post_image = images[0]["image"]
 
+        if topics_v2 is None:
+            topics_v2 = []
+
         feed_post = FeedPost.objects.create(
             title=title,
             visibility=visibility,
@@ -50,10 +56,14 @@ class FeedPostRepository:
             geolocation=GeolocationPointInput.to_point(geolocation),
             priority_duration=priority_duration,
             image=post_image,
+            topics_v2=topics_v2,
         )
 
-        if topics is not None:
+        if topic_ids is not None:
+            topics = Topic.objects.filter(id__in=topic_ids)
             feed_post.topics.set(topics)
+            feed_post.topics_v2 = topics_to_topic_v2_slugs(topics)
+            feed_post.save()
 
         # This code is required for backward compatibility. Remove it after version 1.30.x
         if image is not None:
@@ -80,7 +90,8 @@ class FeedPostRepository:
         description=None,
         visibility=None,
         category=None,
-        topics=None,
+        topic_ids=None,
+        topics_v2=None,
         image=None,
         remove_image=False,
         link_preview=None,
@@ -104,8 +115,14 @@ class FeedPostRepository:
         if category is not None:
             post.category = category
 
-        if topics is not None:
+        if topics_v2 is not None:
+            post.topics_v2 = topics_v2
+
+        if topic_ids is not None:
+            topics = Topic.objects.filter(id__in=topic_ids)
             post.topics.set(topics)
+            post.topics_v2 = topics_to_topic_v2_slugs(topics)
+            post.save()
 
         # This code is required for backward compatibility. Remove it after version 1.30.x
         if remove_image:
diff --git a/feed_posts/schema/feed_queries.py b/feed_posts/schema/feed_queries.py
index cb56b78d47bc089b2a101383fd5f329618f5759a..a6ccfc3a405b86a79a3abdaeddb0af2f63435a79 100644
--- a/feed_posts/schema/feed_queries.py
+++ b/feed_posts/schema/feed_queries.py
@@ -138,7 +138,7 @@ class Query:
             .order_by("-created_at", "-reaction_count", "-total_comments")
         )
 
-        user_interests = user.profile.interests.values_list("id", flat=True)
+        user_interests_v2 = user.profile.interests_v2
         user_connections = user.connections.values_list("target_user_id", flat=True)
         current_time = timezone.now()
 
@@ -169,10 +169,10 @@ class Query:
                 created__gte=current_time - priority_duration,
             ),
             # Feed posts matching user's topics
-            feed_posts.filter(topics__id__in=user_interests, created_at__gte=current_time - priority_duration),
+            feed_posts.filter(topics_v2__overlap=user_interests_v2, created_at__gte=current_time - priority_duration),
             # Space posts matching user's topics
             space_posts.filter(
-                community__topics__id__in=user_interests, created__gte=current_time - priority_duration
+                community__topics_v2__overlap=user_interests_v2, created__gte=current_time - priority_duration
             ),
             # Editors posts
             feed_posts.filter(creator__role=UserRole.EDITOR, created_at__gte=current_time - priority_duration),
diff --git a/feed_posts/schema/mutations.py b/feed_posts/schema/mutations.py
index 6721a02daf11fde9400fff0d2aa592974b29fa7d..81bfef3d422d1d75b482793b805c048c858e3e45 100644
--- a/feed_posts/schema/mutations.py
+++ b/feed_posts/schema/mutations.py
@@ -32,6 +32,7 @@ class Mutation:
             description=data.get("description"),
             category_id=data.get("category_id"),
             topic_ids=data.get("topic_ids"),
+            topics_v2=data.get("topics_v2"),
             image=data.get("image"),
             creator=user,
             language=data.get("language"),
@@ -58,6 +59,7 @@ class Mutation:
             description=data.get("description"),
             category_id=data.get("category_id"),
             topic_ids=data.get("topic_ids"),
+            topics_v2=data.get("topics_v2"),
             image=data.get("image"),
             remove_image=is_parameter_null(data, "image"),
             language=data.get("language"),
@@ -71,9 +73,7 @@ class Mutation:
     @sync_to_async
     def delete_feed_post(self, info, post_id: uuid.UUID) -> Result:
         user = verify_authorized_user(info)
-
         FeedPostService().delete_feed_post(user=user, post_id=post_id)
-
         return Result(success=True)
 
     @strawberry.mutation
diff --git a/feed_posts/schema/types.py b/feed_posts/schema/types.py
index 9c59eed79316435133454211378923acd92e4962..fa3caa8fec19135c7274e5d3566d1492786aa9e9 100644
--- a/feed_posts/schema/types.py
+++ b/feed_posts/schema/types.py
@@ -1,5 +1,5 @@
 import uuid
-from typing import Optional, List
+from typing import Optional, List, Annotated
 
 import strawberry
 import strawberry_django
@@ -40,14 +40,20 @@ class FeedPost:
     description: strawberry.auto
     location: Optional[str]
     link_preview: Optional[LinkPreview]
-    image: Optional[
-        str
-    ]  # This field is deprecated. Please use the new images field instead. Remove it after version 1.30.x
-    image_blurhash: Optional[
-        str
-    ]  # This field is deprecated. Please use the new images field instead. Remove it after version 1.30.x
+    image: Annotated[
+        Optional[str],
+        strawberry.argument(description="This field is deprecated. Please use the new images field instead."),
+    ]  # Can be removed after version 1.30.x
+    image_blurhash: Annotated[
+        Optional[str],
+        strawberry.argument(description="This field is deprecated. Please use the new images field instead."),
+    ]  # Can be removed after version 1.30.x
     visibility: VisibilityLevel
-    topics: Optional[List[Topic]]
+    topics: Annotated[
+        Optional[List[Topic]],
+        strawberry.argument(description="Deprecated since 1.52. Use topics_v2 instead."),
+    ]
+    topics_v2: Optional[List[str]]
     category: Optional[FeedPostCategory]
     creator: Optional[User]
 
@@ -119,7 +125,11 @@ class CreateFeedPostInput:
         strawberry.UNSET
     )  # This field is deprecated. Please use the new images field instead. Remove it after version 1.30.x
     visibility: VisibilityLevel
-    topic_ids: Optional[List[Optional[uuid.UUID]]] = strawberry.UNSET
+    topic_ids: Annotated[
+        Optional[List[Optional[uuid.UUID]]],
+        strawberry.argument(description="Deprecated since 1.52. Use topics_v2 instead."),
+    ] = strawberry.UNSET
+    topics_v2: Optional[List[str]] = strawberry.UNSET
     category_id: Optional[uuid.UUID] = strawberry.UNSET
     language: Optional[str] = strawberry.UNSET
     location: Optional[str] = strawberry.UNSET
@@ -144,7 +154,11 @@ class UpdateFeedPostInput:
         strawberry.UNSET
     )  # This field is deprecated. Please use the new images field instead. Remove it after version 1.30.x
     visibility: Optional[VisibilityLevel] = strawberry.UNSET
-    topic_ids: Optional[List[Optional[uuid.UUID]]] = strawberry.UNSET
+    topic_ids: Annotated[
+        Optional[List[Optional[uuid.UUID]]],
+        strawberry.argument(description="Deprecated since 1.52. Use topics_v2 instead."),
+    ] = strawberry.UNSET
+    topics_v2: Optional[List[str]] = strawberry.UNSET
     category_id: Optional[uuid.UUID] = strawberry.UNSET
     language: Optional[str] = strawberry.UNSET
     location: Optional[str] = strawberry.UNSET
diff --git a/feed_posts/serializers.py b/feed_posts/serializers.py
index db68f8b0fe6b69a16ef47b3029e8602edf759225..f05e3bba9faac73735e79d1b21dd8d9b0963c7aa 100644
--- a/feed_posts/serializers.py
+++ b/feed_posts/serializers.py
@@ -10,7 +10,7 @@ from openbook import settings
 from openbook_common.serializers import GeolocationPointSerializer
 from openbook_common.serializers_fields.request import RestrictedImageFileSizeField
 from openbook_common.types import LanguageCode
-from openbook_common.validators import sanitize_text
+from openbook_common.validators import sanitize_text, topic_v2_exists
 
 
 class FeedPostImageSerializer(serializers.Serializer):
@@ -42,43 +42,39 @@ class CreateFeedPostSerializer(serializers.Serializer):
         required=False,
         allow_blank=True,
     )
-
     description = serializers.CharField(
         min_length=settings.FEED_POST_DESCRIPTION_MIN_LENGTH,
         max_length=settings.FEED_POST_DESCRIPTION_MAX_LENGTH,
         required=True,
         validators=[sanitize_text],
     )
-
     visibility = serializers.ChoiceField(required=True, choices=VisibilityLevel.choices)
-
     topic_ids = serializers.ListField(
         max_length=settings.FEED_POST_TOPICS_MAX_AMOUNT,
         required=False,
         child=serializers.UUIDField(validators=[validate_topic_exists_by_id]),
     )
-
+    topics_v2 = serializers.ListField(
+        max_length=settings.FEED_POST_TOPICS_MAX_AMOUNT,
+        required=False,
+        child=serializers.CharField(validators=[topic_v2_exists]),
+    )
     category_id = serializers.UUIDField(
         required=False, allow_null=True, validators=[validate_feed_post_category_exists_by_id]
     )
-
     image = RestrictedImageFileSizeField(
         allow_empty_file=False,
         required=False,
         max_upload_size=settings.FEED_POST_IMAGE_MAX_SIZE,
     )
-
     images = FeedPostImageSerializer(many=True, required=False, allow_null=True)
-
     language = serializers.CharField(max_length=2, required=False, allow_blank=True)
-
     location = serializers.CharField(
         max_length=settings.APPOINTMENT_LOCATION_MAX_LENGTH,
         required=False,
         allow_null=True,
         allow_blank=True,
     )
-
     geolocation = GeolocationPointSerializer(required=False)
 
 
@@ -92,44 +88,39 @@ class UpdateFeedPostSerializer(serializers.Serializer):
         max_length=settings.FEED_POST_TITLE_MAX_LENGTH,
         required=False,
     )
-
     description = serializers.CharField(
         min_length=settings.FEED_POST_DESCRIPTION_MIN_LENGTH,
         max_length=settings.FEED_POST_DESCRIPTION_MAX_LENGTH,
         required=False,
         validators=[sanitize_text],
     )
-
     visibility = serializers.ChoiceField(required=False, choices=VisibilityLevel.choices)
-
     topic_ids = serializers.ListField(
         max_length=settings.FEED_POST_TOPICS_MAX_AMOUNT,
         required=False,
         child=serializers.UUIDField(validators=[validate_topic_exists_by_id]),
     )
-
+    topics_v2 = serializers.ListField(
+        max_length=settings.FEED_POST_TOPICS_MAX_AMOUNT,
+        required=False,
+        child=serializers.CharField(validators=[topic_v2_exists]),
+    )
     category_id = serializers.UUIDField(
         required=False, allow_null=True, validators=[validate_feed_post_category_exists_by_id]
     )
-
     image = RestrictedImageFileSizeField(
         allow_empty_file=False,
         required=False,
         max_upload_size=settings.FEED_POST_IMAGE_MAX_SIZE,
         allow_null=True,
     )
-
     language = serializers.CharField(max_length=2, required=False, allow_blank=True, default=LanguageCode.ENGLISH)
-
     location = serializers.CharField(
         max_length=settings.APPOINTMENT_LOCATION_MAX_LENGTH,
         required=False,
         allow_null=True,
         allow_blank=True,
     )
-
     geolocation = GeolocationPointSerializer(required=False)
-
     existing_images = FeedPostImageMetadataSerializer(many=True, required=False, allow_null=True)
-
     new_images = FeedPostImageSerializer(many=True, required=False, allow_null=True)
diff --git a/feed_posts/services.py b/feed_posts/services.py
index 43d7732082f1e231840e7fa88d612e509a8edf00..f107c75b3e7b363bea67c342ad2e496a836b7e41 100644
--- a/feed_posts/services.py
+++ b/feed_posts/services.py
@@ -9,7 +9,7 @@ from openbook_common.models import LinkPreview
 from openbook_common.tracking import TrackingEvent, track
 from openbook_common.utils.helpers import extract_usernames_from_string
 from openbook_notifications.services import NotificationsService
-from openbook_terms.models import FeedPostCategory, Topic
+from openbook_terms.models import FeedPostCategory
 
 
 class FeedPostService:
@@ -27,6 +27,7 @@ class FeedPostService:
         image,
         category_id=None,
         topic_ids=None,
+        topics_v2=None,
         language=None,
         location=None,
         geolocation=None,
@@ -45,7 +46,8 @@ class FeedPostService:
             category=category,
             creator=creator,
             image=image,
-            topics=topic_ids,
+            topic_ids=topic_ids,
+            topics_v2=topics_v2,
             link_preview=link_preview,
             language=language,
             location=location,
@@ -64,7 +66,7 @@ class FeedPostService:
                     "locations": 0 if geolocation is None else 1,
                     "categories": 0 if category is None else 1,
                     "images": 0 if images is None else len(images),
-                    "topics": 0 if topic_ids is None else len(topic_ids),
+                    "topics": len(topic_ids or topics_v2 or []),
                 },
             ),
         )
@@ -91,6 +93,7 @@ class FeedPostService:
         visibility=None,
         category_id=None,
         topic_ids=None,
+        topics_v2=None,
         image=None,
         remove_image=False,
         language=None,
@@ -105,8 +108,6 @@ class FeedPostService:
 
         category = FeedPostCategory.objects.get(id=category_id) if category_id else None
 
-        topics = Topic.objects.filter(id__in=topic_ids) if topic_ids else None
-
         urls = extract_urls_from_string(description)
 
         link_preview = LinkPreview.from_url(urls[0]) if len(urls) > 0 else None
@@ -119,7 +120,8 @@ class FeedPostService:
             description=description,
             visibility=visibility,
             category=category,
-            topics=topics,
+            topic_ids=topic_ids,
+            topics_v2=topics_v2,
             image=image,
             remove_image=remove_image,
             link_preview=link_preview,
diff --git a/feed_posts/tests/helpers.py b/feed_posts/tests/helpers.py
index cb99c26d5c34fc504cf3bb3bedcaf63604dfd4a7..4bb76d267fdd5d0648301d91e3e96df4e27ebf4a 100644
--- a/feed_posts/tests/helpers.py
+++ b/feed_posts/tests/helpers.py
@@ -23,6 +23,7 @@ def create_feed_post(user, input):
                 topics {
                     id
                 }
+                topicsV2
                 creator {
                     id
                 }
@@ -68,6 +69,7 @@ def update_feed_post(user, input):
                 topics {
                     id
                 }
+                topicsV2
                 creator {
                     id
                 }
@@ -175,6 +177,7 @@ def my_posts(user):
                     topics {
                         id
                     }
+                    topicsV2
                     creator {
                         id
                     }
@@ -211,6 +214,7 @@ def posts_by_user_id(user, user_id):
                     topics {
                         id
                     }
+                    topicsV2
                     creator {
                         id
                     }
@@ -248,6 +252,7 @@ def feed_posts(user):
                     topics {
                         id
                     }
+                    topicsV2
                     creator {
                         id
                     }
@@ -282,6 +287,7 @@ def post(user, id_or_slug):
                     topics {
                         id
                     }
+                    topicsV2
                     creator {
                         id
                     }
diff --git a/feed_posts/tests/test_graphql.py b/feed_posts/tests/test_graphql.py
index 32d62f8b12418e242fcf5eba17085a6689a07dea..42a3d28b4f2cd73069d3fa684900311dcd211753 100644
--- a/feed_posts/tests/test_graphql.py
+++ b/feed_posts/tests/test_graphql.py
@@ -27,11 +27,11 @@ from openbook_auth.models import UserRole
 from openbook_common.enums import ReactionType, ReactionAction, VisibilityType
 from openbook_common.tests.helpers import (
     make_user,
-    make_topic,
     make_feed_post_category,
     simple_jpeg_bytes,
     make_community,
     make_feed_post_founder_update_category,
+    make_topics,
 )
 from openbook_notifications.notifications import PostReactionNotification
 
@@ -54,13 +54,15 @@ class TestFeedPosts(TestCase):
         cls.yet_another_user = make_user()
         cls.space = make_community(creator=cls.user)
         cls.space.add_member(cls.yet_another_user)
+        topics = make_topics(6)
+        cls.topics_v2 = [t.slug for t in topics]
 
         cls.public_feed_post_input_data = {
             "title": fake.text(max_nb_chars=settings.FEED_POST_TITLE_MAX_LENGTH),
             "description": fake.text(max_nb_chars=settings.FEED_POST_DESCRIPTION_MAX_LENGTH),
             "visibility": VisibilityLevel.PUBLIC,
             "categoryId": str(make_feed_post_category().id),
-            "topicIds": [str(make_topic().id) for _ in range(6)],
+            "topicIds": [str(t.id) for t in topics],
             "image": SimpleUploadedFile(name="image_1.jpg", content=simple_jpeg_bytes),
             "location": "Munich, Germany",
             "geolocation": {
@@ -74,7 +76,7 @@ class TestFeedPosts(TestCase):
             "description": fake.text(max_nb_chars=settings.FEED_POST_DESCRIPTION_MAX_LENGTH),
             "visibility": VisibilityLevel.REGISTERED_USERS_ONLY,
             "categoryId": str(make_feed_post_category().id),
-            "topicIds": [str(make_topic().id) for _ in range(3)],
+            "topicsV2": cls.topics_v2[:3],
             "image": SimpleUploadedFile(name="image_2.jpg", content=simple_jpeg_bytes),
             "location": "Munich, Germany",
             "geolocation": {
@@ -88,7 +90,7 @@ class TestFeedPosts(TestCase):
             "description": fake.text(max_nb_chars=settings.FEED_POST_DESCRIPTION_MAX_LENGTH),
             "visibility": VisibilityLevel.PUBLIC,
             "categoryId": str(make_feed_post_category().id),
-            "topicIds": [str(make_topic().id) for _ in range(6)],
+            "topicIds": [str(t.id) for t in topics],
             "image": SimpleUploadedFile(name="image_3.jpg", content=simple_jpeg_bytes),
             "location": "Munich, Germany",
             "geolocation": {
@@ -102,7 +104,7 @@ class TestFeedPosts(TestCase):
             "description": fake.text(max_nb_chars=settings.FEED_POST_DESCRIPTION_MAX_LENGTH),
             "visibility": VisibilityLevel.REGISTERED_USERS_ONLY,
             "categoryId": str(make_feed_post_category().id),
-            "topicIds": [str(make_topic().id) for _ in range(3)],
+            "topicIds": [str(t.id) for t in topics[:3]],
             "image": SimpleUploadedFile(name="image_4.jpg", content=simple_jpeg_bytes),
             "location": "Munich, Germany",
             "geolocation": {
@@ -116,7 +118,7 @@ class TestFeedPosts(TestCase):
             "description": f"@{cls.another_user.username} @{cls.yet_another_user.username} {fake.text(max_nb_chars=100)}",
             "visibility": VisibilityLevel.PUBLIC,
             "categoryId": str(make_feed_post_category().id),
-            "topicIds": [str(make_topic().id) for _ in range(6)],
+            "topicIds": [str(t.id) for t in topics],
             "image": SimpleUploadedFile(name="image_1.jpg", content=simple_jpeg_bytes),
             "location": "Munich, Germany",
             "geolocation": {
@@ -130,7 +132,7 @@ class TestFeedPosts(TestCase):
             "description": f"@{cls.another_user.username} {fake.text(max_nb_chars=100)}",
             "visibility": VisibilityLevel.PUBLIC,
             "categoryId": str(make_feed_post_category().id),
-            "topicIds": [str(make_topic().id) for _ in range(6)],
+            "topicIds": [str(t.id) for t in topics],
             "image": SimpleUploadedFile(name="image_1.jpg", content=simple_jpeg_bytes),
             "location": "Munich, Germany",
             "geolocation": {
@@ -144,7 +146,7 @@ class TestFeedPosts(TestCase):
             "description": fake.text(max_nb_chars=settings.FEED_POST_DESCRIPTION_MAX_LENGTH),
             "visibility": VisibilityLevel.REGISTERED_USERS_ONLY,
             "categoryId": str(make_feed_post_category().id),
-            "topicIds": [str(make_topic().id) for _ in range(3)],
+            "topicIds": [str(t.id) for t in topics[:3]],
             "newImages": [
                 {
                     "image": SimpleUploadedFile(name="image_2.jpg", content=simple_jpeg_bytes),
@@ -165,7 +167,7 @@ class TestFeedPosts(TestCase):
             "description": fake.text(max_nb_chars=settings.FEED_POST_DESCRIPTION_MAX_LENGTH),
             "visibility": VisibilityLevel.PUBLIC,
             "categoryId": str(make_feed_post_category().id),
-            "topicIds": [str(make_topic().id) for _ in range(6)],
+            "topicIds": [str(t.id) for t in topics],
             "image": SimpleUploadedFile(name="image_3.jpg", content=simple_jpeg_bytes),
             "images": [
                 {
@@ -220,6 +222,7 @@ class TestFeedPosts(TestCase):
         assert data["location"] == self.public_feed_post_input_data["location"]
         assert data["geolocation"]["latitude"] == self.public_feed_post_input_data["geolocation"]["latitude"]
         assert data["geolocation"]["longitude"] == self.public_feed_post_input_data["geolocation"]["longitude"]
+        assert sorted(data["topicsV2"]) == sorted(self.topics_v2)
 
     def test_can_query_my_posts_if_anonymous_user(self):
         first_mutation_response = create_feed_post(user=self.user, input=self.public_feed_post_input_data)
@@ -332,6 +335,7 @@ class TestFeedPosts(TestCase):
         assert data["visibility"] == self.public_feed_post_input_data["visibility"]
         assert data["category"]["id"] == self.public_feed_post_input_data["categoryId"]
         assert len(data["topics"]) == len(self.public_feed_post_input_data["topicIds"])
+        assert sorted(data["topicsV2"]) == sorted(self.topics_v2)
         assert data["creator"]["id"] == str(self.user.pk)
         assert data["image"]
 
@@ -353,7 +357,7 @@ class TestFeedPosts(TestCase):
         assert data["description"] == self.registered_users_only_feed_post_input_data["description"]
         assert data["visibility"] == self.registered_users_only_feed_post_input_data["visibility"]
         assert data["category"]["id"] == self.registered_users_only_feed_post_input_data["categoryId"]
-        assert len(data["topics"]) == len(self.registered_users_only_feed_post_input_data["topicIds"])
+        assert sorted(data["topicsV2"]) == sorted(self.registered_users_only_feed_post_input_data["topicsV2"])
         assert data["creator"]["id"] == str(self.user.pk)
         assert data["image"]
         assert data["location"] == self.registered_users_only_feed_post_input_data["location"]
@@ -381,6 +385,7 @@ class TestFeedPosts(TestCase):
         assert data["visibility"] == self.public_feed_post_input_data["visibility"]
         assert data["category"]["id"] == self.public_feed_post_input_data["categoryId"]
         assert len(data["topics"]) == len(self.public_feed_post_input_data["topicIds"])
+        assert sorted(data["topicsV2"]) == sorted(self.topics_v2)
         assert data["creator"]["id"] == str(self.user.pk)
         assert data["image"]
         assert data["location"] == self.public_feed_post_input_data["location"]
@@ -446,6 +451,7 @@ class TestFeedPosts(TestCase):
         assert data["visibility"] == self.registered_users_only_feed_post_update_input_data["visibility"]
         assert data["category"]["id"] == self.registered_users_only_feed_post_update_input_data["categoryId"]
         assert len(data["topics"]) == len(self.registered_users_only_feed_post_update_input_data["topicIds"])
+        assert sorted(data["topicsV2"]) == sorted(self.topics_v2[:3])
         assert data["creator"]["id"] == str(self.user.pk)
         assert data["image"]
         assert len(data["images"]) == 1
@@ -835,14 +841,15 @@ class TestFeedPosts(TestCase):
             title=input_data["title"],
             visibility=input_data["visibility"],
             description=input_data["description"],
-            topics=input_data["topicIds"],
+            topic_ids=input_data["topicIds"],
+            topics_v2=None,
             category=founder_update_category,
             creator=self.co_founder,
             image=input_data["image"],
-            location=input_data["location"],
-            geolocation=input_data["geolocation"],
             link_preview=None,
             language=None,
+            location=input_data["location"],
+            geolocation=input_data["geolocation"],
             images=None,
         )
 
@@ -850,14 +857,15 @@ class TestFeedPosts(TestCase):
             title=input_data["title"],
             visibility=input_data["visibility"],
             description=input_data["description"],
-            topics=input_data["topicIds"],
+            topic_ids=input_data["topicIds"],
+            topics_v2=None,
             category=regular_category,
             creator=self.user,
             image=input_data["image"],
-            location=input_data["location"],
-            geolocation=input_data["geolocation"],
             link_preview=None,
             language=None,
+            location=input_data["location"],
+            geolocation=input_data["geolocation"],
             images=None,
         )
 
diff --git a/openbook_auth/fixtures/users.json b/openbook_auth/fixtures/users.json
index b0186e4f8dd61b3a62df010b86aa9f1de4aaaabf..e0acc2ed0a34db9ccce9ac190db4aca3617f62f3 100644
--- a/openbook_auth/fixtures/users.json
+++ b/openbook_auth/fixtures/users.json
@@ -1,143 +1,168 @@
 [
-  {
-    "model": "openbook_auth.User",
-    "pk": "1",
-    "fields": {
-      "username": "9ebfc2c2-2d2b-413d-8bfd-ec30af1b7d49",
-      "password": "F0OB4R",
-      "is_superuser": false,
-      "is_staff": false,
-      "is_active": true,
-      "date_joined": "2022-08-25 12:08:16+00",
-      "email": "oswald_rippin@example.com",
-      "is_email_verified": false,
-      "uuid": "9ebfc2c2-2d2b-413d-8bfd-ec30af1b7d49",
-      "invite_count": 0,
-      "are_guidelines_accepted": true,
-      "is_deleted": false,
-      "visibility": "O"
-    }
-  },
-  {
-    "model": "openbook_auth.UserProfile",
-    "pk": "9829b4fb-9c99-44b5-8568-476be3ae01f9",
-    "fields": {
-      "user_id": 1,
-      "followers_count_visible": false,
-      "is_of_legal_age": true,
-      "community_posts_visible": true,
-      "interests": ["2ebecfaa-3a6d-4f39-9389-28bbbca628c9", "e61f3914-930e-4178-a954-73c7a129bd1f", "6244ff16-823b-4c51-b695-58b1aadab32c"],
-      "sdgs": ["4c832a1f-9b1d-4bb7-8186-57a8eaacf1ef"],
-      "skills": ["fcc4a764-d646-4fc6-ba8a-41dc4e224022"],
-      "name": "Oswald",
-      "last_name": "Rippin",
-      "avatar": "https://cloudflare-ipfs.com/ipfs/Qmd3W5DuhgHirLHGVixi6V76LhCkZUz6pnFt5AJBiyvHye/avatar/871.jpg",
-      "engagement_level": "INTERESTED"
-    }
-  },
-  {
-    "model": "openbook_auth.User",
-    "pk": "2",
-    "fields": {
-      "username": "5846dd02-e2c9-4350-9e68-e4c79bf02e02",
-      "password": "F0OB4R",
-      "is_superuser": false,
-      "is_staff": false,
-      "is_active": true,
-      "date_joined": "2022-08-24 12:08:16+00",
-      "email": "lamar_maggio@example.com",
-      "is_email_verified": false,
-      "uuid": "5846dd02-e2c9-4350-9e68-e4c79bf02e02",
-      "invite_count": 0,
-      "are_guidelines_accepted": true,
-      "is_deleted": false,
-      "visibility": "O"
-    }
-  },
-  {
-    "model": "openbook_auth.UserProfile",
-    "pk": "4ee7305e-ef9b-42ec-9b85-a4602f01a72e",
-    "fields": {
-      "user_id": 2,
-      "followers_count_visible": false,
-      "is_of_legal_age": true,
-      "community_posts_visible": true,
-      "interests": [],
-      "sdgs": [],
-      "skills": []
-    }
-  },
-  {
-    "model": "openbook_auth.User",
-    "pk": "3",
-    "fields": {
-      "username": "a09124b7-cdaf-4922-9c02-10b40b3b1530",
-      "password": "F0OB4R",
-      "is_superuser": false,
-      "is_staff": false,
-      "is_active": true,
-      "date_joined": "2022-08-23 12:08:16+00",
-      "email": "nelda_auer@example.com",
-      "is_email_verified": false,
-      "uuid": "a09124b7-cdaf-4922-9c02-10b40b3b1530",
-      "invite_count": 0,
-      "are_guidelines_accepted": true,
-      "is_deleted": false,
-      "visibility": "O"
-    }
-  },
-  {
-    "model": "openbook_auth.UserProfile",
-    "pk": "46fd9173-5895-4c3e-997d-568f0838ab24",
-    "fields": {
-      "user_id": 3,
-      "followers_count_visible": false,
-      "is_of_legal_age": true,
-      "community_posts_visible": true,
-      "interests": ["9cafd8e2-85ab-4e54-9cca-f4327015648a"],
-      "sdgs": ["9fbb8ea2-c82d-44cc-867e-2f94032f4d37"],
-      "skills": ["f8399855-4123-4ed6-9595-e0fb65bc2ade", "8c0e9623-0b29-4203-a4aa-ebfbe5ef39e5"],
-      "name": "Nelda",
-      "last_name": "Auer",
-      "engagement_level": "INTERESTED",
-      "about_me": "Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua."
-    }
-  },
-  {
-    "model": "openbook_auth.User",
-    "pk": "4",
-    "fields": {
-      "username": "d2384748-0151-40d6-8527-0fe182112a6c",
-      "password": "F0OB4R",
-      "is_superuser": false,
-      "is_staff": false,
-      "is_active": true,
-      "date_joined": "2022-08-22 12:08:16+00",
-      "email": "kaitlyn_veum@example.com",
-      "is_email_verified": false,
-      "uuid": "d2384748-0151-40d6-8527-0fe182112a6c",
-      "invite_count": 0,
-      "are_guidelines_accepted": true,
-      "is_deleted": false,
-      "visibility": "O"
-    }
-  },
-  {
-    "model": "openbook_auth.UserProfile",
-    "pk": "ac17e49d-7eeb-4e8f-a34d-dc4ee0921c67",
-    "fields": {
-      "user_id": 4,
-      "followers_count_visible": false,
-      "is_of_legal_age": true,
-      "community_posts_visible": true,
-      "interests": ["3c285e6f-239e-43a5-ae83-927ce8694ca9", "13049222-f742-49f1-8091-760c2100d7c8"],
-      "sdgs": ["ec00e781-ed8f-4295-9429-28be990bb42d", "681124cc-1acb-4b59-b616-868b48837c74"],
-      "skills": ["df485e09-9e38-420b-a990-fc94211e1b84"],
-      "name": "Kaitlyn",
-      "last_name": "Veum",
-      "avatar": "https://cloudflare-ipfs.com/ipfs/Qmd3W5DuhgHirLHGVixi6V76LhCkZUz6pnFt5AJBiyvHye/avatar/513.jpg",
-      "engagement_level": "ENGAGED",
-      "about_me": "At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet."
-    }
-  }
+	{
+		"model": "openbook_auth.User",
+		"pk": "1",
+		"fields": {
+			"username": "9ebfc2c2-2d2b-413d-8bfd-ec30af1b7d49",
+			"password": "F0OB4R",
+			"is_superuser": false,
+			"is_staff": false,
+			"is_active": true,
+			"date_joined": "2022-08-25 12:08:16+00",
+			"email": "oswald_rippin@example.com",
+			"is_email_verified": false,
+			"uuid": "9ebfc2c2-2d2b-413d-8bfd-ec30af1b7d49",
+			"invite_count": 0,
+			"are_guidelines_accepted": true,
+			"is_deleted": false,
+			"visibility": "O"
+		}
+	},
+	{
+		"model": "openbook_auth.UserProfile",
+		"pk": "9829b4fb-9c99-44b5-8568-476be3ae01f9",
+		"fields": {
+			"user_id": 1,
+			"followers_count_visible": false,
+			"is_of_legal_age": true,
+			"community_posts_visible": true,
+			"interests": [
+				"2ebecfaa-3a6d-4f39-9389-28bbbca628c9",
+				"e61f3914-930e-4178-a954-73c7a129bd1f",
+				"6244ff16-823b-4c51-b695-58b1aadab32c"
+			],
+			"interests_v2": [
+				"food-hunger-relief",
+				"mental-physical-health",
+				"climate-environment"
+			],
+			"sdgs": ["4c832a1f-9b1d-4bb7-8186-57a8eaacf1ef"],
+			"skills": ["fcc4a764-d646-4fc6-ba8a-41dc4e224022"],
+			"skills_v2": ["fundraising"],
+			"name": "Oswald",
+			"last_name": "Rippin",
+			"avatar": "https://cloudflare-ipfs.com/ipfs/Qmd3W5DuhgHirLHGVixi6V76LhCkZUz6pnFt5AJBiyvHye/avatar/871.jpg",
+			"engagement_level": "INTERESTED"
+		}
+	},
+	{
+		"model": "openbook_auth.User",
+		"pk": "2",
+		"fields": {
+			"username": "5846dd02-e2c9-4350-9e68-e4c79bf02e02",
+			"password": "F0OB4R",
+			"is_superuser": false,
+			"is_staff": false,
+			"is_active": true,
+			"date_joined": "2022-08-24 12:08:16+00",
+			"email": "lamar_maggio@example.com",
+			"is_email_verified": false,
+			"uuid": "5846dd02-e2c9-4350-9e68-e4c79bf02e02",
+			"invite_count": 0,
+			"are_guidelines_accepted": true,
+			"is_deleted": false,
+			"visibility": "O"
+		}
+	},
+	{
+		"model": "openbook_auth.UserProfile",
+		"pk": "4ee7305e-ef9b-42ec-9b85-a4602f01a72e",
+		"fields": {
+			"user_id": 2,
+			"followers_count_visible": false,
+			"is_of_legal_age": true,
+			"community_posts_visible": true,
+			"interests": [],
+			"interests_v2": [],
+			"sdgs": [],
+			"skills": [],
+			"skills_v2": []
+		}
+	},
+	{
+		"model": "openbook_auth.User",
+		"pk": "3",
+		"fields": {
+			"username": "a09124b7-cdaf-4922-9c02-10b40b3b1530",
+			"password": "F0OB4R",
+			"is_superuser": false,
+			"is_staff": false,
+			"is_active": true,
+			"date_joined": "2022-08-23 12:08:16+00",
+			"email": "nelda_auer@example.com",
+			"is_email_verified": false,
+			"uuid": "a09124b7-cdaf-4922-9c02-10b40b3b1530",
+			"invite_count": 0,
+			"are_guidelines_accepted": true,
+			"is_deleted": false,
+			"visibility": "O"
+		}
+	},
+	{
+		"model": "openbook_auth.UserProfile",
+		"pk": "46fd9173-5895-4c3e-997d-568f0838ab24",
+		"fields": {
+			"user_id": 3,
+			"followers_count_visible": false,
+			"is_of_legal_age": true,
+			"community_posts_visible": true,
+			"interests": ["9cafd8e2-85ab-4e54-9cca-f4327015648a"],
+			"interests_v2": ["gender-equality"],
+			"sdgs": ["9fbb8ea2-c82d-44cc-867e-2f94032f4d37"],
+			"skills": [
+				"f8399855-4123-4ed6-9595-e0fb65bc2ade",
+				"8c0e9623-0b29-4203-a4aa-ebfbe5ef39e5"
+			],
+			"skills_v2": ["finances-taxes", "translation"],
+			"name": "Nelda",
+			"last_name": "Auer",
+			"engagement_level": "INTERESTED",
+			"about_me": "Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua."
+		}
+	},
+	{
+		"model": "openbook_auth.User",
+		"pk": "4",
+		"fields": {
+			"username": "d2384748-0151-40d6-8527-0fe182112a6c",
+			"password": "F0OB4R",
+			"is_superuser": false,
+			"is_staff": false,
+			"is_active": true,
+			"date_joined": "2022-08-22 12:08:16+00",
+			"email": "kaitlyn_veum@example.com",
+			"is_email_verified": false,
+			"uuid": "d2384748-0151-40d6-8527-0fe182112a6c",
+			"invite_count": 0,
+			"are_guidelines_accepted": true,
+			"is_deleted": false,
+			"visibility": "O"
+		}
+	},
+	{
+		"model": "openbook_auth.UserProfile",
+		"pk": "ac17e49d-7eeb-4e8f-a34d-dc4ee0921c67",
+		"fields": {
+			"user_id": 4,
+			"followers_count_visible": false,
+			"is_of_legal_age": true,
+			"community_posts_visible": true,
+			"interests": [
+				"3c285e6f-239e-43a5-ae83-927ce8694ca9",
+				"13049222-f742-49f1-8091-760c2100d7c8"
+			],
+			"interests_v2": ["animal-welfare", "climate-environment"],
+			"sdgs": [
+				"ec00e781-ed8f-4295-9429-28be990bb42d",
+				"681124cc-1acb-4b59-b616-868b48837c74"
+			],
+			"skills": ["df485e09-9e38-420b-a990-fc94211e1b84"],
+			"skills_v2": ["research-analysis"],
+			"name": "Kaitlyn",
+			"last_name": "Veum",
+			"avatar": "https://cloudflare-ipfs.com/ipfs/Qmd3W5DuhgHirLHGVixi6V76LhCkZUz6pnFt5AJBiyvHye/avatar/513.jpg",
+			"engagement_level": "ENGAGED",
+			"about_me": "At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet."
+		}
+	}
 ]
diff --git a/openbook_auth/migrations/0035_userprofile_interests_v2_userprofile_skills_v2.py b/openbook_auth/migrations/0035_userprofile_interests_v2_userprofile_skills_v2.py
new file mode 100644
index 0000000000000000000000000000000000000000..af94d848729699c6a1a5683b83304a206bc393fd
--- /dev/null
+++ b/openbook_auth/migrations/0035_userprofile_interests_v2_userprofile_skills_v2.py
@@ -0,0 +1,45 @@
+# Generated by Django 5.0.8 on 2025-02-24 16:15
+
+import django.contrib.postgres.fields
+from django.db import migrations, models
+
+from openbook_terms.helpers import topics_to_topic_v2_slugs, skills_to_skill_v2_slugs
+
+
+def migrate_topics_and_skills(apps, schema_editor):
+    UserProfile = apps.get_model("openbook_auth", "UserProfile")
+
+    for user_profile in UserProfile.objects.all():
+        old_interests = user_profile.interests.all()
+        new_interests = topics_to_topic_v2_slugs(old_interests)
+
+        old_skills = user_profile.skills.all()
+        new_skills = skills_to_skill_v2_slugs(old_skills)
+
+        user_profile.interests_v2 = new_interests
+        user_profile.skills_v2 = new_skills
+        user_profile.save()
+
+
+class Migration(migrations.Migration):
+    dependencies = [
+        ("openbook_auth", "0034_remove_userprofile_cover_and_more"),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name="userprofile",
+            name="interests_v2",
+            field=django.contrib.postgres.fields.ArrayField(
+                base_field=models.CharField(max_length=128), blank=True, default=list, size=None
+            ),
+        ),
+        migrations.AddField(
+            model_name="userprofile",
+            name="skills_v2",
+            field=django.contrib.postgres.fields.ArrayField(
+                base_field=models.CharField(max_length=128), blank=True, default=list, size=None
+            ),
+        ),
+        migrations.RunPython(migrate_topics_and_skills),
+    ]
diff --git a/openbook_auth/migrations/tests/__init__.py b/openbook_auth/migrations/tests/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/openbook_auth/migrations/tests/test_0035_userprofile_interests_v2_userprofile_skills_v2.py b/openbook_auth/migrations/tests/test_0035_userprofile_interests_v2_userprofile_skills_v2.py
new file mode 100644
index 0000000000000000000000000000000000000000..aef6c38bebb9feb0f94178878afc3785b9f53bb4
--- /dev/null
+++ b/openbook_auth/migrations/tests/test_0035_userprofile_interests_v2_userprofile_skills_v2.py
@@ -0,0 +1,26 @@
+from importlib import import_module
+import pytest
+from django.apps import apps
+from django.db import connection
+from openbook_auth.models import User
+from openbook_common.tests.helpers import make_user, make_topic, make_skill
+# Required because of file name starting with a number
+migration = import_module('openbook_auth.migrations.0035_userprofile_interests_v2_userprofile_skills_v2')
+
+@pytest.mark.django_db
+class TestMigrateUserProfileTopicsAndSkills:
+    def test_migrate_topics_and_skills(self):
+        # GIVEN a user with only V1 interests and skills
+        interests_v1 = [make_topic(slug="eco-tourism"), make_topic(slug="elderly-people")]
+        skills_v1 = [make_skill(slug="writing-translation"), make_skill(slug="assertiveness")]
+        user = make_user(interests=[], skills=[], interests_v2=[], skills_v2=[])
+        user.profile.interests.set(interests_v1)
+        user.profile.skills.set(skills_v1)
+
+        # WHEN migrating user profile interests and skills from V1 to V2
+        migration.migrate_topics_and_skills(apps, connection.schema_editor())
+
+        # THEN the user has mapped V2 interests and skills
+        updated_profile = User.objects.get(username=user.username).profile
+        assert sorted(updated_profile.interests_v2) == sorted(["lifestyle-consumption", "elderly-care"])
+        assert sorted(updated_profile.skills_v2) == sorted(["translation", "other"])
diff --git a/openbook_auth/models.py b/openbook_auth/models.py
index 905f06fe21099b34a348bc16af56f0fde5c9235a..b19fc5fde736a6e11c499d9197bb447662a6d942 100644
--- a/openbook_auth/models.py
+++ b/openbook_auth/models.py
@@ -5,14 +5,16 @@ import secrets
 from datetime import datetime, timedelta
 from enum import Enum
 from itertools import chain
-from typing import Iterable, Set, Callable, Type, Optional
+from typing import Iterable, Set, Callable, Type, Optional, List
 from uuid import UUID
 
 import strawberry
+from openbook_terms.helpers import topics_to_topic_v2_slugs, skills_to_skill_v2_slugs
 from django.contrib.auth.models import AbstractUser, UserManager
 from django.contrib.auth.validators import UnicodeUsernameValidator
 from django.contrib.contenttypes.fields import GenericRelation
 from django.contrib.gis.db import models
+from django.contrib.postgres.fields import ArrayField
 from django.core import validators
 from django.core.cache import caches
 from django.core.mail import EmailMultiAlternatives
@@ -38,6 +40,8 @@ from openbook_common.enums import VisibilityType
 from openbook_common.helpers import ExifRotate, generate_blurhash, PrefixedQ
 from openbook_common.models import Badge, ModelWithUUID
 from openbook_common.schema.types import Paged
+from openbook_common.skills import slug_to_skill_v2
+from openbook_common.topics import slug_to_topic_v2
 from openbook_common.types import GeolocationFeature, ReactableType
 from openbook_common.utils.helpers import delete_file_field
 from openbook_common.utils.model_loaders import (
@@ -788,9 +792,11 @@ class User(ModelWithUUID, AbstractUser):
         last_name=None,
         pronouns=None,
         engagement_level=None,
-        interests=None,
+        interests: Optional[List[Topic]] = None,
+        interests_v2: Optional[List[str]] = None,
         sdgs=None,
-        skills=None,
+        skills: Optional[List[Skill]] = None,
+        skills_v2: Optional[List[str]] = None,
         save=True,
         update_location=False,
         remove_avatar=False,
@@ -851,11 +857,19 @@ class User(ModelWithUUID, AbstractUser):
         if engagement_level:
             self.profile.engagement_level = engagement_level
         if interests is not None:
-            self.profile.interests.set(interests)
+            updated_interests = Topic.objects.filter(id__in=interests)
+            self.profile.interests.set(updated_interests)
+            self.profile.interests_v2 = topics_to_topic_v2_slugs(updated_interests)
+        elif interests_v2 is not None:
+            self.profile.interests_v2 = interests_v2
         if sdgs is not None:
             self.profile.sdgs.set(sdgs)
         if skills is not None:
-            self.profile.skills.set(skills)
+            updated_skills = Skill.objects.filter(id__in=skills)
+            self.profile.skills.set(updated_skills)
+            self.profile.skills_v2 = skills_to_skill_v2_slugs(updated_skills)
+        elif skills_v2 is not None:
+            self.profile.skills_v2 = skills_v2
 
         if remove_avatar:
             self._delete_profile_avatar(save=False)
@@ -1721,6 +1735,7 @@ class User(ModelWithUUID, AbstractUser):
         invites_enabled=None,
         location=None,
         topic_ids=None,
+        topics_v2=None,
     ):
         Community = get_community_model()
         community = Community.create_community(
@@ -1738,6 +1753,7 @@ class User(ModelWithUUID, AbstractUser):
             invites_enabled=invites_enabled,
             location=location,
             topic_ids=topic_ids,
+            topics_v2=topics_v2,
         )
 
         return community
@@ -1803,6 +1819,7 @@ class User(ModelWithUUID, AbstractUser):
         categories_names=None,
         invites_enabled=None,
         topic_ids=None,
+        topics_v2=None,
         cover=None,
         remove_cover=False,
         avatar=None,
@@ -1830,6 +1847,7 @@ class User(ModelWithUUID, AbstractUser):
             categories_names=categories_names,
             invites_enabled=invites_enabled,
             topic_ids=topic_ids,
+            topics_v2=topics_v2,
             cover=cover,
             remove_cover=remove_cover,
             avatar=avatar,
@@ -1856,6 +1874,7 @@ class User(ModelWithUUID, AbstractUser):
         categories_names=None,
         invites_enabled=None,
         topic_ids=None,
+        topics_v2=None,
         cover=None,
         remove_cover=False,
         avatar=None,
@@ -1886,6 +1905,7 @@ class User(ModelWithUUID, AbstractUser):
             categories_names=categories_names,
             invites_enabled=invites_enabled,
             topic_ids=topic_ids,
+            topics_v2=topics_v2,
             cover=cover,
             remove_cover=remove_cover,
             avatar=avatar,
@@ -4938,6 +4958,7 @@ class User(ModelWithUUID, AbstractUser):
         regularity=None,
         contact_phone=None,
         skills=None,
+        skills_v2=None,
         thumbnail=None,
     ):
         Community = get_community_model()
@@ -4961,6 +4982,7 @@ class User(ModelWithUUID, AbstractUser):
             regularity=regularity,
             contact_phone=contact_phone,
             skills=skills,
+            skills_v2=skills_v2,
             thumbnail=thumbnail,
         )
 
@@ -4978,7 +5000,8 @@ class User(ModelWithUUID, AbstractUser):
         geolocation=None,
         regularity=None,
         contact_phone=None,
-        skills=None,
+        skill_ids=None,
+        skills_v2=None,
         thumbnail=None,
         remove_thumbnail=False,
     ):
@@ -5000,7 +5023,8 @@ class User(ModelWithUUID, AbstractUser):
             geolocation=geolocation,
             regularity=regularity,
             contact_phone=contact_phone,
-            skills=skills,
+            skill_ids=skill_ids,
+            skills_v2=skills_v2,
             thumbnail=thumbnail,
             remove_thumbnail=remove_thumbnail,
         )
@@ -5198,8 +5222,10 @@ class UserProfile(ModelWithUUID):
         null=True,
     )
     interests = models.ManyToManyField(Topic, blank=True)
+    interests_v2 = ArrayField(models.CharField(max_length=settings.TERM_SLUG_MAX_LENGTH), blank=True, default=list)
     sdgs = models.ManyToManyField(SDG, blank=True)
     skills = models.ManyToManyField(Skill, blank=True)
+    skills_v2 = ArrayField(models.CharField(max_length=settings.TERM_SLUG_MAX_LENGTH), blank=True, default=list)
     is_of_legal_age = models.BooleanField(default=False)
     avatar = ProcessedImageField(
         verbose_name=_("avatar"),
diff --git a/openbook_auth/schema/mutations.py b/openbook_auth/schema/mutations.py
index 7728ec0804c9bcfabc40e650cf80b31bfd9265d7..272889f7c657faaee6e112e598b1b6745da4dc75 100644
--- a/openbook_auth/schema/mutations.py
+++ b/openbook_auth/schema/mutations.py
@@ -35,7 +35,7 @@ from openbook.graphql_errors import GraphQLErrorCodes
 from openbook_common.helpers import is_parameter_null, to_input_dict
 from openbook_common.schema.types import Result
 from openbook_common.utils.model_loaders import get_insight_model, get_community_model, get_topic_model
-from openbook_communities.schema.helpers import create_filtered_spaces_query
+from openbook_communities.schema.helpers import create_filtered_spaces_query, DISCOVER_DEFAULT_SPACES_V2
 from openbook_notifications.api import NotificationApi
 from openbook_notifications.enums import NotificationProvider
 from openbook_devices.serializers import RegisterDeviceSerializer
@@ -55,9 +55,9 @@ def _capture_user_fields(user: UserModel) -> Dict[str, str]:
         "aboutMe": user.profile.about_me,
         "pronouns": user.profile.pronouns,
         "engagementLevel": user.profile.engagement_level,
-        "interests": set(user.profile.interests.values_list("id", flat=True)),
+        "interests": set(user.profile.interests.values_list("id", flat=True)) | set(user.profile.interests_v2),
         "sdgs": set(user.profile.sdgs.values_list("id", flat=True)),
-        "skills": set(user.profile.skills.values_list("id", flat=True)),
+        "skills": set(user.profile.skills.values_list("id", flat=True)) | set(user.profile.skills_v2),
         "avatar": user.profile.avatar.url if user.profile.avatar else None,
     }
 
@@ -106,8 +106,10 @@ class Mutation:
             update_location=input.location is not strawberry.UNSET or input.geolocation is not strawberry.UNSET,
             engagement_level=data.get("engagement_level"),
             interests=data.get("interests"),
+            interests_v2=data.get("interests_v2"),
             sdgs=data.get("sdgs"),
             skills=data.get("skills"),
+            skills_v2=data.get("skills_v2"),
             avatar=data.get("avatar"),
             remove_avatar=remove_avatar,
             tracking_consent_analytics=data.get("tracking_consent_analytics"),
@@ -137,22 +139,18 @@ class Mutation:
             topic_model = get_topic_model()
 
             whens = []
-            topic_ids = list(current_user.profile.interests.values_list("id", flat=True).all())
+            topics_v2 = list(current_user.profile.interests_v2)
             geolocation = current_user.geolocation
 
-            if topic_ids:
+            if topics_v2:
                 whens.append(
-                    When(create_filtered_spaces_query(topic_ids=topic_ids, geolocation=geolocation), then=Value(10))
+                    When(create_filtered_spaces_query(topics_v2=topics_v2, geolocation=geolocation), then=Value(10))
                 )
 
             if geolocation:
                 whens.append(When(create_filtered_spaces_query(geolocation=geolocation), then=Value(20)))
 
-            default_topics = list(
-                topic_model.objects.filter(is_discover_spaces_default=True).values_list("id", flat=True)
-            )
-            if default_topics:
-                whens.append(When(create_filtered_spaces_query(topic_ids=default_topics), then=Value(30)))
+            whens.append(When(create_filtered_spaces_query(topics_v2=DISCOVER_DEFAULT_SPACES_V2), then=Value(30)))
 
             spaces = (
                 community_model.objects.prefetch_related("links", "memberships__user__profile")
diff --git a/openbook_auth/schema/types.py b/openbook_auth/schema/types.py
index dfaddd2e7e7d87a1b6b2618fc459134e8e136b9a..e4266c0b3cd9acf801feebe06eb97c8a4b33f0fd 100644
--- a/openbook_auth/schema/types.py
+++ b/openbook_auth/schema/types.py
@@ -1,6 +1,6 @@
 import logging
 import uuid
-from typing import List, Optional, TYPE_CHECKING
+from typing import List, Optional, TYPE_CHECKING, Annotated
 
 import strawberry
 import strawberry_django
@@ -33,9 +33,17 @@ class UpdateAuthenticatedUserInput:
     location: Optional[str] = strawberry.UNSET
     geolocation: Optional[GeoJSONInput] = strawberry.UNSET
     engagement_level: Optional[str] = strawberry.UNSET
-    interests: Optional[List[Optional[uuid.UUID]]] = strawberry.UNSET
+    interests: Annotated[
+        Optional[List[Optional[uuid.UUID]]],
+        strawberry.argument(description="Deprecated since 1.52. Use interests_v2 instead."),
+    ] = strawberry.UNSET
+    interests_v2: Optional[List[str]] = strawberry.UNSET
     sdgs: Optional[List[Optional[uuid.UUID]]] = strawberry.UNSET
-    skills: Optional[List[Optional[uuid.UUID]]] = strawberry.UNSET
+    skills: Annotated[
+        Optional[List[Optional[uuid.UUID]]],
+        strawberry.argument(description="Deprecated since 1.52. Use skills_v2 instead."),
+    ] = strawberry.UNSET
+    skills_v2: Optional[List[str]] = strawberry.UNSET
     tracking_consent_analytics: Optional[bool] = strawberry.UNSET
     tracking_consent_personalization: Optional[bool] = strawberry.UNSET
 
@@ -94,18 +102,30 @@ class User:
     def engagement_level(self) -> Optional[str]:
         return self.profile.engagement_level
 
-    @strawberry_django.field(select_related=["profile__interests"])
+    @strawberry_django.field(
+        select_related=["profile__interests"], deprecation_reason="Deprecated since 1.52. Use interests_v2 instead."
+    )
     def interests(self) -> List[Optional[Topic]]:
         return self.profile.interests.all()
 
+    @strawberry_django.field(select_related=["profile"])
+    def interests_v2(self) -> List[Optional[str]]:
+        return self.profile.interests_v2
+
     @strawberry_django.field(select_related=["profile__sdgs"])
     def sdgs(self) -> List[Optional[SDG]]:
         return self.profile.sdgs.all().order_by("number")
 
-    @strawberry_django.field(select_related=["profile__skills"])
+    @strawberry_django.field(
+        select_related=["profile__skills"], deprecation_reason="Deprecated since 1.52. Use skills_v2 instead."
+    )
     def skills(self) -> List[Optional[Skill]]:
         return self.profile.skills.all()
 
+    @strawberry_django.field(select_related=["profile"])
+    def skills_v2(self) -> List[Optional[str]]:
+        return self.profile.skills_v2
+
     @strawberry_django.field()
     def connection_status_to_myself(self, info) -> Optional[UserConnectionStatus]:
         user = info.context.request.user
diff --git a/openbook_auth/serializers/authenticated_user_serializers.py b/openbook_auth/serializers/authenticated_user_serializers.py
index 916dca8a17fffd90da1d81ef70e57b57ebe7f73f..a1613c4762b58a08302da941b29979679078bd3e 100644
--- a/openbook_auth/serializers/authenticated_user_serializers.py
+++ b/openbook_auth/serializers/authenticated_user_serializers.py
@@ -21,7 +21,13 @@ from openbook_common.serializers_fields.request import (
     FriendlyUrlField,
     RestrictedImageFileSizeField,
 )
-from openbook_common.validators import name_characters_validator, sanitize_text, url_safety_validator
+from openbook_common.validators import (
+    name_characters_validator,
+    sanitize_text,
+    url_safety_validator,
+    topic_v2_exists,
+    skill_v2_exists,
+)
 
 
 class UpdateAuthenticatedUserSerializer(serializers.Serializer):
@@ -83,6 +89,11 @@ class UpdateAuthenticatedUserSerializer(serializers.Serializer):
         allow_empty=True,
         child=serializers.UUIDField(validators=[interest_with_id_exists]),
     )
+    interests_v2 = serializers.ListField(
+        required=False,
+        allow_empty=True,
+        child=serializers.CharField(max_length=settings.TERM_SLUG_MAX_LENGTH, validators=[topic_v2_exists]),
+    )
     sdgs = serializers.ListField(
         required=False,
         allow_empty=True,
@@ -93,6 +104,11 @@ class UpdateAuthenticatedUserSerializer(serializers.Serializer):
         allow_empty=True,
         child=serializers.UUIDField(validators=[skill_with_id_exists]),
     )
+    skills_v2 = serializers.ListField(
+        required=False,
+        allow_empty=True,
+        child=serializers.CharField(max_length=settings.TERM_SLUG_MAX_LENGTH, validators=[skill_v2_exists]),
+    )
     identity = serializers.CharField(
         min_length=1,
         max_length=settings.IDENTITY_MAX_LENGTH,
diff --git a/openbook_auth/tests/helpers.py b/openbook_auth/tests/helpers.py
index 052554c310c501c8cb30391dbaf88a1572d57582..ec1cc5c01bc3056cb4fda06fd798c95de6aa82b1 100644
--- a/openbook_auth/tests/helpers.py
+++ b/openbook_auth/tests/helpers.py
@@ -14,19 +14,23 @@ def get_user_with_id_data(user_to_get, calling_user=None):
     return response.data["userById"]
 
 
-def get_user_with_id_raw(user_to_get, calling_user=None):
+def get_user_by_id_raw(user_to_get, fields: str = "id", calling_user=None):
     return async_to_sync(schema.execute)(
         f"""
-        query {{
-            userById(id: "{user_to_get.username}") {{
-                id
+            query {{
+                userById(id: "{user_to_get.username}") {{
+                    {fields}
+                }}
             }}
-        }}
-        """,
+            """,
         context_value=benedict({"request": {"user": calling_user or AnonymousUser}}),
     )
 
 
+def get_user_with_id_raw(user_to_get, calling_user=None):
+    return get_user_by_id_raw(user_to_get, fields="id", calling_user=calling_user)
+
+
 def get_users_by_chat_identities(user: User, identities: List[str]):
     response = async_to_sync(schema.execute)(
         """
diff --git a/openbook_auth/tests/test_graphql.py b/openbook_auth/tests/test_graphql.py
index d479cdb6d716ebd5c83cdbd7114f0f3d2e55c052..347f40d1155288a1fd45160f593fa94911a8e0ef 100644
--- a/openbook_auth/tests/test_graphql.py
+++ b/openbook_auth/tests/test_graphql.py
@@ -30,6 +30,7 @@ from openbook_auth.tests.helpers import (
     api_update_authenticated_user_names_data,
     get_user_connection_status_to_space_data,
     get_user_with_id_raw,
+    get_user_by_id_raw,
     get_users_by_chat_identities,
     get_connected_users,
     get_connected_users_raw,
@@ -525,6 +526,163 @@ class TestUsers:
         )
         self._validate_tracking_event_user_updated(mock_track)
 
+    @mock.patch("openbook_auth.tasks.requests.patch")
+    @mock.patch("openbook_auth.schema.mutations.publish_user_name_updated_event")
+    @mock.patch("openbook_auth.schema.mutations.publish_user_updated_event")
+    @mock.patch("django.utils.translation.get_language_from_request", return_value="en")
+    @mock.patch("openbook_notifications.api.NotificationApi.create_or_update_subscriber")
+    @mock.patch("openbook_auth.schema.mutations.track")
+    def test_updating_topics_skills_is_backward_compatible(
+        self,
+        mock_track,
+        mock_create_or_update_subscriber,
+        mock_get_language_from_request,
+        mock_publish_user_name_updated_event,
+        mock_publish_user_updated_event,
+        mock_patch,
+    ):
+        # test exemplary topics and skills, there are separate unit tests for exhaustive testing the
+        # backwards-compatibility mapping itself
+
+        # GIVEN V1 topic slugs and V1 skill slugs
+        topics_v1 = [make_topic(slug="biodiversity-animal-welfare"), make_topic(slug="agriculture-food")]
+        skills_v1 = [make_skill(slug="development-concept"), make_skill(slug="empathy")]
+        user = make_user(interests=[], skills=[], interests_v2=[], skills_v2=[])
+
+        # WHEN updating topics and skills of a user using the V1 API
+        self._update_user(
+            user,
+            {"interests": [str(topic.id) for topic in topics_v1], "skills": [str(skill.id) for skill in skills_v1]},
+        )
+
+        # THEN they are updated for the V1 API
+        response = get_user_by_id_raw(
+            user,
+            fields="""
+                interests { slug }
+                skills { slug }
+                interestsV2
+                skillsV2
+            """,
+        )
+        user_data = response.data["userById"]
+
+        assert len(user_data["interests"]) == 2
+        assert sorted([interest["slug"] for interest in user_data["interests"]]) == [
+            "agriculture-food",
+            "biodiversity-animal-welfare",
+        ]
+
+        assert len(user_data["skills"]) == 2
+        assert sorted([skill["slug"] for skill in user_data["skills"]]) == ["development-concept", "empathy"]
+
+        # AND THEN they are updated in a backwards-compatible way in the V2 API
+        assert len(user_data["interestsV2"]) == 2
+        assert sorted(user_data["interestsV2"]) == ["animal-welfare", "food-hunger-relief"]
+        assert len(user_data["skillsV2"]) == 2
+        assert sorted(user_data["skillsV2"]) == ["creativity-design", "other"]
+
+    @mock.patch("openbook_auth.tasks.requests.patch")
+    @mock.patch("openbook_auth.schema.mutations.publish_user_name_updated_event")
+    @mock.patch("openbook_auth.schema.mutations.publish_user_updated_event")
+    @mock.patch("django.utils.translation.get_language_from_request", return_value="en")
+    @mock.patch("openbook_notifications.api.NotificationApi.create_or_update_subscriber")
+    @mock.patch("openbook_auth.schema.mutations.track")
+    def test_updating_topics_skills_v2(
+        self,
+        mock_track,
+        mock_create_or_update_subscriber,
+        mock_get_language_from_request,
+        mock_publish_user_name_updated_event,
+        mock_publish_user_updated_event,
+        mock_patch,
+    ):
+        # GIVEN V2 topic slugs and V2 skill slugs
+        topics_v2 = ["human-rights", "literacy-education"]
+        skills_v2 = ["legal-assistance", "nursing-caregiving"]
+        user = make_user(interests=[], skills=[], interests_v2=[], skills_v2=[])
+
+        # WHEN updating topics and skills of a user using the V2 API
+        self._update_user(
+            user,
+            {"interestsV2": topics_v2, "skillsV2": skills_v2},
+        )
+
+        # THEN they are updated for the V2 API
+        response = get_user_by_id_raw(
+            user,
+            fields="""
+                interests { slug }
+                skills { slug }
+                interestsV2
+                skillsV2
+            """,
+        )
+        user_data = response.data["userById"]
+
+        assert len(user_data["interestsV2"]) == 2
+        assert user_data["interestsV2"] == topics_v2
+        assert len(user_data["skillsV2"]) == 2
+        assert user_data["skillsV2"] == skills_v2
+
+        # AND THEN they are NOT updated in the V1 API
+        assert len(user_data["interests"]) == 0
+        assert len(user_data["skills"]) == 0
+
+    @mock.patch("openbook_auth.tasks.requests.patch")
+    @mock.patch("openbook_auth.schema.mutations.publish_user_name_updated_event")
+    @mock.patch("openbook_auth.schema.mutations.publish_user_updated_event")
+    @mock.patch("django.utils.translation.get_language_from_request", return_value="en")
+    @mock.patch("openbook_notifications.api.NotificationApi.create_or_update_subscriber")
+    @mock.patch("openbook_auth.schema.mutations.track")
+    def test_validating_topics_skills_v2(
+        self,
+        mock_track,
+        mock_create_or_update_subscriber,
+        mock_get_language_from_request,
+        mock_publish_user_name_updated_event,
+        mock_publish_user_updated_event,
+        mock_patch,
+    ):
+        # GIVEN V2 topic slugs and V2 skill slugs with invalid values
+        topics_v2 = ["literacy-education", "invalid"]
+        skills_v2 = ["unknown", "nursing-caregiving"]
+        user = make_user()
+
+        # WHEN updating topics and skills of a user using the V2 API
+        request = HttpRequest()
+        request.COOKIES = {}
+        request.user = user
+        response = async_to_sync(schema.execute)(
+            """
+            mutation updateAuthenticatedUserV2($input: UpdateAuthenticatedUserInput!) {
+                updateAuthenticatedUserV2(input: $input) { id }
+            }
+        """,
+            context_value=benedict({"request": request}),
+            variable_values={"input": {"interestsV2": topics_v2, "skillsV2": skills_v2}},
+        )
+
+        # THEN the request fails
+        assert response.data is None
+        assert response.errors is not None
+
+    # noinspection PyMethodMayBeStatic
+    def _update_user(self, user: User, inputs: Dict[str, any]) -> None:
+        request = HttpRequest()
+        request.COOKIES = {}
+        request.user = user
+        executed = async_to_sync(schema.execute)(
+            """
+            mutation updateAuthenticatedUserV2($input: UpdateAuthenticatedUserInput!) {
+                updateAuthenticatedUserV2(input: $input) { id }
+            }
+        """,
+            context_value=benedict({"request": request}),
+            variable_values={"input": inputs},
+        )
+        assert executed.errors is None
+
     @mock.patch("openbook_auth.tasks.requests.patch")
     @mock.patch("openbook_auth.schema.mutations.publish_user_name_updated_event")
     @mock.patch("openbook_auth.schema.mutations.publish_user_updated_event")
@@ -542,57 +700,50 @@ class TestUsers:
     ):
         response = self.create_response(200)
         mock_patch.return_value = response
-        topic = make_topic()
-        skill = make_skill()
+        topic = make_topic(slug="animal-welfare")
+        topic_v2 = "arts-culture"
+
+        skill = make_skill(slug="animal-care")
+        skill_v2 = "arts-crafts"
+
         sdg = make_sdg()
         user = make_user(name="John", last_name="Doe", engagement_level=EngagementLevelChoice.ENGAGED.value)
 
-        def update_user(inputs: Dict[str, any]) -> None:
-            request = HttpRequest()
-            request.COOKIES = {}
-            request.user = user
-            executed = async_to_sync(schema.execute)(
-                """
-                mutation updateAuthenticatedUserV2($input: UpdateAuthenticatedUserInput!) {
-                    updateAuthenticatedUserV2(input: $input) { id }
-                }
-            """,
-                context_value=benedict({"request": request}),
-                variable_values={"input": inputs},
-            )
-            assert executed.errors is None
-
-        update_user({})
+        self._update_user(user, {})
         mock_track.assert_not_called()
         mock_track.reset_mock()
 
         # should not contain updated_fields if field values didn't change
-        update_user({"firstName": "John", "lastName": "Doe"})
+        self._update_user(user, {"firstName": "John", "lastName": "Doe"})
         mock_track.assert_not_called()
         mock_track.reset_mock()
 
-        update_user({"firstName": "Johnny", "lastName": "Doey", "aboutMe": "All about me"})
+        self._update_user(user, {"firstName": "Johnny", "lastName": "Doey", "aboutMe": "All about me"})
         self._validate_tracking_event_user_updated(mock_track, {"firstName", "lastName", "aboutMe"})
         mock_track.reset_mock()
 
-        update_user({"location": "New York", "geolocation": geolocation})
+        self._update_user(user, {"location": "New York", "geolocation": geolocation})
         self._validate_tracking_event_user_updated(mock_track, {"location", "geolocation"})
         mock_track.reset_mock()
 
-        update_user({"pronouns": "They/Them", "engagementLevel": EngagementLevelChoice.INITIATOR.value})
+        self._update_user(user, {"pronouns": "They/Them", "engagementLevel": EngagementLevelChoice.INITIATOR.value})
         self._validate_tracking_event_user_updated(mock_track, {"pronouns", "engagementLevel"})
         mock_track.reset_mock()
 
-        update_user({"interests": [str(topic.id)], "sdgs": [str(sdg.id)], "skills": [str(skill.id)]})
+        self._update_user(user, {"interests": [str(topic.id)], "sdgs": [str(sdg.id)], "skills": [str(skill.id)]})
         self._validate_tracking_event_user_updated(mock_track, {"interests", "sdgs", "skills"})
         mock_track.reset_mock()
 
+        self._update_user(user, {"interestsV2": [topic_v2], "skillsV2": [skill_v2]})
+        self._validate_tracking_event_user_updated(mock_track, {"interests", "skills"})
+        mock_track.reset_mock()
+
         current_dir = os.path.dirname(os.path.abspath(__file__))
         test_image_path = os.path.join(current_dir, "test_avatar.jpg")
         with open(test_image_path, "rb") as image_file:
             image_content = image_file.read()
             image = SimpleUploadedFile(name="test_avatar.jpg", content=image_content, content_type="image/jpeg")
-        update_user({"avatar": image})
+        self._update_user(user, {"avatar": image})
         self._validate_tracking_event_user_updated(mock_track, {"avatar"})
         mock_track.reset_mock()
 
diff --git a/openbook_common/skills.py b/openbook_common/skills.py
new file mode 100644
index 0000000000000000000000000000000000000000..bd050ff4f53fbc2bc17fcbc91e9a3f5da7921149
--- /dev/null
+++ b/openbook_common/skills.py
@@ -0,0 +1,114 @@
+from typing import Optional, Literal
+
+# keys are the V2 skill slugs, values is a combined list of V1 and V2 skill slugs (for backwards-compatibility)
+skill_v2_slugs = {
+    "administration": ["administration", "organizational-skills"],
+    "animal-care": ["animal-care"],
+    "arts-crafts": ["arts-crafts", "arts-music-culture"],
+    "building-handicraft": ["building-handicraft"],
+    "childcare": ["childcare"],
+    "community-organizing": ["community-organizing"],
+    "creativity-design": ["creativity-design", "creativity", "design", "development-concept"],
+    "driving": ["driving", "drive-transport"],
+    "emergency-care": ["emergency-care"],
+    "event-planning": ["event-planning"],
+    "finances-taxes": ["finances-taxes"],
+    "food-service": ["food-service", "cooking-baking"],
+    "fundraising": ["fundraising"],
+    "gardening-conservation": ["gardening-conservation"],
+    "intercultural-competence": ["intercultural-competence", "inclusive-mindset-intercultural-competence"],
+    "it-technology": ["it-technology"],
+    "legal-assistance": ["legal-assistance", "law-regulations"],
+    "management-strategy": ["management-strategy"],
+    "marketing-communications": [
+        "marketing-communications",
+        "marketing-social-media",
+        "communication-skills",
+        "public-relations",
+    ],
+    "moderation-facilitation": ["moderation-facilitation"],
+    "music-performance": ["music-performance"],
+    "nursing-caregiving": ["nursing-caregiving", "caring-nursing"],
+    "other": [
+        "other",
+        "environmental-awareness",
+        "commitment",
+        "conflict-management",
+        "discipline",
+        "empathy",
+        "hands-on-support",
+        "holistic-thinking",
+        "humour",
+        "independence",
+        "initiative-and-energy",
+        "mindfulness",
+        "motivation",
+        "negotiation-skills",
+        "openness",
+        "relationship-skills",
+        "resilience",
+        "self-confident",
+        "solution-orientedness",
+        "willingness-to-learn",
+        "adaptability",
+        "visionary-thinking",
+        "assertiveness",
+    ],
+    "religious-spiritual-care": ["religious-spiritual-care"],
+    "research-analysis": ["research-analysis", "critical-thinking", "analytical-thinking", "knowledge-management"],
+    "sales-distribution": ["sales-distribution"],
+    "team-leadership": ["team-leadership", "coordination-coordination-of-volunteers", "leadership", "teamwork"],
+    "translation": ["translation", "writing-translation"],
+    "tutoring-coaching": ["tutoring-coaching", "teaching-education", "coaching & mentoring"],
+}
+
+SkillV2 = Literal[
+    "administration",
+    "animal-care",
+    "arts-crafts",
+    "building-handicraft",
+    "childcare",
+    "community-organizing",
+    "creativity-design",
+    "driving",
+    "emergency-care",
+    "event-planning",
+    "finances-taxes",
+    "food-service",
+    "fundraising",
+    "gardening-conservation",
+    "intercultural-competence",
+    "it-technology",
+    "legal-assistance",
+    "management-strategy",
+    "marketing-communications",
+    "moderation-facilitation",
+    "music-performance",
+    "nursing-caregiving",
+    "other",
+    "religious-spiritual-care",
+    "research-analysis",
+    "sales-distribution",
+    "team-leadership",
+    "translation",
+    "tutoring-coaching",
+]
+
+
+def slug_to_skill_v2(slug: str) -> Optional[str]:
+    """
+    Convert a slug to its corresponding V2 skill.
+    Returns None if no match is found.
+    """
+    if slug in skill_v2_slugs:
+        return slug
+
+    for skill, slugs in skill_v2_slugs.items():
+        if slug in slugs:
+            return skill
+
+    return None
+
+
+def slugs_to_skill_v2_slugs(slugs: [str]) -> [str]:
+    return list(set(filter(None, map(slug_to_skill_v2, slugs))))
diff --git a/openbook_common/tests/helpers.py b/openbook_common/tests/helpers.py
index a8c076aa91a699a0905ab457666c268009fafeec..2fbaf00e3fcf1981107713b0e434f09e9a0025cc 100644
--- a/openbook_common/tests/helpers.py
+++ b/openbook_common/tests/helpers.py
@@ -1,12 +1,13 @@
 import random
 import tempfile
 import uuid
-from typing import List, Optional
+from typing import List, Optional, get_args
 
 from PIL import Image
 from asgiref.sync import async_to_sync
 from django.conf import settings
 from django.contrib.contenttypes.models import ContentType
+from django.core.exceptions import ObjectDoesNotExist
 from django.core.files.uploadedfile import SimpleUploadedFile
 from django.utils import timezone
 from faker import Faker
@@ -19,6 +20,8 @@ from openbook_auth.models import EngagementLevelChoice, User, UserProfile, UserR
 from openbook_categories.models import Category
 from openbook_circles.models import Circle
 from openbook_common.enums import VisibilityType
+from openbook_common.topics import TopicV2
+from openbook_common.skills import SkillV2
 from openbook_common.models import (
     Badge,
     Emoji,
@@ -27,6 +30,7 @@ from openbook_common.models import (
     ProxyBlacklistedDomain,
 )
 from openbook_common.utils.helpers import get_random_pastel_color
+from openbook_common.utils.model_loaders import get_topic_model, get_skill_model
 from openbook_communities.models import CollaborationTool, Community, ExternalLink, SpacesRelationship, Task
 from openbook_devices.models import Device
 from openbook_hashtags.models import Hashtag
@@ -43,6 +47,7 @@ from openbook_moderation.models import (
 from openbook_notifications.models import Notification
 from openbook_posts.models import Post
 from openbook_terms.models import SDG, Skill, Topic, FeedPostCategory
+from openbook_terms.helpers import topics_to_topic_v2_slugs, skills_to_skill_v2_slugs
 
 fake = Faker()
 
@@ -86,6 +91,10 @@ def make_user(
     role: UserRole = UserRole.REGULAR,
     is_employee: bool = False,
     engagement_level=None,
+    interests: Optional[List[Topic]] = None,
+    interests_v2: Optional[List[str]] = None,
+    skills: Optional[List[Skill]] = None,
+    skills_v2: Optional[List[str]] = None,
 ) -> User:
     defined_arguments = {k: v for k, v in locals().items() if v is not None}
     defined_arguments.setdefault("username", str(uuid.uuid4()))
@@ -265,39 +274,73 @@ def make_category():
     return mixer.blend(Category)
 
 
-def make_topic(is_discover_spaces_default=False) -> Topic:
-    return mixer.blend(Topic, is_discover_spaces_default=is_discover_spaces_default)
+def make_topic(
+    title: Optional[str] = None, slug: Optional[str] = None, is_discover_spaces_default: bool = False
+) -> Topic:
+    TopicModel = get_topic_model()
+    if slug is not None:
+        try:
+            return TopicModel.objects.get(slug=slug)
+        except ObjectDoesNotExist:
+            pass
+
+    defined_arguments = {k: v for k, v in locals().items() if v is not None}
+    if slug is None:
+        existing_topics = [t.slug for t in TopicModel.objects.all()]
+        all_topic_slugs = set(get_args(TopicV2))
+        existing_topic_slugs = set(existing_topics)
+        defined_arguments["slug"] = random.choice(list(all_topic_slugs.difference(existing_topic_slugs)))  # noqa: S311
+    return mixer.blend(TopicModel, **defined_arguments)
 
 
 def make_sdg() -> SDG:
     return mixer.blend(SDG)
 
 
-def make_community_topics():
-    community_topics = []
+def make_topics(count: int) -> List[Topic]:
+    slugs = list(get_args(TopicV2))
+    return [make_topic(slug=slugs[index]) for index in range(count)]
 
-    for _ in range(0, settings.COMMUNITY_TOPICS_MAX_AMOUNT):
-        topic = make_topic()
-        community_topics.append(topic.id)
 
-    return community_topics
+def make_skill(title: Optional[str] = None, slug: Optional[str] = None) -> Skill:
+    SkillModel = get_skill_model()
+    if slug is not None:
+        try:
+            return SkillModel.objects.get(slug=slug)
+        except ObjectDoesNotExist:
+            pass
 
+    defined_arguments = {k: v for k, v in locals().items() if v is not None}
+    if slug is None:
+        existing_skills = [t.slug for t in SkillModel.objects.all()]
+        all_skill_slugs = set(get_args(SkillV2))
+        existing_skill_slugs = set(existing_skills)
+        defined_arguments["slug"] = random.choice(list(all_skill_slugs.difference(existing_skill_slugs)))  # noqa: S311
+    return mixer.blend(Skill, **defined_arguments)
 
-def make_skill() -> Skill:
-    return mixer.blend(Skill)
+
+def make_skills(count: int) -> List[Skill]:
+    slugs = list(get_args(SkillV2))
+    return [make_skill(slug=slugs[index]) for index in range(count)]
 
 
 def make_community_invites_enabled():
     return fake.boolean()
 
 
-def make_community_task(community, skills=None, visibility=VisibilityType.MEMBERS, is_deleted=False) -> Task:
+def make_community_task(
+    community, skills=None, skills_v2=None, visibility=VisibilityType.MEMBERS, is_deleted=False
+) -> Task:
     if not skills:
         skills = [make_skill()]
+    if not skills_v2:
+        skills_v2 = skills_to_skill_v2_slugs(skills)
 
     deleted_at = timezone.now() if is_deleted else None
 
-    return mixer.blend(Task, community=community, skills=skills, visibility=visibility, deleted_at=deleted_at)
+    return mixer.blend(
+        Task, community=community, skills=skills, skills_v2=skills_v2, visibility=visibility, deleted_at=deleted_at
+    )
 
 
 def make_community(
@@ -305,6 +348,7 @@ def make_community(
     type=Community.COMMUNITY_TYPE_PUBLIC,
     title=None,
     topics=None,
+    topics_v2=None,
     location=None,
     geolocation=None,
     admins: List[User] = None,
@@ -319,7 +363,10 @@ def make_community(
         title = make_community_title()
 
     if not topics:
-        topics = make_community_topics()
+        topics = make_topics(settings.COMMUNITY_TOPICS_MAX_AMOUNT)
+
+    if not topics_v2:
+        topics_v2 = topics_to_topic_v2_slugs(topics)
 
     if not location:
         location = fake.text(max_nb_chars=settings.COMMUNITY_LOCATION_MAX_LENGTH)
@@ -335,6 +382,7 @@ def make_community(
         categories_names=[make_category().name],
         invites_enabled=make_community_invites_enabled(),
         location=location,
+        topics_v2=topics_v2,
     )
     community.topics.set(topics)
     community.geolocation = geolocation
@@ -696,6 +744,10 @@ def make_insight():
     return insight
 
 
+def make_feed_post():
+    return mixer.blend(FeedPost)
+
+
 def make_feed_post_category():
     return mixer.blend(FeedPostCategory)
 
diff --git a/openbook_common/tests/test_skills.py b/openbook_common/tests/test_skills.py
new file mode 100644
index 0000000000000000000000000000000000000000..5ac7ee377594fe164e9683c942243062e834d892
--- /dev/null
+++ b/openbook_common/tests/test_skills.py
@@ -0,0 +1,176 @@
+import logging
+import unittest
+
+from openbook_common.skills import skill_v2_slugs, slug_to_skill_v2, slugs_to_skill_v2_slugs
+
+logger = logging.getLogger(__name__)
+
+
+class TestSlugToSkillV2(unittest.TestCase):
+    # Define test cases as a list of tuples (input, expected)
+    test_cases = [
+        # Music Performance
+        ("music-performance", "music-performance"),
+        # Childcare
+        ("childcare", "childcare"),
+        # Sales & Distribution
+        ("sales-distribution", "sales-distribution"),
+        # Emergency Care
+        ("emergency-care", "emergency-care"),
+        # Animal Care
+        ("animal-care", "animal-care"),
+        # Religious & Spiritual Care
+        ("religious-spiritual-care", "religious-spiritual-care"),
+        # Gardening & Conservation
+        ("gardening-conservation", "gardening-conservation"),
+        # Community Organizing
+        ("community-organizing", "community-organizing"),
+        # Event Planning
+        ("event-planning", "event-planning"),
+        # Arts & Crafts
+        ("arts-crafts", "arts-crafts"),
+        ("arts-music-culture", "arts-crafts"),
+        # Intercultural Competence
+        ("intercultural-competence", "intercultural-competence"),
+        ("inclusive-mindset-intercultural-competence", "intercultural-competence"),
+        # Driving
+        ("driving", "driving"),
+        ("drive-transport", "driving"),
+        # Legal Assistance
+        ("legal-assistance", "legal-assistance"),
+        ("law-regulations", "legal-assistance"),
+        # Nursing & Caregiving
+        ("nursing-caregiving", "nursing-caregiving"),
+        ("caring-nursing", "nursing-caregiving"),
+        # Food Service
+        ("food-service", "food-service"),
+        ("cooking-baking", "food-service"),
+        # Translation
+        ("translation", "translation"),
+        ("writing-translation", "translation"),
+        # Administration
+        ("administration", "administration"),
+        ("organizational-skills", "administration"),
+        # Creativity & Design
+        ("creativity-design", "creativity-design"),
+        ("creativity", "creativity-design"),
+        ("design", "creativity-design"),
+        ("development-concept", "creativity-design"),
+        # Tutoring & Coaching
+        ("tutoring-coaching", "tutoring-coaching"),
+        ("teaching-education", "tutoring-coaching"),
+        ("coaching & mentoring", "tutoring-coaching"),
+        # Marketing & Communications
+        ("marketing-communications", "marketing-communications"),
+        ("marketing-social-media", "marketing-communications"),
+        ("communication-skills", "marketing-communications"),
+        ("public-relations", "marketing-communications"),
+        # Team Leadership
+        ("team-leadership", "team-leadership"),
+        ("coordination-coordination-of-volunteers", "team-leadership"),
+        ("leadership", "team-leadership"),
+        ("teamwork", "team-leadership"),
+        # Research & Analysis
+        ("research-analysis", "research-analysis"),
+        ("critical-thinking", "research-analysis"),
+        ("analytical-thinking", "research-analysis"),
+        ("knowledge-management", "research-analysis"),
+        # Other
+        ("other", "other"),
+        ("environmental-awareness", "other"),
+        ("commitment", "other"),
+        ("conflict-management", "other"),
+        ("discipline", "other"),
+        ("empathy", "other"),
+        ("hands-on-support", "other"),
+        ("holistic-thinking", "other"),
+        ("humour", "other"),
+        ("independence", "other"),
+        ("initiative-and-energy", "other"),
+        ("mindfulness", "other"),
+        ("motivation", "other"),
+        ("negotiation-skills", "other"),
+        ("openness", "other"),
+        ("relationship-skills", "other"),
+        ("resilience", "other"),
+        ("self-confident", "other"),
+        ("solution-orientedness", "other"),
+        ("willingness-to-learn", "other"),
+        ("adaptability", "other"),
+        ("visionary-thinking", "other"),
+        ("assertiveness", "other"),
+        # Fundraising
+        ("fundraising", "fundraising"),
+        # Moderation & Facilitation
+        ("moderation-facilitation", "moderation-facilitation"),
+        # Management & Strategy
+        ("management-strategy", "management-strategy"),
+        # IT & Technology
+        ("it-technology", "it-technology"),
+        # Finances & Taxes
+        ("finances-taxes", "finances-taxes"),
+        # Building & Handicraft
+        ("building-handicraft", "building-handicraft"),
+        # Edge cases
+        ("", None),
+        ("unknown-slug", None),
+    ]
+
+    def test_slug_to_skill_v2_mapping(self):
+        """Test that each slug maps to its expected skill."""
+        for input_slug, expected in self.test_cases:
+            assert slug_to_skill_v2(input_slug) == expected
+
+    def test_all_implemented_slugs_are_tested(self):
+        """Test that all slugs in the implementation have a corresponding test case."""
+        all_defined_slugs = [slug for slugs in skill_v2_slugs.values() for slug in slugs]
+        tested_slugs = [test_case[0] for test_case in self.test_cases]
+
+        for slug in all_defined_slugs:
+            assert slug in tested_slugs, f"Slug '{slug}' is missing a test case"
+
+
+class TestSlugsToSkillV2Slugs(unittest.TestCase):
+    def test_skill_v1_slugs_mapping(self):
+        """Test that a list of SkillV1 slugs is mapped to a list of SkillV2 slugs"""
+        input_slugs = ["law-regulations", "empathy", "it-technology", "discipline"]
+        expected = ["legal-assistance", "other", "it-technology"]
+
+        actual = slugs_to_skill_v2_slugs(input_slugs)
+
+        assert len(actual) == len(expected), "Invalid number of slugs"
+        for slug in expected:
+            assert slug in actual, f"Slug '{slug}' is missing"
+
+    def test_skill_v2_slugs_mapping(self):
+        """Test that a list of SkillV2 slugs is mapped to a distinct list of SkillV2 slugs"""
+        input_slugs = ["animal-care", "other", "it-technology", "other"]
+        expected = ["animal-care", "other", "it-technology"]
+
+        actual = slugs_to_skill_v2_slugs(input_slugs)
+
+        assert len(actual) == len(expected), "Invalid number of slugs"
+        for slug in expected:
+            assert slug in actual, f"Slug '{slug}' is missing"
+
+    def test_mixed_version_slugs_mapping(self):
+        """Test that a list of mixed version slugs is mapped to a list of SkillV2 slugs"""
+        input_slugs = ["law-regulations", "empathy", "it-technology", "animal-care"]
+        expected = ["legal-assistance", "other", "it-technology", "animal-care"]
+
+        actual = slugs_to_skill_v2_slugs(input_slugs)
+
+        assert len(actual) == len(expected), "Invalid number of slugs"
+        for slug in expected:
+            assert slug in actual, f"Slug '{slug}' is missing"
+
+    def test_unknown_slug_mapping(self):
+        """Test that unknown or empty slugs are ignored when mapping to a list of SkillV2 slugs"""
+        input_slugs = ["law-regulations", "", "it-technology", "unknown"]
+        expected = ["legal-assistance", "it-technology"]
+
+        actual = slugs_to_skill_v2_slugs(input_slugs)
+
+        assert len(actual) == len(expected), "Invalid number of slugs"
+        for slug in expected:
+            assert slug in actual, f"Slug '{slug}' is missing"
diff --git a/openbook_common/tests/test_topics.py b/openbook_common/tests/test_topics.py
new file mode 100644
index 0000000000000000000000000000000000000000..e5340b52a3e063968a68725886dfce3d2cf24748
--- /dev/null
+++ b/openbook_common/tests/test_topics.py
@@ -0,0 +1,145 @@
+import unittest
+
+from openbook_common.topics import topic_v2_slugs, slug_to_topic_v2, slugs_to_topic_v2_slugs
+
+
+class TestSlugToTopicV2(unittest.TestCase):
+    # Define test cases as a list of tuples (input, expected)
+    test_cases = [
+        # ANIMAL_WELFARE
+        ("animal-welfare", "animal-welfare"),
+        ("biodiversity-animal-welfare", "animal-welfare"),
+        # ARTS_CULTURE
+        ("arts-culture", "arts-culture"),
+        ("culture-social-change", "arts-culture"),
+        # CLIMATE_ENVIRONMENT
+        ("climate-environment", "climate-environment"),
+        ("climate-protection", "climate-environment"),
+        ("water-ocean", "climate-environment"),
+        ("energy-transition", "climate-environment"),
+        ("natural-resources", "climate-environment"),
+        # DEMOCRACY_POLITICS
+        ("democracy-politics", "democracy-politics"),
+        ("governance", "democracy-politics"),
+        ("politics-society", "democracy-politics"),
+        # DISABILITY_INCLUSION
+        ("disability-inclusion", "disability-inclusion"),
+        ("disabilities-inclusion", "disability-inclusion"),
+        # DISASTER_RELIEF
+        ("disaster-relief", "disaster-relief"),
+        ("natural-disaster-response", "disaster-relief"),
+        # ELDERLY_CARE
+        ("elderly-care", "elderly-care"),
+        ("elderly-people", "elderly-care"),
+        # FAMILY_KIDS
+        ("family-kids", "family-kids"),
+        # FOOD_HUNGER_RELIEF
+        ("food-hunger-relief", "food-hunger-relief"),
+        ("agriculture-food", "food-hunger-relief"),
+        # GENDER_EQUALITY
+        ("gender-equality", "gender-equality"),
+        ("diversity-gender-equality", "gender-equality"),
+        # HOUSING
+        ("housing", "housing"),
+        ("building-housing", "housing"),
+        # HUMAN_RIGHTS
+        ("human-rights", "human-rights"),
+        ("democracy-human-rights", "human-rights"),
+        # LGBTQIA_PLUS
+        ("lgbtqia-plus", "lgbtqia-plus"),
+        # LIFESTYLE_CONSUMPTION
+        ("lifestyle-consumption", "lifestyle-consumption"),
+        ("consumption-lifestyle", "lifestyle-consumption"),
+        ("eco-tourism", "lifestyle-consumption"),
+        # LITERACY_EDUCATION
+        ("literacy-education", "literacy-education"),
+        ("education-science", "literacy-education"),
+        # MENTAL_PHYSICAL_HEALTH
+        ("mental-physical-health", "mental-physical-health"),
+        ("physical-mental-health", "mental-physical-health"),
+        # MIGRATION_REFUGEES
+        ("migration-refugees", "migration-refugees"),
+        ("migration-integration", "migration-refugees"),
+        # MOBILITY_TRANSPORT
+        ("mobility-transport", "mobility-transport"),
+        ("mobility-infrastructure", "mobility-transport"),
+        # OTHER
+        ("other", "other"),
+        ("alternative-economy", "other"),
+        ("urban-rural-development", "other"),
+        # PEACE_SECURITY
+        ("peace-security", "peace-security"),
+        # POVERTY_HOMELESSNESS
+        ("poverty-homelessness", "poverty-homelessness"),
+        ("poverty-homelesness", "poverty-homelessness"),
+        # SPORTS_RECREATION
+        ("sports-recreation", "sports-recreation"),
+        # TECHNOLOGY_INNOVATION
+        ("technology-innovation", "technology-innovation"),
+        ("technological-innovation", "technology-innovation"),
+        # YOUTH_DEVELOPMENT
+        ("youth-development", "youth-development"),
+        ("children-youth", "youth-development"),
+        # Edge cases
+        ("", None),
+        ("unknown-slug", None),
+    ]
+
+    def test_slug_to_topic_v2_mapping(self):
+        """Test that each slug maps to its expected topic."""
+        for input_slug, expected in self.test_cases:
+            assert slug_to_topic_v2(input_slug) == expected
+
+    def test_all_implemented_slugs_are_tested(self):
+        """Test that all slugs in the implementation have a corresponding test case."""
+        all_slugs = [slug for slugs in topic_v2_slugs.values() for slug in slugs]
+        tested_slugs = [test_case[0] for test_case in self.test_cases]
+
+        for slug in all_slugs:
+            assert slug in tested_slugs, f"Slug '{slug}' is missing a test case"
+
+
+class TestSlugsToTopicV2Slugs(unittest.TestCase):
+    def test_topic_v1_slugs_mapping(self):
+        """Test that a list of TopicV1 slugs is mapped to a list of TopicV2 slugs"""
+        input_slugs = ["water-ocean", "alternative-economy", "peace-security", "urban-rural-development"]
+        expected = ["climate-environment", "other", "peace-security"]
+
+        actual = slugs_to_topic_v2_slugs(input_slugs)
+
+        assert len(actual) == len(expected), "Invalid number of slugs"
+        for slug in expected:
+            assert slug in actual, f"Slug '{slug}' is missing"
+
+    def test_topic_v2_slugs_mapping(self):
+        """Test that a list of TopicV2 slugs is mapped to a distinct list of TopicV2 slugs"""
+        input_slugs = ["sports-recreation", "other", "peace-security", "other"]
+        expected = ["sports-recreation", "other", "peace-security"]
+
+        actual = slugs_to_topic_v2_slugs(input_slugs)
+
+        assert len(actual) == len(expected), "Invalid number of slugs"
+        for slug in expected:
+            assert slug in actual, f"Slug '{slug}' is missing"
+
+    def test_mixed_version_slugs_mapping(self):
+        """Test that a list of mixed version slugs is mapped to a list of TopicV2 slugs"""
+        input_slugs = ["water-ocean", "alternative-economy", "peace-security", "sports-recreation"]
+        expected = ["climate-environment", "other", "peace-security", "sports-recreation"]
+
+        actual = slugs_to_topic_v2_slugs(input_slugs)
+
+        assert len(actual) == len(expected), "Invalid number of slugs"
+        for slug in expected:
+            assert slug in actual, f"Slug '{slug}' is missing"
+
+    def test_unknown_slug_mapping(self):
+        """Test that unknown or empty slugs are ignored when mapping to a list of TopicV2 slugs"""
+        input_slugs = ["water-ocean", "", "peace-security", "unknown"]
+        expected = ["climate-environment", "peace-security"]
+
+        actual = slugs_to_topic_v2_slugs(input_slugs)
+
+        assert len(actual) == len(expected), "Invalid number of slugs"
+        for slug in expected:
+            assert slug in actual, f"Slug '{slug}' is missing"
diff --git a/openbook_common/tests/test_validators.py b/openbook_common/tests/test_validators.py
new file mode 100644
index 0000000000000000000000000000000000000000..c67b15b2dfa1d7edaa7ecc2e87145067067a27ab
--- /dev/null
+++ b/openbook_common/tests/test_validators.py
@@ -0,0 +1,58 @@
+import unittest
+from rest_framework.exceptions import NotFound
+
+from openbook_common.topics import topic_v2_slugs
+from openbook_common.skills import skill_v2_slugs
+from openbook_common.validators import topic_v2_exists, skill_v2_exists
+
+
+class TestTopicV2Validation(unittest.TestCase):
+    def test_validation_succeeds_for_existing_slugs(self):
+        all_v2_slugs = topic_v2_slugs.keys()
+
+        for slug in all_v2_slugs:
+            topic_v2_exists(slug)
+
+    def test_validation_fails_for_v1_slugs(self):
+        all_v2_slugs = topic_v2_slugs.keys()
+        all_v1_slugs = filter(
+            lambda slug: slug not in all_v2_slugs, [slug for slugs in topic_v2_slugs.values() for slug in slugs]
+        )
+
+        for slug in all_v1_slugs:
+            with self.assertRaises(NotFound):
+                topic_v2_exists(slug)
+
+    def test_validation_fails_for_unknown_slug(self):
+        with self.assertRaises(NotFound):
+            topic_v2_exists("unknown")
+
+    def test_validation_fails_for_empty_slug(self):
+        with self.assertRaises(NotFound):
+            topic_v2_exists("")
+
+
+class TestSkillV2Validation(unittest.TestCase):
+    def test_validation_succeeds_for_existing_slugs(self):
+        all_v2_slugs = skill_v2_slugs.keys()
+
+        for slug in all_v2_slugs:
+            skill_v2_exists(slug)
+
+    def test_validation_fails_for_v1_slugs(self):
+        all_v2_slugs = skill_v2_slugs.keys()
+        all_v1_slugs = filter(
+            lambda slug: slug not in all_v2_slugs, [slug for slugs in skill_v2_slugs.values() for slug in slugs]
+        )
+
+        for slug in all_v1_slugs:
+            with self.assertRaises(NotFound):
+                skill_v2_exists(slug)
+
+    def test_validation_fails_for_unknown_slug(self):
+        with self.assertRaises(NotFound):
+            skill_v2_exists("unknown")
+
+    def test_validation_fails_for_empty_slug(self):
+        with self.assertRaises(NotFound):
+            skill_v2_exists("")
diff --git a/openbook_common/topics.py b/openbook_common/topics.py
new file mode 100644
index 0000000000000000000000000000000000000000..55d6a8b07ab8c64e129c028ef754041873f6e38a
--- /dev/null
+++ b/openbook_common/topics.py
@@ -0,0 +1,81 @@
+from typing import Optional, Literal, List
+
+# keys are the V2 topic slugs, values is a combined list of V1 and V2 topic slugs (for backwards-compatibility)
+topic_v2_slugs = {
+    "animal-welfare": ["animal-welfare", "biodiversity-animal-welfare"],
+    "arts-culture": ["arts-culture", "culture-social-change"],
+    "climate-environment": [
+        "climate-environment",
+        "climate-protection",
+        "water-ocean",
+        "energy-transition",
+        "natural-resources",
+    ],
+    "democracy-politics": ["democracy-politics", "governance", "politics-society"],
+    "disability-inclusion": ["disability-inclusion", "disabilities-inclusion"],
+    "disaster-relief": ["disaster-relief", "natural-disaster-response"],
+    "elderly-care": ["elderly-care", "elderly-people"],
+    "family-kids": ["family-kids"],
+    "food-hunger-relief": ["food-hunger-relief", "agriculture-food"],
+    "gender-equality": ["gender-equality", "diversity-gender-equality"],
+    "housing": ["housing", "building-housing"],
+    "human-rights": ["human-rights", "democracy-human-rights"],
+    "lgbtqia-plus": ["lgbtqia-plus"],
+    "lifestyle-consumption": ["lifestyle-consumption", "consumption-lifestyle", "eco-tourism"],
+    "literacy-education": ["literacy-education", "education-science"],
+    "mental-physical-health": ["mental-physical-health", "physical-mental-health"],
+    "migration-refugees": ["migration-refugees", "migration-integration"],
+    "mobility-transport": ["mobility-transport", "mobility-infrastructure"],
+    "other": ["other", "alternative-economy", "urban-rural-development"],
+    "peace-security": ["peace-security"],
+    "poverty-homelessness": ["poverty-homelessness", "poverty-homelesness"],
+    "sports-recreation": ["sports-recreation"],
+    "technology-innovation": ["technology-innovation", "technological-innovation"],
+    "youth-development": ["youth-development", "children-youth"],
+}
+
+TopicV2 = Literal[
+    "animal-welfare",
+    "arts-culture",
+    "climate-environment",
+    "democracy-politics",
+    "disability-inclusion",
+    "disaster-relief",
+    "elderly-care",
+    "family-kids",
+    "food-hunger-relief",
+    "gender-equality",
+    "housing",
+    "human-rights",
+    "lgbtqia-plus",
+    "lifestyle-consumption",
+    "literacy-education",
+    "mental-physical-health",
+    "migration-refugees",
+    "mobility-transport",
+    "other",
+    "peace-security",
+    "poverty-homelessness",
+    "sports-recreation",
+    "technology-innovation",
+    "youth-development",
+]
+
+
+def slug_to_topic_v2(slug: str) -> Optional[str]:
+    """
+    Convert a slug to its corresponding V2 topic.
+    Returns None if no match is found.
+    """
+    if slug in topic_v2_slugs:
+        return slug
+
+    for topic, slugs in topic_v2_slugs.items():
+        if slug in slugs:
+            return topic
+
+    return None
+
+
+def slugs_to_topic_v2_slugs(slugs: List[str]) -> List[str]:
+    return list(set(filter(None, map(slug_to_topic_v2, slugs))))
diff --git a/openbook_common/validators.py b/openbook_common/validators.py
index 160da61bff027705ddbb9002daf80b0d2cb7d1ee..53016a33b0fe744321922844bdeb84e370c7ebaa 100644
--- a/openbook_common/validators.py
+++ b/openbook_common/validators.py
@@ -1,8 +1,8 @@
-# ruff: noqa
 import re
+from typing import get_args
 
 from graphql import GraphQLError
-from rest_framework.exceptions import ValidationError
+from rest_framework.exceptions import ValidationError, NotFound
 from django.utils.translation import gettext_lazy as _
 
 from openbook.graphql_errors import GraphQLErrorCodes
@@ -13,6 +13,8 @@ from openbook_common.utils.model_loaders import (
     get_emoji_group_model,
     get_language_model,
 )
+from openbook_common.topics import TopicV2
+from openbook_common.skills import SkillV2
 
 from openbook import settings
 
@@ -50,7 +52,7 @@ def reaction_type_exists(reaction_type):
     except KeyError:
         raise ValidationError(
             _("No reaction_type with the provided name exists."),
-        )
+        ) from None
 
 
 def emoji_group_id_exists(emoji_group_id):
@@ -118,3 +120,13 @@ def sanitize_text(text: str):
     urls = extract_urls_from_string(text)
     for url in urls:
         url_safety_validator(url)
+
+
+def topic_v2_exists(topic_v2_slug):
+    if topic_v2_slug not in get_args(TopicV2):
+        raise NotFound(_(f"TopicV2 '{topic_v2_slug}' does not exist."))
+
+
+def skill_v2_exists(skill_v2_slug):
+    if skill_v2_slug not in get_args(SkillV2):
+        raise NotFound(_(f"SkillV2 '{skill_v2_slug}' does not exist."))
diff --git a/openbook_communities/migrations/0043_community_topics_v2_task_skills_v2.py b/openbook_communities/migrations/0043_community_topics_v2_task_skills_v2.py
new file mode 100644
index 0000000000000000000000000000000000000000..b41c038033a6df0820cfd86b02a459b22e14632e
--- /dev/null
+++ b/openbook_communities/migrations/0043_community_topics_v2_task_skills_v2.py
@@ -0,0 +1,53 @@
+# Generated by Django 5.0.12 on 2025-02-27 14:37
+
+import django.contrib.postgres.fields
+from django.db import migrations, models
+
+from openbook_terms.helpers import topics_to_topic_v2_slugs, skills_to_skill_v2_slugs
+
+
+def migrate_task_skills(apps, schema_editor):
+    Task = apps.get_model("openbook_communities", "Task")
+
+    for task in Task.objects.all():
+        old_skills = task.skills.all()
+        new_skills = skills_to_skill_v2_slugs(old_skills)
+
+        task.skills_v2 = new_skills
+        task.save()
+
+
+def migrate_space_topics(apps, schema_editor):
+    Community = apps.get_model("openbook_communities", "Community")
+
+    for space in Community.objects.all():
+        old_topics = space.topics.all()
+        new_topics = topics_to_topic_v2_slugs(old_topics)
+
+        space.topics_v2 = new_topics
+        space.save()
+
+
+class Migration(migrations.Migration):
+    dependencies = [
+        ("openbook_communities", "0042_alter_collaborationtool_thumbnail"),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name="community",
+            name="topics_v2",
+            field=django.contrib.postgres.fields.ArrayField(
+                base_field=models.CharField(max_length=128), blank=True, default=list, size=None
+            ),
+        ),
+        migrations.AddField(
+            model_name="task",
+            name="skills_v2",
+            field=django.contrib.postgres.fields.ArrayField(
+                base_field=models.CharField(max_length=128), blank=True, default=list, size=None
+            ),
+        ),
+        migrations.RunPython(migrate_space_topics),
+        migrations.RunPython(migrate_task_skills),
+    ]
diff --git a/openbook_communities/migrations/0044_community_openbook_co_topics__765075_gin.py b/openbook_communities/migrations/0044_community_openbook_co_topics__765075_gin.py
new file mode 100644
index 0000000000000000000000000000000000000000..1f3de593e8a046c066ea0e2f03b1669cb3967c35
--- /dev/null
+++ b/openbook_communities/migrations/0044_community_openbook_co_topics__765075_gin.py
@@ -0,0 +1,23 @@
+# Generated by Django 5.0.12 on 2025-02-28 15:50
+
+import django.contrib.postgres.indexes
+from django.conf import settings
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ("openbook_communities", "0043_community_topics_v2_task_skills_v2"),
+        ("openbook_terms", "0010_delete_inspirationpostcategory"),
+        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+    ]
+
+    operations = [
+        migrations.AddIndex(
+            model_name="community",
+            index=django.contrib.postgres.indexes.GinIndex(
+                fields=["topics_v2"], name="openbook_co_topics__765075_gin"
+            ),
+        ),
+    ]
diff --git a/openbook_communities/migrations/0045_task_openbook_co_skills__85edfe_gin.py b/openbook_communities/migrations/0045_task_openbook_co_skills__85edfe_gin.py
new file mode 100644
index 0000000000000000000000000000000000000000..912844588b358ba6f2e532e79175f16ff2281719
--- /dev/null
+++ b/openbook_communities/migrations/0045_task_openbook_co_skills__85edfe_gin.py
@@ -0,0 +1,23 @@
+# Generated by Django 5.0.12 on 2025-02-28 15:54
+
+import django.contrib.postgres.indexes
+from django.conf import settings
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ("openbook_communities", "0044_community_openbook_co_topics__765075_gin"),
+        ("openbook_terms", "0010_delete_inspirationpostcategory"),
+        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+    ]
+
+    operations = [
+        migrations.AddIndex(
+            model_name="task",
+            index=django.contrib.postgres.indexes.GinIndex(
+                fields=["skills_v2"], name="openbook_co_skills__85edfe_gin"
+            ),
+        ),
+    ]
diff --git a/openbook_communities/migrations/tests/__init__.py b/openbook_communities/migrations/tests/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/openbook_communities/migrations/tests/test_0043_community_topics_v2_task_skills_v2.py b/openbook_communities/migrations/tests/test_0043_community_topics_v2_task_skills_v2.py
new file mode 100644
index 0000000000000000000000000000000000000000..e3c19fe2f31546fb33b90d22634e26ecd294a629
--- /dev/null
+++ b/openbook_communities/migrations/tests/test_0043_community_topics_v2_task_skills_v2.py
@@ -0,0 +1,38 @@
+from importlib import import_module
+import pytest
+from django.apps import apps
+from django.db import connection
+from openbook_communities.models import Community, Task
+from openbook_common.tests.helpers import make_community, make_community_task, make_topic, make_skill
+
+# Required because of file name starting with a number
+migration = import_module("openbook_communities.migrations.0043_community_topics_v2_task_skills_v2")
+
+
+@pytest.mark.django_db
+class TestMigrateSpaceTopicsAndTaskSkills:
+    def test_migrate_space_topics(self):
+        # GIVEN a space with only V1 topics
+        topics_v1 = [make_topic(slug="eco-tourism"), make_topic(slug="elderly-people")]
+        space = make_community(topics=[], topics_v2=[])
+        space.topics.set(topics_v1)
+
+        # WHEN migrating space topics from V1 to V2
+        migration.migrate_space_topics(apps, connection.schema_editor())
+
+        # THEN the space has mapped V2 topics
+        updated_space = Community.objects.get(id=space.id)
+        assert sorted(updated_space.topics_v2) == sorted(["lifestyle-consumption", "elderly-care"])
+
+    def test_migrate_task_skills(self):
+        # GIVEN a task with only V1 skills
+        skills_v1 = [make_skill(slug="writing-translation"), make_skill(slug="assertiveness")]
+        task = make_community_task(community=make_community(), skills=[], skills_v2=[])
+        task.skills.set(skills_v1)
+
+        # WHEN migrating task skills from V1 to V2
+        migration.migrate_task_skills(apps, connection.schema_editor())
+
+        # THEN the task has mapped V2 skills
+        updated_task = Task.objects.get(id=task.id)
+        assert sorted(updated_task.skills_v2) == sorted(["translation", "other"])
diff --git a/openbook_communities/models/__init__.py b/openbook_communities/models/__init__.py
index 1806bcce0390adc4db7bbe5af42452278b22cce9..f3cb162bb76b041624a8e9e17f90cfab31d118e3 100644
--- a/openbook_communities/models/__init__.py
+++ b/openbook_communities/models/__init__.py
@@ -10,6 +10,7 @@ from django.contrib.contenttypes.fields import GenericRelation
 from django.contrib.gis.db import models
 from django.contrib.gis.db.models import Count
 from django.contrib.gis.db.models import Q
+from django.contrib.postgres.indexes import GinIndex
 from django.core.files.storage import default_storage
 from django.db import transaction
 from django.db.models import QuerySet
@@ -53,6 +54,8 @@ from openbook_communities.queries import (
 from openbook_moderation.models import ModeratedObject
 from openbook_notifications.api import NotificationApi
 from openbook_terms.models import SDG, Topic, Skill
+from openbook_terms.helpers import topics_to_topic_v2_slugs, skills_to_skill_v2_slugs
+from django.contrib.postgres.fields import ArrayField
 
 community_image_storage = default_storage
 community_task_image_storage = default_storage
@@ -187,6 +190,7 @@ class Community(ModelWithUUID):
         db_index=True,
     )
     topics = models.ManyToManyField(Topic, related_name="communities", blank=False, db_index=True)
+    topics_v2 = ArrayField(models.CharField(max_length=settings.TERM_SLUG_MAX_LENGTH), blank=True, default=list)
     sdgs = models.ManyToManyField(SDG, related_name="communities", blank=True, db_index=True)
     geolocation = models.PointField(blank=True, null=True, spatial_index=True)
     contact_description = models.CharField(
@@ -210,6 +214,7 @@ class Community(ModelWithUUID):
 
     class Meta:
         verbose_name_plural = "communities"
+        indexes = [GinIndex(fields=["topics_v2"])]
 
     @property
     def avatar_default_color(self):
@@ -352,6 +357,7 @@ class Community(ModelWithUUID):
         invites_enabled=None,
         location=None,
         topic_ids=None,
+        topics_v2=None,
     ):
         # If it's a private community and no invites_enabled
         if type == Community.COMMUNITY_TYPE_PRIVATE and invites_enabled is None:
@@ -360,6 +366,9 @@ class Community(ModelWithUUID):
             # The default for this field is not working when passed None?
             invites_enabled = True
 
+        if topics_v2 is None:
+            topics_v2 = []
+
         community = cls.objects.create(
             title=title,
             creator=creator,
@@ -375,6 +384,7 @@ class Community(ModelWithUUID):
             rules=rules,
             invites_enabled=invites_enabled,
             location=location,
+            topics_v2=topics_v2,
         )
 
         CommunityMembership.create_membership(
@@ -545,6 +555,7 @@ class Community(ModelWithUUID):
         categories_names=None,
         invites_enabled=None,
         topic_ids=None,
+        topics_v2=None,
         sdg_ids=None,
         cover=None,
         remove_cover=False,
@@ -584,6 +595,9 @@ class Community(ModelWithUUID):
         if categories_names is not None:
             self.set_categories_with_names(categories_names=categories_names)
 
+        if topics_v2 is not None:
+            self.topics_v2 = topics_v2
+
         if topic_ids is not None:
             self.set_topics_with_ids(topic_ids=topic_ids)
 
@@ -679,14 +693,12 @@ class Community(ModelWithUUID):
         self.categories.clear()
 
     def set_topics_with_ids(self, topic_ids):
-        self.clear_topics()
+        self.topics.clear()
         Topic = get_topic_model()
         topics = Topic.objects.filter(id__in=topic_ids)
+        self.topics_v2 = topics_to_topic_v2_slugs(topics)
         self.topics.set(topics)
 
-    def clear_topics(self):
-        self.topics.clear()
-
     def set_sdgs_with_ids(self, sdg_ids):
         self.clear_sdgs()
         Sdg = get_sdg_model()
@@ -1272,6 +1284,7 @@ class Task(ModelWithUUID):
         choices=Regularity.choices,
     )
     skills = models.ManyToManyField(Skill, related_name="tasks", blank=True, db_index=True)
+    skills_v2 = ArrayField(models.CharField(max_length=settings.TERM_SLUG_MAX_LENGTH), blank=True, default=list)
     thumbnail = ProcessedImageField(
         storage=community_task_image_storage,
         verbose_name=_("thumbnail"),
@@ -1302,6 +1315,9 @@ class Task(ModelWithUUID):
 
     deleted_at = models.DateTimeField(blank=True, null=True, default=None)
 
+    class Meta:
+        indexes = [GinIndex(fields=["skills_v2"])]
+
     @property
     def is_deleted(self) -> bool:
         return self.deleted_at is not None
@@ -1321,13 +1337,11 @@ class Task(ModelWithUUID):
             & (pQ(visibility=VisibilityType.PUBLIC) | pQ(community__memberships__user__id=user.id))
         )
 
-    def clear_skills(self):
-        self.skills.clear()
-
     def set_skills_with_ids(self, skill_ids):
-        self.clear_skills()
+        self.skills.clear()
         Skill = get_skill_model()
         skills = Skill.objects.filter(id__in=skill_ids)
+        self.skills_v2 = skills_to_skill_v2_slugs(skills)
         self.skills.set(skills)
 
     @classmethod
@@ -1346,8 +1360,12 @@ class Task(ModelWithUUID):
         regularity=None,
         contact_phone=None,
         skills=None,
+        skills_v2=None,
         thumbnail=None,
     ):
+        if skills_v2 is None:
+            skills_v2 = []
+
         task = cls.objects.create(
             creator=creator,
             community=community,
@@ -1363,6 +1381,7 @@ class Task(ModelWithUUID):
             contact_phone=contact_phone,
             thumbnail=thumbnail,
             thumbnail_blurhash=generate_blurhash(thumbnail),
+            skills_v2=skills_v2,
         )
 
         if skills:
@@ -1383,7 +1402,8 @@ class Task(ModelWithUUID):
         geolocation=None,
         regularity=None,
         contact_phone=None,
-        skills=None,
+        skill_ids=None,
+        skills_v2=None,
         thumbnail=None,
         remove_thumbnail=False,
     ):
@@ -1401,8 +1421,13 @@ class Task(ModelWithUUID):
             self.geolocation = geolocation
         if contact_phone is not None:
             self.contact_phone = contact_phone
-        if skills is not None:
-            self.skills.set(skills)
+        if skills_v2 is not None:
+            self.skills_v2 = skills_v2
+        if skill_ids is not None:
+            Skill = get_skill_model()
+            updated_skills = Skill.objects.filter(id__in=skill_ids)
+            self.skills_v2 = skills_to_skill_v2_slugs(updated_skills)
+            self.skills.set(updated_skills)
 
         if remove_thumbnail:
             self._delete_thumbnail(save=False)
diff --git a/openbook_communities/schema/helpers.py b/openbook_communities/schema/helpers.py
index 2c23961edff0b3e64ed74fb36eac6d89b068e6ec..7b01c6b44260800bedce04d94305255163925aed 100644
--- a/openbook_communities/schema/helpers.py
+++ b/openbook_communities/schema/helpers.py
@@ -9,6 +9,17 @@ from openbook_common.types import GeolocationFeature
 from openbook_communities.models import Community as CommunityModel
 
 
+DISCOVER_DEFAULT_SPACES_V2 = [
+    "human-rights",
+    "animal-welfare",
+    "climate-environment",
+    "arts-culture",
+    "literacy-education",
+    "mental-physical-health",
+    "democracy-politics",
+]
+
+
 @sync_to_async
 def filtered_spaces(
     topic_id: Optional[uuid.UUID] = None,
@@ -18,10 +29,21 @@ def filtered_spaces(
     topic_slug: Optional[str] = None,
     topic_slugs: Optional[List[Optional[str]]] = None,
     skill_slugs: Optional[List[Optional[str]]] = None,
+    topics_v2: Optional[List[str]] = None,
+    skills_v2: Optional[List[str]] = None,
     member_id: Optional[uuid.UUID] = None,
 ):
     query = create_filtered_spaces_query(
-        topic_id, topic_ids, geolocation, skill_ids, topic_slug, topic_slugs, skill_slugs, member_id
+        topic_id,
+        topic_ids,
+        geolocation,
+        skill_ids,
+        topic_slug,
+        topic_slugs,
+        skill_slugs,
+        topics_v2,
+        skills_v2,
+        member_id,
     )
     return create_spaces_queryset(query)
 
@@ -34,6 +56,8 @@ def create_filtered_spaces_query(
     topic_slug: Optional[str] = None,
     topic_slugs: Optional[List[Optional[str]]] = None,
     skill_slugs: Optional[List[Optional[str]]] = None,
+    topics_v2: Optional[List[str]] = None,
+    skills_v2: Optional[List[str]] = None,
     member_id: Optional[uuid.UUID] = None,
 ):
     query = Q()
@@ -55,6 +79,10 @@ def create_filtered_spaces_query(
         query.add(Q(topics__slug__contains=topic_slug), Q.AND)
     if skill_slugs:
         query.add(Q(tasks__skills__slug__in=skill_slugs), Q.AND)
+    if topics_v2:
+        query.add(Q(topics_v2__overlap=topics_v2), Q.AND)
+    if skills_v2:
+        query.add(Q(tasks__skills_v2__overlap=skills_v2), Q.AND)
     if member_id:
         user = UserModel.objects.get(username=member_id)
         query.add(Q(memberships__user=user), Q.AND)
diff --git a/openbook_communities/schema/mutations.py b/openbook_communities/schema/mutations.py
index 2c1be6a8b289ccbeda9c6125df4dc4d0d8e07f26..c71dafd6dc8c36bcaa3d1d3fb269d4f9d40f9d31 100644
--- a/openbook_communities/schema/mutations.py
+++ b/openbook_communities/schema/mutations.py
@@ -177,6 +177,15 @@ class Mutation:
         serializer = CreateCommunitySerializer(data=to_input_dict(input))
         serializer.is_valid(raise_exception=True)
         data = serializer.validated_data
+        if not data.get("topics") and not data.get("topics_v2"):
+            raise GraphQLError(
+                "Either topics or topics_v2 must be specified",
+                extensions={
+                    "code": GraphQLErrorCodes.BAD_USER_INPUT,
+                    "errors": [],
+                },
+            ) from None
+
         with transaction.atomic():
             space = current_user.create_community(
                 title=data.get("title"),
@@ -185,6 +194,7 @@ class Mutation:
                 cover=data.get("cover"),
                 invites_enabled=True,
                 topic_ids=data.get("topics"),
+                topics_v2=data.get("topics_v2"),
             )
 
         publish_space_created_event(community=space)
@@ -207,6 +217,7 @@ class Mutation:
             title=data.get("title"),
             description=data.get("description"),
             topic_ids=data.get("topics"),
+            topics_v2=data.get("topics_v2"),
             cover=data.get("cover"),
             remove_cover=is_parameter_null(data, "cover"),
             avatar=data.get("avatar"),
@@ -288,6 +299,7 @@ class Mutation:
                 contact_email=data.get("contact_email"),
                 contact_phone=data.get("contact_phone"),
                 skills=data.get("skills"),
+                skills_v2=data.get("skills_v2"),
                 thumbnail=data.get("thumbnail"),
             )
 
@@ -323,7 +335,8 @@ class Mutation:
                 regularity=data.get("regularity"),
                 contact_email=data.get("contact_email"),
                 contact_phone=data.get("contact_phone"),
-                skills=data.get("skills"),
+                skill_ids=data.get("skills"),
+                skills_v2=data.get("skills_v2"),
                 thumbnail=data.get("thumbnail"),
                 remove_thumbnail=is_parameter_null(data, "thumbnail"),
             )
diff --git a/openbook_communities/schema/queries.py b/openbook_communities/schema/queries.py
index 0c9965532924a239e6fc16d364e7b951103a222f..6e9a22a1277e2cff825bcb4b9ff6bb64c16b9f21 100644
--- a/openbook_communities/schema/queries.py
+++ b/openbook_communities/schema/queries.py
@@ -5,7 +5,7 @@ import strawberry
 import strawberry_django
 from asgiref.sync import async_to_sync
 from django.core.exceptions import PermissionDenied
-from django.db.models import Count, Prefetch, F, Q, Case, When, Value, IntegerField, OuterRef, Exists
+from django.db.models import Count, Prefetch, F, Q, Case, When, Value, IntegerField
 from django.db.models.lookups import IContains
 from django.utils.translation import gettext_lazy as _
 from strawberry.django.context import StrawberryDjangoContext
@@ -20,10 +20,10 @@ from openbook_communities.models import CollaborationTool as CollaborationToolMo
 from openbook_communities.models import Community as CommunityModel
 from openbook_communities.models import SpaceUserConnectionType
 from openbook_communities.models import Task as TaskModel
-from openbook_communities.models import Topic as TopicModel
 from openbook_communities.schema.helpers import (
     filtered_spaces,
     create_filtered_spaces_query,
+    DISCOVER_DEFAULT_SPACES_V2,
 )
 from openbook_communities.schema.types import (
     CollaborationTool,
@@ -32,17 +32,25 @@ from openbook_communities.schema.types import (
     Space,
     Task,
 )
-from openbook_terms.models import Skill as SkillModel
 
 Info: TypeAlias = StrawberryInfo[StrawberryDjangoContext, Any]
 
 
 @strawberry.type
 class Query:
+    @strawberry_django.field()
+    def discover_spaces_default_topics_v2(self) -> List[str]:
+        return DISCOVER_DEFAULT_SPACES_V2
+
     @strawberry_django.field()
     def spaces(
         self,
-        topic_id: Optional[uuid.UUID] = None,
+        topic_id: Annotated[
+            Optional[uuid.UUID],
+            strawberry.argument(
+                description="Deprecated since version 1.52, use topics_v2 instead",
+            ),
+        ] = None,
         location: Annotated[
             Optional[str],
             strawberry.argument(
@@ -51,10 +59,32 @@ class Query:
             ),
         ] = None,
         geolocation: Optional[GeoJSON] = None,
-        skill_ids: Optional[List[Optional[uuid.UUID]]] = None,
-        topic_slug: Optional[str] = None,
-        topic_slugs: Optional[List[Optional[str]]] = None,
-        skill_slugs: Optional[List[Optional[str]]] = None,
+        skill_ids: Annotated[
+            Optional[List[Optional[uuid.UUID]]],
+            strawberry.argument(
+                description="Deprecated since version 1.52, use skills_v2 instead",
+            ),
+        ] = None,
+        topic_slug: Annotated[
+            Optional[str],
+            strawberry.argument(
+                description="Deprecated since version 1.52, use topics_v2 instead",
+            ),
+        ] = None,
+        topic_slugs: Annotated[
+            Optional[List[Optional[str]]],
+            strawberry.argument(
+                description="Deprecated since version 1.52, use topics_v2 instead",
+            ),
+        ] = None,
+        skill_slugs: Annotated[
+            Optional[List[Optional[str]]],
+            strawberry.argument(
+                description="Deprecated since version 1.52, use skills_v2 instead",
+            ),
+        ] = None,
+        topics_v2: Optional[List[str]] = None,
+        skills_v2: Optional[List[str]] = None,
         member_id: Optional[uuid.UUID] = None,
         offset: int = 0,
         limit: int = 10,
@@ -66,6 +96,8 @@ class Query:
             topic_slug=topic_slug,
             topic_slugs=topic_slugs,
             skill_slugs=skill_slugs,
+            topics_v2=topics_v2,
+            skills_v2=skills_v2,
             member_id=member_id,
         )
 
@@ -83,20 +115,18 @@ class Query:
         whens = []
 
         if current_user and not current_user.is_anonymous:
-            topic_ids = list(current_user.profile.interests.values_list("id", flat=True).all())
+            topics_v2 = list(current_user.profile.interests_v2)
             geolocation = current_user.geolocation
 
-            if topic_ids:
+            if topics_v2:
                 whens.append(
-                    When(create_filtered_spaces_query(topic_ids=topic_ids, geolocation=geolocation), then=Value(10))
+                    When(create_filtered_spaces_query(topics_v2=topics_v2, geolocation=geolocation), then=Value(10))
                 )
 
             if geolocation:
                 whens.append(When(create_filtered_spaces_query(geolocation=geolocation), then=Value(20)))
 
-        default_topics = list(TopicModel.objects.filter(is_discover_spaces_default=True).values_list("id", flat=True))
-        if default_topics:
-            whens.append(When(create_filtered_spaces_query(topic_ids=default_topics), then=Value(30)))
+        whens.append(When(create_filtered_spaces_query(topics_v2=DISCOVER_DEFAULT_SPACES_V2), then=Value(30)))
 
         return Paged.of(
             CommunityModel.objects.prefetch_related("links", "memberships__user__profile")
@@ -119,14 +149,26 @@ class Query:
     def discover_spaces(
         self,
         info,
-        topic_ids: Optional[List[Optional[uuid.UUID]]] = None,
+        topic_ids: Annotated[
+            Optional[List[Optional[uuid.UUID]]],
+            strawberry.argument(
+                description="Deprecated since version 1.52, use topics_v2 instead",
+            ),
+        ] = None,
+        topics_v2: Optional[List[str]] = None,
         location: Annotated[
             Optional[str],
             # Using description instead of deprecation_reason as deprecated arguments are removed in GraphQL Mesh
             strawberry.argument(description="Unsupported since version 1.0.16, use geolocation instead"),
         ] = None,
         geolocation: Optional[GeoJSON] = None,
-        skill_ids: Optional[List[Optional[uuid.UUID]]] = None,
+        skill_ids: Annotated[
+            Optional[List[Optional[uuid.UUID]]],
+            strawberry.argument(
+                description="Deprecated since version 1.52, use skills_v2 instead",
+            ),
+        ] = None,
+        skills_v2: Optional[List[str]] = None,
         offset: int = 0,
         limit: int = 10,
     ) -> List[Optional[DiscoverSpaces]]:
@@ -153,6 +195,16 @@ class Query:
                     )
                 )
 
+        if topics_v2 is not None:
+            for topic_v2 in topics_v2:
+                spaces = async_to_sync(filtered_spaces)(topics_v2=[topic_v2])
+                result.append(
+                    DiscoverSpaces(
+                        topic_v2=topic_v2,
+                        spaces=Paged.of(spaces, offset, limit),
+                    )
+                )
+
         # get all spaces by skill_id (including geolocation filter)
         if skill_ids is not None:
             for skill_id in skill_ids:
@@ -164,6 +216,18 @@ class Query:
                         spaces=Paged.of(spaces, offset, limit),
                     )
                 )
+
+        if skills_v2 is not None:
+            for skill_v2 in skills_v2:
+                spaces = async_to_sync(filtered_spaces)(skills_v2=[skill_v2], geolocation=geolocation)
+                result.append(
+                    DiscoverSpaces(
+                        skill_v2=skill_v2,
+                        location_id=geolocation.id if geolocation else None,
+                        spaces=Paged.of(spaces, offset, limit),
+                    )
+                )
+
         return result
 
     @strawberry_django.field()
@@ -300,19 +364,17 @@ class Query:
                 limit,
             )
 
-        skills = list(current_user.profile.skills.all())
+        skills_v2 = list(current_user.profile.skills_v2)
         whens = []
 
         locations_match = Q(geolocation__within=current_user.profile.geolocation)
-        # We need to use a skill exists query, because we want to check that at least one skill from the user profile exists for the task.
-        skill_exists_subquery = SkillModel.objects.filter(id__in=[skill.id for skill in skills], tasks=OuterRef("id"))
 
-        if skills and current_user.profile.geolocation:
+        if skills_v2 and current_user.profile.geolocation:
             whens.append(
-                When(Q(Exists(skill_exists_subquery)) & (locations_match | Q(location_type="REMOTE")), then=Value(10))
+                When(Q(skills_v2__overlap=skills_v2) & (locations_match | Q(location_type="REMOTE")), then=Value(10))
             )
-        if skills:
-            whens.append(When(skills__in=skills, then=Value(20)))
+        if skills_v2:
+            whens.append(When(skills_v2__overlap=skills_v2, then=Value(20)))
         if current_user.profile.geolocation:
             whens.append(When(locations_match, then=Value(30)))
 
diff --git a/openbook_communities/schema/types.py b/openbook_communities/schema/types.py
index 25e8f4be862876174638bbbfda531faed411d5eb..eb3b0f1f42367d8a16be35f79c57cba1eee19e0c 100644
--- a/openbook_communities/schema/types.py
+++ b/openbook_communities/schema/types.py
@@ -48,7 +48,11 @@ Info: TypeAlias = StrawberryInfo[StrawberryDjangoContext, Any]
 class ValidateSpaceInput:
     title: Optional[str] = strawberry.UNSET
     description: Optional[str] = strawberry.UNSET
-    topics: Optional[List[Optional[uuid.UUID]]] = strawberry.UNSET
+    topics: Annotated[
+        Optional[List[Optional[uuid.UUID]]],
+        strawberry.argument(description="Deprecated since 1.52. Use topics_v2 instead."),
+    ] = strawberry.UNSET
+    topics_v2: Optional[List[str]] = strawberry.UNSET
     cover: Optional[Upload] = strawberry.UNSET
     sdgs: Optional[List[Optional[uuid.UUID]]] = strawberry.UNSET
     location: Optional[str] = strawberry.UNSET
@@ -58,7 +62,11 @@ class ValidateSpaceInput:
 class CreateSpaceInput:
     title: str
     description: str
-    topics: List[Optional[uuid.UUID]]
+    topics: Annotated[
+        Optional[List[Optional[uuid.UUID]]],
+        strawberry.argument(description="Deprecated since 1.52. Use topics_v2 instead."),
+    ] = strawberry.UNSET
+    topics_v2: Optional[List[str]] = strawberry.UNSET
     cover: Upload
 
 
@@ -73,7 +81,11 @@ class CreateTaskInput:
     contact_email: str
     slots_available: Optional[int] = strawberry.UNSET
     thumbnail: Optional[Upload] = strawberry.UNSET
-    skills: Optional[List[Optional[uuid.UUID]]] = strawberry.UNSET
+    skills: Annotated[
+        Optional[List[Optional[uuid.UUID]]],
+        strawberry.argument(description="Deprecated since 1.52. Use skills_v2 instead."),
+    ] = strawberry.UNSET
+    skills_v2: Optional[List[str]] = strawberry.UNSET
     location: Optional[str] = strawberry.UNSET
     geolocation: Optional[GeolocationPointInput] = strawberry.UNSET
     contact_phone: Optional[str] = strawberry.UNSET
@@ -91,7 +103,11 @@ class UpdateTaskInput:
     contact_email: str
     slots_available: Optional[int] = strawberry.UNSET
     thumbnail: Optional[Upload] = strawberry.UNSET
-    skills: Optional[List[Optional[uuid.UUID]]] = strawberry.UNSET
+    skills: Annotated[
+        Optional[List[Optional[uuid.UUID]]],
+        strawberry.argument(description="Deprecated since 1.52. Use skills_v2 instead."),
+    ] = strawberry.UNSET
+    skills_v2: Optional[List[str]] = strawberry.UNSET
     location: Optional[str] = strawberry.UNSET
     geolocation: Optional[GeolocationPointInput] = strawberry.UNSET
     contact_phone: Optional[str] = strawberry.UNSET
@@ -151,7 +167,11 @@ class UpdateSpaceInput:
     id: uuid.UUID
     title: Optional[str] = strawberry.UNSET
     description: Optional[str] = strawberry.UNSET
-    topics: Optional[List[Optional[uuid.UUID]]] = strawberry.UNSET
+    topics: Annotated[
+        Optional[List[Optional[uuid.UUID]]],
+        strawberry.argument(description="Deprecated since 1.52. Use topics_v2 instead."),
+    ] = strawberry.UNSET
+    topics_v2: Optional[List[str]] = strawberry.UNSET
     cover: Optional[Upload] = strawberry.UNSET
     avatar: Optional[Upload] = strawberry.UNSET
     sdgs: Optional[List[Optional[uuid.UUID]]] = strawberry.UNSET
@@ -189,7 +209,10 @@ class Task:
     location_type: TaskModel.LocationType
     location: strawberry.auto
     regularity: TaskModel.Regularity
-    skills: List[Skill]
+    skills: List[Skill] = strawberry_django.field(
+        deprecation_reason="Deprecated since version 1.52, use skills_v2 instead.", default=None
+    )
+    skills_v2: List[str]
     thumbnail: Optional[str]
     thumbnail_blurhash: Optional[str]
     contact_email: str
@@ -258,7 +281,10 @@ class Space:
     cover_blurhash: Optional[str]
     location: strawberry.auto
     links: List[Optional[ExternalLink]]
-    topics: List[Optional[Topic]]
+    topics: List[Optional[Topic]] = strawberry_django.field(
+        deprecation_reason="Deprecated since version 1.52, use topics_v2 instead.", default=None
+    )
+    topics_v2: List[str]
     sdgs: List[Optional[SDG]]
     contact_description: Optional[str]
     contact_email: Optional[str]
@@ -430,13 +456,20 @@ class Space:
 @strawberry.type
 class DiscoverSpaces:
     spaces: Paged[Space]
-    topic_id: Optional[uuid.UUID] = None
+    topic_id: Optional[uuid.UUID] = strawberry.field(
+        deprecation_reason="Deprecated since version 1.52, use topic_v2 instead", default=None
+    )
+    topic_v2: Optional[str] = None
     location: Optional[str] = strawberry.field(
-        deprecation_reason="Unsupported since version 1.0.16, use location_id instead",
+        deprecation_reason="Deprecated since version 1.0.16, use location_id instead",
         default=None,
     )
     location_id: Optional[str] = None
-    skill_id: Optional[uuid.UUID] = None
+    skill_id: Optional[uuid.UUID] = strawberry.field(
+        deprecation_reason="Deprecated since version 1.52, use skill_v2 instead",
+        default=None,
+    )
+    skill_v2: Optional[str] = None
 
 
 @strawberry.type
diff --git a/openbook_communities/serializers/communities_serializers.py b/openbook_communities/serializers/communities_serializers.py
index 7300ffec50a8d9fa162f806f9f670ff45d2872ba..e4c2ce4832aa2c88c93a2f7997a479c4a6e067c4 100644
--- a/openbook_communities/serializers/communities_serializers.py
+++ b/openbook_communities/serializers/communities_serializers.py
@@ -7,7 +7,13 @@ from openbook_categories.validators import category_name_exists
 from openbook_common.serializers import CommonCommunityMembershipSerializer, GeolocationPointSerializer
 from openbook_common.serializers_fields.community import CommunityPostsCountField
 from openbook_common.serializers_fields.request import RestrictedImageFileSizeField
-from openbook_common.validators import hex_color_validator, url_safety_validator, sanitize_text
+from openbook_common.validators import (
+    hex_color_validator,
+    url_safety_validator,
+    sanitize_text,
+    topic_v2_exists,
+    skill_v2_exists,
+)
 from openbook_communities.models import CollaborationTool, Community, ExternalLinkType, Task, VisibilityType
 from openbook_communities.serializers_fields import (
     IsInvitedField,
@@ -78,9 +84,15 @@ class CreateCommunitySerializer(serializers.Serializer):
     topics = serializers.ListField(
         min_length=settings.COMMUNITY_TOPICS_MIN_AMOUNT,
         max_length=settings.COMMUNITY_TOPICS_MAX_AMOUNT,
-        required=True,
+        required=False,
         child=serializers.UUIDField(validators=[topic_with_id_exists]),
     )
+    topics_v2 = serializers.ListField(
+        min_length=settings.COMMUNITY_TOPICS_MIN_AMOUNT,
+        max_length=settings.COMMUNITY_TOPICS_MAX_AMOUNT,
+        required=False,
+        child=serializers.CharField(validators=[topic_v2_exists]),
+    )
 
 
 class ValidateCommunitySerializer(serializers.Serializer):
@@ -98,6 +110,12 @@ class ValidateCommunitySerializer(serializers.Serializer):
         required=False,
         child=serializers.UUIDField(validators=[topic_with_id_exists]),
     )
+    topics_v2 = serializers.ListField(
+        min_length=settings.COMMUNITY_TOPICS_MIN_AMOUNT,
+        max_length=settings.COMMUNITY_TOPICS_MAX_AMOUNT,
+        required=False,
+        child=serializers.CharField(validators=[topic_v2_exists]),
+    )
     cover = RestrictedImageFileSizeField(
         allow_empty_file=False,
         required=False,
@@ -141,6 +159,12 @@ class UpdateCommunitySerializer(serializers.Serializer):
         required=False,
         child=serializers.UUIDField(validators=[topic_with_id_exists]),
     )
+    topics_v2 = serializers.ListField(
+        min_length=settings.COMMUNITY_TOPICS_MIN_AMOUNT,
+        max_length=settings.COMMUNITY_TOPICS_MAX_AMOUNT,
+        required=False,
+        child=serializers.CharField(max_length=settings.TERM_SLUG_MAX_LENGTH, validators=[topic_v2_exists]),
+    )
     cover = RestrictedImageFileSizeField(
         allow_empty_file=False,
         allow_null=True,
@@ -406,6 +430,12 @@ class CreateTaskSerializer(serializers.Serializer):
         required=False,
         child=serializers.UUIDField(validators=[skill_with_id_exists]),
     )
+    skills_v2 = serializers.ListField(
+        min_length=settings.TASK_SKILLS_MIN_AMOUNT,
+        max_length=settings.TASK_SKILLS_MAX_AMOUNT,
+        required=False,
+        child=serializers.CharField(max_length=settings.TERM_SLUG_MAX_LENGTH, validators=[skill_v2_exists]),
+    )
     thumbnail = RestrictedImageFileSizeField(
         allow_empty_file=False,
         required=False,
@@ -455,6 +485,13 @@ class UpdateTaskSerializer(serializers.Serializer):
         required=False,
         child=serializers.UUIDField(validators=[skill_with_id_exists]),
     )
+    skills_v2 = serializers.ListField(
+        min_length=settings.TASK_SKILLS_MIN_AMOUNT,
+        max_length=settings.TASK_SKILLS_MAX_AMOUNT,
+        required=False,
+        allow_empty=True,
+        child=serializers.CharField(max_length=settings.TERM_SLUG_MAX_LENGTH, validators=[skill_v2_exists]),
+    )
     thumbnail = RestrictedImageFileSizeField(
         allow_empty_file=False,
         allow_null=True,
diff --git a/openbook_communities/tests/test_graphql_communities.py b/openbook_communities/tests/test_graphql_communities.py
index 834ff8b02aee303257be26e47c935e1303f0101e..32bdb2868d8a3742411b0ec13534f2cd14979787 100644
--- a/openbook_communities/tests/test_graphql_communities.py
+++ b/openbook_communities/tests/test_graphql_communities.py
@@ -30,10 +30,10 @@ from openbook_common.tests.helpers import (
     make_fake_post_text,
     make_fake_post_title,
     make_sdg,
-    make_skill,
-    make_topic,
+    make_skills,
     make_user,
     simple_jpeg_bytes,
+    make_topics,
 )
 from openbook_common.utils.model_loaders import (
     get_community_admin_privileges_granted_notification_model,
@@ -58,6 +58,7 @@ from openbook_communities.tests.helpers import (
 from openbook_notifications.tests.helpers import get_notifications
 from openbook_posts.enums import VisibilityFilter
 from openbook_posts.tests.helpers import private_post_ids, public_post_ids
+from openbook_terms.helpers import topics_to_topic_v2_slugs
 
 logger = logging.getLogger(__name__)
 
@@ -77,8 +78,8 @@ def django_db_setup(django_db_setup, django_db_blocker):
 class TestCommunities(TestCase):
     def test_filters_for_spaces(self):
         creator = make_user()
-        topic1 = make_topic()
-        topic2 = make_topic()
+        [topic1, topic2] = make_topics(2)
+        topic1_v2 = topic1.slug
         # locations inside geolocation filter
         geolocation1 = geos.Point(8.5541567, 53.5101827, srid=4326)
         geolocation2 = geos.Point(8.5798387, 53.5590841, srid=4326)
@@ -87,16 +88,15 @@ class TestCommunities(TestCase):
         space1 = make_community(creator=creator, topics=[topic1, topic2], geolocation=geolocation1)
         space2 = make_community(topics=[topic1], geolocation=geolocation2)
         space3 = make_community(topics=[topic2], geolocation=geolocation3)
-        skill1 = make_skill()
-        skill2 = make_skill()
-        skill3 = make_skill()
+        [skill1, skill2, skill3] = make_skills(3)
+        [skill1_v2, skill2_v2] = [skill1.slug, skill2.slug]
         make_community_task(space1, skills=[skill1, skill2])
         make_community_task(space2, skills=[skill2, skill3])
         make_community_task(space3, skills=[skill3])
 
         filter_query = """
-            query($topicId: UUID, $geolocation: GeoJSON, $skillIds: [UUID], $memberId: UUID, $topicSlug: String, $topicSlugs: [String]) {
-                spaces(topicId: $topicId, geolocation: $geolocation, skillIds: $skillIds, memberId: $memberId, topicSlug: $topicSlug, topicSlugs: $topicSlugs) {
+            query($topicId: UUID, $geolocation: GeoJSON, $skillIds: [UUID], $memberId: UUID, $topicSlug: String, $topicSlugs: [String], $topicsV2: [String!], $skillsV2: [String!]) {
+                spaces(topicId: $topicId, geolocation: $geolocation, skillIds: $skillIds, memberId: $memberId, topicSlug: $topicSlug, topicSlugs: $topicSlugs, topicsV2: $topicsV2, skillsV2: $skillsV2) {
                     totalResults
                     data {
                         id
@@ -120,6 +120,7 @@ class TestCommunities(TestCase):
         assert_filtered_spaces({"topicId": str(topic1.id)}, [space1, space2])
         assert_filtered_spaces({"topicSlug": topic1.slug}, [space1, space2])
         assert_filtered_spaces({"topicSlugs": [topic1.slug]}, [space1, space2])
+        assert_filtered_spaces({"topicsV2": [topic1_v2]}, [space1, space2])
         # topicSlug and topicSlugs can be combined
         assert_filtered_spaces({"topicSlugs": [topic1.slug], "topicSlug": topic2.slug}, [space1, space2, space3])
         assert_filtered_spaces({"topicSlugs": [topic1.slug, topic2.slug]}, [space1, space2, space3])
@@ -138,6 +139,7 @@ class TestCommunities(TestCase):
 
         # Filter by multiple skills
         assert_filtered_spaces({"skillIds": [str(skill1.id), str(skill2.id)]}, [space1, space2])
+        assert_filtered_spaces({"skillsV2": [skill1_v2, skill2_v2]}, [space1, space2])
 
         # Filter by skills and location
         assert_filtered_spaces({"skillIds": [str(skill3.id)], "geolocation": geolocation}, [space2])
@@ -159,11 +161,20 @@ class TestCommunities(TestCase):
             [],
         )
 
+        # Filter by skills V2, topics V2 and location
+        assert_filtered_spaces(
+            {
+                "skillsV2": [skill2_v2],
+                "topicsV2": [topic1_v2],
+            },
+            [space1, space2],
+        )
+
     def test_filters_for_recommended_spaces(self):
-        topic1 = make_topic()
-        topic2 = make_topic()
-        default_topic = make_topic(is_discover_spaces_default=True)
-        uninteresting_topic = make_topic()
+        # skipping some default topics V2
+        [default_topic, _, _, _, topic1, topic2, uninteresting_topic] = make_topics(7)
+        default_topic.is_discover_spaces_default = True
+        default_topic.save()
 
         # locations inside geolocation filter
         geolocation1 = geos.Point(8.5541567, 53.5101827, srid=4326)
@@ -206,6 +217,7 @@ class TestCommunities(TestCase):
         user = make_user()
         user.profile.geolocation = geos.GEOSGeometry(json.dumps(geolocation_geometry))
         user.profile.interests.add(topic1, topic2)
+        user.profile.interests_v2 = [t.slug for t in [topic1, topic2]]
         assert_filtered_spaces(
             user,
             [
@@ -217,6 +229,7 @@ class TestCommunities(TestCase):
         )
         user = make_user()
         user.profile.interests.add(topic1)
+        user.profile.interests_v2 = [topic1.slug]
         assert_filtered_spaces(
             user,
             [
@@ -245,12 +258,15 @@ class TestCommunities(TestCase):
         )
 
     def test_filters_for_recommended_spaces_paging(self):
-        topic1 = make_topic()
-        default_topic = make_topic(is_discover_spaces_default=True)
-        uninteresting_topic = make_topic()
+        # skipping some default topics V2
+        [default_topic, _, _, _, _, topic1, uninteresting_topic] = make_topics(7)
+        default_topic.is_discover_spaces_default = True
+        default_topic.save()
+
         user = make_user()
         user.profile.geolocation = geos.GEOSGeometry(json.dumps(geolocation_geometry))
         user.profile.interests.add(topic1)
+        user.profile.interests_v2 = [topic1.slug]
 
         # locations inside geolocation filter
         geolocation1 = geos.Point(8.5541567, 53.5101827, srid=4326)
@@ -282,10 +298,10 @@ class TestCommunities(TestCase):
         def transform_space(space):
             return {"id": str(space.id)}
 
-        def assert_filtered_spaces(user, filter_options, expected_spaces, total_results):
+        def assert_recommendended_spaces(user, paging_options, expected_spaces, total_results):
             filter_response = async_to_sync(schema.execute)(
                 recommended_spaces_query,
-                variable_values=filter_options,
+                variable_values=paging_options,
                 context_value=benedict({"request": {"user": user}}),
             )
             assert filter_response.errors is None
@@ -293,9 +309,9 @@ class TestCommunities(TestCase):
             assert filter_response.data["recommendedSpaces"]["data"] == expected_data
             assert filter_response.data["recommendedSpaces"]["totalResults"] == total_results
 
-        assert_filtered_spaces(
+        assert_recommendended_spaces(
             user,
-            {"topicIds": [str(topic1.id)], "geolocation": geolocation, "offset": 2, "limit": 3},
+            {"offset": 2, "limit": 3},
             [
                 topic1_inside_space1,
                 uninteresting_topic_inside_space3,
@@ -303,9 +319,9 @@ class TestCommunities(TestCase):
             ],
             9,
         )
-        assert_filtered_spaces(
+        assert_recommendended_spaces(
             user,
-            {"topicIds": [str(topic1.id)], "geolocation": geolocation, "offset": 2, "limit": 6},
+            {"offset": 2, "limit": 6},
             [
                 topic1_inside_space1,
                 uninteresting_topic_inside_space3,
@@ -316,9 +332,9 @@ class TestCommunities(TestCase):
             ],
             9,
         )
-        assert_filtered_spaces(
+        assert_recommendended_spaces(
             user,
-            {"topicIds": [str(topic1.id)], "geolocation": geolocation, "offset": 4, "limit": 3},
+            {"offset": 4, "limit": 3},
             [
                 uninteresting_topic_inside_space2,
                 uninteresting_topic_inside_space1,
@@ -326,9 +342,9 @@ class TestCommunities(TestCase):
             ],
             9,
         )
-        assert_filtered_spaces(
+        assert_recommendended_spaces(
             user,
-            {"topicIds": [str(topic1.id)], "geolocation": geolocation, "offset": 0, "limit": 20},
+            {"offset": 0, "limit": 20},
             [
                 topic1_inside_space3,
                 topic1_inside_space2,
@@ -342,16 +358,16 @@ class TestCommunities(TestCase):
             ],
             9,
         )
-        assert_filtered_spaces(
+        assert_recommendended_spaces(
             user,
-            {"topicIds": [str(topic1.id)], "geolocation": geolocation, "offset": 10, "limit": 20},
+            {"offset": 10, "limit": 20},
             [],
             9,
         )
 
     def test_filters_for_discover_spaces(self):
-        topic1 = make_topic()
-        topic2 = make_topic()
+        [topic1, topic2] = make_topics(2)
+        [topic1_v2, topic2_v2] = [topic1.slug, topic2.slug]
         # locations inside geolocation filter
         geolocation1 = geos.Point(8.5541567, 53.5101827, srid=4326)
         geolocation2 = geos.Point(8.5798387, 53.5590841, srid=4326)
@@ -360,18 +376,19 @@ class TestCommunities(TestCase):
         space1 = make_community(topics=[topic1, topic2], geolocation=geolocation1)
         space2 = make_community(topics=[topic1], geolocation=geolocation2)
         space3 = make_community(topics=[topic2], geolocation=geolocation3)
-        make_community(topics=[topic1], geolocation=geolocation2, is_deleted=True)  # deleted spaces are not be shown
-        skill1 = make_skill()
-        skill2 = make_skill()
-        skill3 = make_skill()
+        make_community(topics=[topic1], geolocation=geolocation2, is_deleted=True)  # deleted spaces are not shown
+        [skill1, skill2, skill3] = make_skills(3)
+        skill3_v2 = skill3.slug
         make_community_task(space1, skills=[skill1, skill2])
         make_community_task(space2, skills=[skill2, skill3])
 
         filter_query = """
-            query($topicIds: [UUID], $geolocation: GeoJSON, $skillIds: [UUID]) {
-                discoverSpaces(topicIds: $topicIds, geolocation: $geolocation, skillIds: $skillIds) {
+            query($topicIds: [UUID], $geolocation: GeoJSON, $skillIds: [UUID], $topicsV2: [String!], $skillsV2: [String!]) {
+                discoverSpaces(topicIds: $topicIds, geolocation: $geolocation, skillIds: $skillIds, topicsV2: $topicsV2, skillsV2: $skillsV2) {
                     topicId
+                    topicV2
                     skillId
+                    skillV2
                     locationId
                     spaces {
                         totalResults
@@ -400,10 +417,28 @@ class TestCommunities(TestCase):
 
         # Filter by common topic
         assert_filtered_spaces({"topicIds": [str(topic1.id)]}, [space1, space2])
+        assert_filtered_spaces({"topicsV2": [topic1_v2]}, [space1, space2])
 
         # Filter by unique topic
         assert_filtered_spaces({"topicIds": [str(topic2.id)]}, [space1, space3])
+        assert_filtered_spaces({"topicsV2": [topic2_v2]}, [space1, space3])
+
+        # Filter by topics, skills and geolocation
+        filter_response = async_to_sync(schema.execute)(
+            filter_query,
+            variable_values={
+                "topicIds": [str(topic1.id), str(topic2.id)],
+                "skillIds": [str(skill3.id)],
+                "geolocation": geolocation,
+            },
+            context_value=benedict({"request": {"user": AnonymousUser}}),
+        )
+        assert len(filter_response.data["discoverSpaces"]) == 4
+        location, t1, t2, skill = (
+            (0, 1, 2, 3) if filter_response.data["discoverSpaces"][1]["topicId"] == str(topic1.id) else (0, 2, 1, 3)
+        )
 
+        # Filter by topics, skills and geolocation
         filter_response = async_to_sync(schema.execute)(
             filter_query,
             variable_values={
@@ -435,6 +470,38 @@ class TestCommunities(TestCase):
             list(map(transform_space, [space2])),
         )
 
+        # Filter by topics V2, skills V2 and geolocation
+        filter_response = async_to_sync(schema.execute)(
+            filter_query,
+            variable_values={
+                "topicsV2": [topic1_v2, topic2_v2],
+                "skillsV2": [skill3_v2],
+                "geolocation": geolocation,
+            },
+            context_value=benedict({"request": {"user": AnonymousUser}}),
+        )
+        assert len(filter_response.data["discoverSpaces"]) == 4
+        location, t1, t2, skill = (
+            (0, 1, 2, 3) if filter_response.data["discoverSpaces"][1]["topicV2"] == topic1_v2 else (0, 2, 1, 3)
+        )
+
+        assert_paged_results_equal(
+            filter_response.data["discoverSpaces"][location]["spaces"],
+            list(map(transform_space, [space1, space2])),
+        )
+        assert_paged_results_equal(
+            filter_response.data["discoverSpaces"][t1]["spaces"],
+            list(map(transform_space, [space1, space2])),
+        )
+        assert_paged_results_equal(
+            filter_response.data["discoverSpaces"][t2]["spaces"],
+            list(map(transform_space, [space1, space3])),
+        )
+        assert_paged_results_equal(
+            filter_response.data["discoverSpaces"][skill]["spaces"],
+            list(map(transform_space, [space2])),
+        )
+
     def test_my_spaces(self):
         creator1 = make_user()
         creator2 = make_user()
@@ -2532,7 +2599,9 @@ class TestCommunities(TestCase):
 
     def test_invalid_spaces_do_not_pass_validation(self):
         user = make_user()
-        topic_ids = [str(make_topic().id) for _ in range(7)]
+        topics = make_topics(7)
+        topic_ids = [str(t.id) for t in topics]
+        topics_v2 = topics_to_topic_v2_slugs(topics)
         empty_image = SimpleUploadedFile(name="cover.jpg", content="")
 
         validate_space_mutation = """
@@ -2566,6 +2635,14 @@ class TestCommunities(TestCase):
                 "value": [invalid_uuid],
                 "expected_error": f"No topic with the id '{invalid_uuid}' found.",
             },
+            {"field": "topicsV2", "value": None, "expected_code": "null"},
+            {"field": "topicsV2", "value": [], "expected_code": "min_length"},
+            {"field": "topicsV2", "value": topics_v2, "expected_code": "max_length"},
+            {
+                "field": "topicsV2",
+                "value": ["unknown"],
+                "expected_error": "TopicV2 'unknown' does not exist.",
+            },
             {"field": "cover", "value": None, "expected_code": "null"},
             {"field": "cover", "value": {}, "expected_code": "invalid"},
             {"field": "cover", "value": "", "expected_code": "invalid"},
@@ -2582,18 +2659,25 @@ class TestCommunities(TestCase):
                 variable_values={"input": valid_input | {testCase["field"]: testCase["value"]}},
             )
 
-            assert (response.errors, testCase) is not None
+            assert (response.errors, testCase) is not None, "Expected error"
             if "expected_code" in testCase:
                 field_errors = json.loads(response.errors[0].extensions["errors"])
-                assert testCase["expected_code"] == field_errors[testCase["field"]][0]["code"]
+                actual_code = field_errors[testCase["field"]][0]["code"]
+                assert testCase["expected_code"] == actual_code, (
+                    f"Expected error code {testCase['expected_code']} but got {actual_code} for input {testCase}"
+                )
             else:
-                assert testCase["expected_error"] in str(response.errors[0].message)
+                assert testCase["expected_error"] in str(response.errors[0].message), (
+                    f"Expected error {testCase['expected_error']} but got {str(response.errors[0].message)} for input {testCase}"
+                )
             assert response.data is None
 
     def test_valid_partial_spaces_pass_validation(self):
         user = make_user()
         valid_input = {"title": "valid", "description": "valid"}
-        topic_ids = [str(make_topic().id) for _ in range(2)]
+        topics = make_topics(2)
+        topic_ids = [str(t.id) for t in topics]
+        topics_v2 = topics_to_topic_v2_slugs(topics)
         cover = SimpleUploadedFile(name="cover.jpg", content=simple_jpeg_bytes)
         sdg_ids = [str(make_sdg().id) for _ in range(2)]
         location = "Hamburg, Germany"
@@ -2613,6 +2697,8 @@ class TestCommunities(TestCase):
             valid_input | {"location": location},
             valid_input | {"topics": topic_ids, "cover": cover, "sdgs": sdg_ids, "location": location},
             valid_input | {"topics": topic_ids, "cover": cover, "sdgs": []},
+            valid_input | {"topicsV2": topics_v2},
+            valid_input | {"topicsV2": topics_v2, "cover": cover, "sdgs": []},
         ]
 
         for input in valid_inputs:
@@ -2649,8 +2735,10 @@ class TestCommunities(TestCase):
         self, mock_publish_space_created_event, mock_invalidate_discover_spaces_query_cache
     ):
         user = make_user()
-        topic_ids = [str(make_topic().id) for _ in range(7)]
+        topics = make_topics(7)
+        topic_ids = [str(t.id) for t in topics]
         valid_topics = topic_ids[:2]
+        valid_topics_v2 = topics_to_topic_v2_slugs(topics[:2])
         valid_image = SimpleUploadedFile(name="cover.jpg", content=simple_jpeg_bytes)
 
         create_space_mutation = """
@@ -2668,7 +2756,7 @@ class TestCommunities(TestCase):
             },
             {
                 "input": {"title": "valid", "description": "valid", "cover": valid_image},
-                "expected_error": "was not provided",
+                "expected_error": "Either topics or topics_v2 must be specified",
             },
             {
                 "input": {"title": "valid", "topics": valid_topics, "cover": valid_image},
@@ -2678,6 +2766,10 @@ class TestCommunities(TestCase):
                 "input": {"description": "valid", "topics": valid_topics, "cover": valid_image},
                 "expected_error": "was not provided",
             },
+            {
+                "input": {"description": "valid", "topicsV2": valid_topics_v2, "cover": valid_image},
+                "expected_error": "was not provided",
+            },
         ]
 
         for testCase in expectedErrors:
@@ -2695,7 +2787,9 @@ class TestCommunities(TestCase):
 
     def test_validates_space_before_creation(self):
         user = make_user()
-        topic_ids = [str(make_topic().id) for _ in range(7)]
+        topics = make_topics(7)
+        topic_ids = [str(t.id) for t in topics]
+        topics_v2 = topics_to_topic_v2_slugs(topics)
         valid_topics = topic_ids[:2]
         valid_image = SimpleUploadedFile(name="cover.jpg", content=simple_jpeg_bytes)
         empty_image = SimpleUploadedFile(name="cover.jpg", content="")
@@ -2722,7 +2816,6 @@ class TestCommunities(TestCase):
                 "value": "f" * (COMMUNITY_DESCRIPTION_MAX_LENGTH + 1),
                 "expected_code": "max_length",
             },
-            {"field": "topics", "value": None, "expected_error": "Expected non-nullable type"},
             {"field": "topics", "value": [], "expected_code": "min_length"},
             {"field": "topics", "value": topic_ids, "expected_code": "max_length"},
             {
@@ -2730,6 +2823,13 @@ class TestCommunities(TestCase):
                 "value": [invalid_uuid],
                 "expected_error": f"No topic with the id '{invalid_uuid}' found.",
             },
+            {"field": "topicsV2", "value": [], "expected_code": "min_length"},
+            {"field": "topicsV2", "value": topics_v2, "expected_code": "max_length"},
+            {
+                "field": "topicsV2",
+                "value": ["unknown"],
+                "expected_error": "TopicV2 'unknown' does not exist.",
+            },
             {"field": "cover", "value": None, "expected_error": "Expected non-nullable type"},
             {"field": "cover", "value": {}, "expected_code": "invalid"},
             {"field": "cover", "value": "", "expected_code": "invalid"},
@@ -2757,7 +2857,9 @@ class TestCommunities(TestCase):
     @mock.patch("openbook_communities.schema.mutations.publish_space_created_event")
     def test_can_create_space(self, mock_publish_space_created_event, mock_invalidate_discover_spaces_query_cache):
         user = make_user()
-        topic_ids = [str(make_topic().id) for _ in range(2)]
+        topics = make_topics(2)
+        topic_ids = [str(t.id) for t in topics]
+        expected_topics_v2 = topics_to_topic_v2_slugs(topics)
         valid_image = SimpleUploadedFile(name="cover.jpg", content=simple_jpeg_bytes)
 
         create_space_mutation = """
@@ -2768,6 +2870,7 @@ class TestCommunities(TestCase):
                     title
                     description
                     topics { id }
+                    topicsV2
                     cover
                     members {
                         data {
@@ -2797,6 +2900,7 @@ class TestCommunities(TestCase):
         for topic in created_space["topics"]:
             assert topic["id"] in topic_ids
         assert "cover.jpg" in created_space["cover"]
+        assert expected_topics_v2 == created_space["topicsV2"]
 
         # Verify that creator is admin
         assert 1 == len(created_space["members"]["data"])
@@ -2808,12 +2912,42 @@ class TestCommunities(TestCase):
         mock_publish_space_created_event.assert_called_once_with(community=community)
         mock_invalidate_discover_spaces_query_cache.assert_called_once()
 
+    @mock.patch("openbook_communities.schema.mutations.invalidate_discover_spaces_query_cache")
+    @mock.patch("openbook_communities.schema.mutations.publish_space_created_event")
+    def test_can_create_space_with_topics_v2(
+        self, mock_publish_space_created_event, mock_invalidate_discover_spaces_query_cache
+    ):
+        user = make_user()
+        topics_v2 = topics_to_topic_v2_slugs(make_topics(2))
+        valid_image = SimpleUploadedFile(name="cover.jpg", content=simple_jpeg_bytes)
+
+        create_space_mutation = """
+            mutation test($input: CreateSpaceInput!) {
+                createSpace(input: $input) {
+                    id
+                    topicsV2
+                }
+            }
+            """
+        valid_input = {"title": "valid", "description": "valid", "topicsV2": topics_v2, "cover": valid_image}
+
+        response = async_to_sync(schema.execute)(
+            create_space_mutation,
+            context_value=benedict({"request": {"user": user}}),
+            variable_values={"input": valid_input},
+        )
+
+        assert response.errors is None
+
+        created_space = response.data["createSpace"]
+        assert sorted(topics_v2) == sorted(created_space["topicsV2"])
+
     @mock.patch("openbook_communities.schema.mutations.invalidate_discover_spaces_query_cache")
     @mock.patch("openbook_communities.schema.mutations.publish_space_created_event")
     def test_can_not_create_space_if_not_logged_in(
         self, mock_publish_space_created_event, mock_invalidate_discover_spaces_query_cache
     ):
-        topic_ids = [str(make_topic().id) for _ in range(7)]
+        topic_ids = [str(t.id) for t in make_topics(7)]
         valid_topics = topic_ids[:2]
         valid_image = SimpleUploadedFile(name="cover.jpg", content=simple_jpeg_bytes)
 
@@ -2839,7 +2973,9 @@ class TestCommunities(TestCase):
     def test_validates_space_update(self):
         user = make_user()
         community = make_community()
-        topic_ids = [str(make_topic().id) for _ in range(7)]
+        topics = make_topics(7)
+        topic_ids = [str(t.id) for t in topics]
+        topics_v2 = topics_to_topic_v2_slugs(topics)
         empty_image = SimpleUploadedFile(name="cover.jpg", content="")
 
         update_space_mutation = """
@@ -2871,6 +3007,13 @@ class TestCommunities(TestCase):
                 "value": [invalid_uuid],
                 "expected_error": f"No topic with the id '{invalid_uuid}' found.",
             },
+            {"field": "topicsV2", "value": [], "expected_code": "min_length"},
+            {"field": "topicsV2", "value": topics_v2, "expected_code": "max_length"},
+            {
+                "field": "topicsV2",
+                "value": ["unknown"],
+                "expected_error": "TopicV2 'unknown' does not exist",
+            },
             {"field": "cover", "value": {}, "expected_code": "invalid"},
             {"field": "cover", "value": "", "expected_code": "invalid"},
             {"field": "cover", "value": "foobar", "expected_code": "invalid"},
@@ -2954,7 +3097,9 @@ class TestCommunities(TestCase):
     def test_can_update_space(self, mock_invalidate_discover_spaces_query_cache):
         user = make_user()
         community = make_community(creator=user)
-        topic_ids = [str(make_topic().id) for _ in range(3)]
+        topics = make_topics(3)
+        topic_ids = [str(t.id) for t in topics]
+        topics_v2 = topics_to_topic_v2_slugs(topics)
         cover = SimpleUploadedFile(name="cover.jpg", content=simple_jpeg_bytes)
         cover2 = SimpleUploadedFile(name="cover2.jpg", content=simple_jpeg_bytes)
         avatar = SimpleUploadedFile(name="avatar.jpg", content=simple_jpeg_bytes)
@@ -2973,6 +3118,7 @@ class TestCommunities(TestCase):
             {"title": "new title"},
             {"description": "new description"},
             {"topics": topic_ids[:2]},
+            {"topicsV2": topics_v2[2:]},
             {"cover": cover},
             {"avatar": avatar},
             {"sdgs": sdg_ids[:2]},
@@ -3008,7 +3154,9 @@ class TestCommunities(TestCase):
     def test_updated_space_has_new_values(self):
         user = make_user()
         community = make_community(creator=user)
-        topic_ids = [str(make_topic().id) for _ in range(2)]
+        topics = make_topics(2)
+        topic_ids = [str(t.id) for t in topics]
+        expected_topics_v2 = topics_to_topic_v2_slugs(topics)
 
         cover = SimpleUploadedFile(name="cover.jpg", content=simple_jpeg_bytes)
         avatar = SimpleUploadedFile(name="avatar.jpg", content=simple_jpeg_bytes)
@@ -3025,6 +3173,7 @@ class TestCommunities(TestCase):
                     title
                     description
                     topics { id }
+                    topicsV2
                     cover
                     coverBlurhash
                     avatar
@@ -3066,6 +3215,7 @@ class TestCommunities(TestCase):
         assert len(topic_ids) == len(updated_space["topics"])
         for topic in updated_space["topics"]:
             assert topic["id"] in topic_ids
+        assert sorted(updated_space["topicsV2"]) == sorted(expected_topics_v2)
         assert "cover.jpg" in updated_space["cover"]
         assert "avatar.jpg" in updated_space["avatar"]
         assert blurhash_img == updated_space["coverBlurhash"]
diff --git a/openbook_communities/tests/test_graphql_tasks.py b/openbook_communities/tests/test_graphql_tasks.py
index 8d741fd20f9c1479aed67b4f5754c219c35e5fea..dd9dc871dea38bb308f9fd9ff849dc8467ec9c47 100644
--- a/openbook_communities/tests/test_graphql_tasks.py
+++ b/openbook_communities/tests/test_graphql_tasks.py
@@ -21,6 +21,7 @@ from openbook_common.tests.helpers import (
     make_community,
     make_community_task,
     make_skill,
+    make_skills,
     make_user,
     simple_jpeg_bytes,
     assert_is_forbidden,
@@ -217,6 +218,11 @@ class TestTasks:
                 "value": [non_existent_skill_id],
                 "expected_error": f"No skill with the id '{non_existent_skill_id}' found.",
             },
+            {
+                "field": "skillsV2",
+                "value": ["unknown"],
+                "expected_error": "SkillV2 'unknown' does not exist.",
+            },
         ]
 
         for testCase in expectedErrors:
@@ -238,7 +244,8 @@ class TestTasks:
 
         community = make_community(creator=user)
         task = make_community_task(community)
-        skill = make_skill()
+        [skill, skill2] = make_skills(2)
+        expected_skills_v2 = [skill.slug]
 
         update_task_mutation = """
             mutation test($input: UpdateTaskInput!) {
@@ -249,6 +256,7 @@ class TestTasks:
                     visibility
                     thumbnail
                     thumbnailBlurhash
+                    skillsV2
                 }
             }
             """
@@ -289,6 +297,7 @@ class TestTasks:
         assert valid_visibility == updateTask["visibility"]
         assert "thumbnail.jpg" in updateTask["thumbnail"]
         assert updateTask["thumbnailBlurhash"] is not None
+        assert expected_skills_v2 == updateTask["skillsV2"]
 
         valid_name = "New valid name"
         valid_description = "New valid description"
@@ -305,6 +314,7 @@ class TestTasks:
             "contactEmail": "email@email.com",
             "thumbnail": None,
             "slotsAvailable": 1,
+            "skillsV2": [skill2.slug],
         }
 
         response = async_to_sync(schema.execute)(
@@ -322,6 +332,7 @@ class TestTasks:
         assert valid_visibility == updateTask["visibility"]
         assert updateTask["thumbnail"] is None
         assert updateTask["thumbnailBlurhash"] is None
+        assert [skill2.slug] == updateTask["skillsV2"]
 
     def test_can_update_members_only_task_if_collaborator(self):
         user = make_user()
@@ -692,6 +703,11 @@ class TestTasks:
                 "value": [non_existent_skill_id],
                 "expected_error": f"No skill with the id '{non_existent_skill_id}' found.",
             },
+            {
+                "field": "skillsV2",
+                "value": ["unknown"],
+                "expected_error": "SkillV2 'unknown' does not exist.",
+            },
         ]
 
         for testCase in expectedErrors:
@@ -713,6 +729,7 @@ class TestTasks:
 
         community = make_community(creator=user)
         skill = make_skill()
+        expected_skills_v2 = [skill.slug]
 
         create_task_mutation = """
             mutation test($input: CreateTaskInput!) {
@@ -721,6 +738,7 @@ class TestTasks:
                     name
                     description
                     visibility
+                    skillsV2
                 }
             }
             """
@@ -751,11 +769,56 @@ class TestTasks:
 
         assert response.errors is None
 
-        added_collaboration_tool = response.data["createTask"]
-        assert "id" in added_collaboration_tool
-        assert valid_name == added_collaboration_tool["name"]
-        assert valid_description == added_collaboration_tool["description"]
-        assert valid_visibility == added_collaboration_tool["visibility"]
+        created_task = response.data["createTask"]
+        assert "id" in created_task
+        assert valid_name == created_task["name"]
+        assert valid_description == created_task["description"]
+        assert valid_visibility == created_task["visibility"]
+        assert expected_skills_v2 == created_task["skillsV2"]
+
+    def test_can_create_task_with_skills_v2(self):
+        user = make_user()
+
+        community = make_community(creator=user)
+        skills_v2 = [skill.slug for skill in make_skills(2)]
+
+        create_task_mutation = """
+            mutation test($input: CreateTaskInput!) {
+                createTask(input: $input) {
+                    id
+                    skillsV2
+                }
+            }
+            """
+
+        valid_name = "Valid name"
+        valid_description = "Valid description"
+        valid_visibility = "MEMBERS"
+
+        valid_input = {
+            "spaceId": str(community.id),
+            "name": valid_name,
+            "description": valid_description,
+            "visibility": valid_visibility,
+            "locationType": "HYBRID",
+            "regularity": "REGULARLY",
+            "contactEmail": "email@email.com",
+            "skillsV2": skills_v2,
+            "slotsAvailable": 1,
+            "location": "Hamburg, Germany",
+            "contactPhone": "+49176176176176",
+        }
+
+        response = async_to_sync(schema.execute)(
+            create_task_mutation,
+            context_value=benedict({"request": {"user": user}}),
+            variable_values={"input": valid_input},
+        )
+
+        assert response.errors is None
+
+        created_task = response.data["createTask"]
+        assert skills_v2 == created_task["skillsV2"]
 
     def test_can_create_members_only_task_if_collaborator(self):
         user = make_user()
@@ -1171,9 +1234,7 @@ class TestTaskRecommendations:
             assert filter_response.data["recommendedTasks"]["totalResults"] == total_results
 
     def test_filters_for_recommended_tasks(self):
-        skill1 = make_skill()
-        skill2 = make_skill()
-        other_skill = make_skill()
+        [skill1, skill2, other_skill] = make_skills(3)
 
         # locations inside geolocation filter
         geolocation1 = geos.Point(8.5541567, 53.5101827, srid=4326)
@@ -1224,6 +1285,7 @@ class TestTaskRecommendations:
 
         self.user.profile.skills.add(skill1)
         self.user.profile.skills.add(skill2)
+        self.user.profile.skills_v2 = [skill1.slug, skill2.slug]
         self.user.profile.geolocation = geos.GEOSGeometry(json.dumps(geolocation_geometry))
 
         self.assert_filtered_tasks(
@@ -1247,9 +1309,9 @@ class TestTaskRecommendations:
         )
 
     def test_filters_for_recommended_tasks_paging(self):
-        skill1 = make_skill()
-        other_skill = make_skill()
+        [skill1, other_skill] = make_skills(2)
         self.user.profile.skills.add(skill1)
+        self.user.profile.skills_v2 = [skill1.slug]
         self.user.profile.geolocation = geos.GEOSGeometry(json.dumps(geolocation_geometry))
 
         # locations inside geolocation filter
diff --git a/openbook_communities/tests/test_space_event_tracking.py b/openbook_communities/tests/test_space_event_tracking.py
index bb66df21881fe6ae5cfb662a9db378afa0928c0c..2786a8533831de03f705dc74bab70ddc945cbf1f 100644
--- a/openbook_communities/tests/test_space_event_tracking.py
+++ b/openbook_communities/tests/test_space_event_tracking.py
@@ -12,11 +12,11 @@ from openbook.graphql_schema import schema
 from openbook_common.tracking import initialize_tracking, PostHogConfig, TrackingEvent
 from openbook_common.tests.helpers import (
     make_user,
-    make_topic,
     make_community as make_space,
     make_skill,
     make_community_task,
     simple_jpeg_bytes,
+    make_topics,
 )
 from openbook_communities.tests.helpers import mutate_update_space_user_connection_data
 from openbook_communities.models import SpaceUserConnectionType
@@ -194,7 +194,7 @@ class TestSpaceEvents(TestCase):
         input_create_space = {
             "title": "valid",
             "description": "valid",
-            "topics": [str(make_topic().id) for _ in range(2)],
+            "topics": [str(t.id) for t in make_topics(2)],
             "cover": (SimpleUploadedFile(name="cover.jpg", content=simple_jpeg_bytes)),
         }
         mock_track.reset_mock()
diff --git a/openbook_insights/admin.py b/openbook_insights/admin.py
index 2a5b70a7d91dfbd540ff7a9ad1e657823f28e4df..e516f05e4389ae5dd0587debdd733b7dc540973e 100644
--- a/openbook_insights/admin.py
+++ b/openbook_insights/admin.py
@@ -1,4 +1,7 @@
 from django.contrib import admin
+from django.forms import Select
+from typing import get_args
+from openbook_common.topics import TopicV2
 
 from openbook_insights.models import Discussion, Initiative, Insight, Quote, Recommendation
 
@@ -41,6 +44,13 @@ class InsightAdmin(admin.ModelAdmin):
 
     inlines = [InsightRecommendationInline, InsightQuoteInline, InsightInitiativeInline, DiscussionInline]
 
+    # Override the formfield_for_dbfield to customize the topic_v2 field widget
+    def formfield_for_dbfield(self, db_field, request, **kwargs):
+        if db_field.name == "topic_v2":
+            kwargs["widget"] = Select(choices=[("", "---------")] + list(zip(get_args(TopicV2), get_args(TopicV2))))
+            kwargs["required"] = False
+        return super().formfield_for_dbfield(db_field, request, **kwargs)
+
     @admin.display(description="Description Preview")
     def description_preview(self, obj):
         if (len(obj.description)) > 100:
diff --git a/openbook_insights/migrations/0020_insight_topic_v2.py b/openbook_insights/migrations/0020_insight_topic_v2.py
new file mode 100644
index 0000000000000000000000000000000000000000..868d3dc0fcac5530ad405b322da4103c6c03a2e7
--- /dev/null
+++ b/openbook_insights/migrations/0020_insight_topic_v2.py
@@ -0,0 +1,28 @@
+# Generated by Django 5.0.12 on 2025-02-28 09:08
+from django.db import migrations, models
+
+from openbook_common.topics import slug_to_topic_v2
+
+
+def migrate_topics(apps, schema_editor):
+    Insight = apps.get_model("openbook_insights", "Insight")
+
+    for insight in Insight.objects.all():
+        if insight.topic is not None:
+            insight.topic_v2 = slug_to_topic_v2(insight.topic.slug)
+            insight.save()
+
+
+class Migration(migrations.Migration):
+    dependencies = [
+        ("openbook_insights", "0019_insight_polls"),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name="insight",
+            name="topic_v2",
+            field=models.CharField(blank=True, max_length=128, null=True),
+        ),
+        migrations.RunPython(migrate_topics),
+    ]
diff --git a/openbook_insights/migrations/0021_alter_insight_topic_v2.py b/openbook_insights/migrations/0021_alter_insight_topic_v2.py
new file mode 100644
index 0000000000000000000000000000000000000000..5949f131fbd39c9ecb1dbb3706bcb206f492f277
--- /dev/null
+++ b/openbook_insights/migrations/0021_alter_insight_topic_v2.py
@@ -0,0 +1,18 @@
+# Generated by Django 5.0.12 on 2025-02-28 15:52
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ("openbook_insights", "0020_insight_topic_v2"),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name="insight",
+            name="topic_v2",
+            field=models.CharField(blank=True, db_index=True, max_length=128, null=True),
+        ),
+    ]
diff --git a/openbook_insights/migrations/tests/test_0020_insight_topic_v2.py b/openbook_insights/migrations/tests/test_0020_insight_topic_v2.py
new file mode 100644
index 0000000000000000000000000000000000000000..8eeaac48dbdee285c8e23bc7b716df51a163ba37
--- /dev/null
+++ b/openbook_insights/migrations/tests/test_0020_insight_topic_v2.py
@@ -0,0 +1,26 @@
+from importlib import import_module
+import pytest
+from django.apps import apps
+from django.db import connection
+from openbook_insights.models import Insight
+from openbook_common.tests.helpers import make_insight, make_topic
+
+# Required because of file name starting with a number
+migration = import_module("openbook_insights.migrations.0020_insight_topic_v2")
+
+
+@pytest.mark.django_db
+class TestMigrateInsightTopic:
+    def test_migrate_insight_topics(self):
+        # GIVEN an insight with only V1 topic
+        topic_v1 = make_topic(slug="eco-tourism")
+        insight = make_insight()
+        insight.topic = topic_v1
+        insight.save()
+
+        # WHEN migrating insight topics from V1 to V2
+        migration.migrate_topics(apps, connection.schema_editor())
+
+        # THEN the insight has a mapped V2 topic
+        updated_insight = Insight.objects.get(id=insight.id)
+        assert updated_insight.topic_v2 == "lifestyle-consumption"
diff --git a/openbook_insights/models/insight.py b/openbook_insights/models/insight.py
index cfcbb4633aa079ac331816645914485961f1e158..c152d12dffcc2f8b361e433d574283b9eddebcc2 100644
--- a/openbook_insights/models/insight.py
+++ b/openbook_insights/models/insight.py
@@ -59,11 +59,10 @@ class Insight(ModelWithUUID):
         blank=True,
         null=True,
     )
+    topic_v2 = models.CharField(max_length=settings.TERM_SLUG_MAX_LENGTH, blank=True, null=True, db_index=True)
 
     class Meta:
-        indexes = [
-            models.Index(fields=["-date_published"]),
-        ]
+        indexes = [models.Index(fields=["-date_published"])]
 
     def save(self, *args, **kwargs):
         # Attempt to delete the cache matching the pattern
diff --git a/openbook_insights/schema/types/insight.py b/openbook_insights/schema/types/insight.py
index 156868a271b4f8d82f67398c0e75ad9faf859326..e05b656b8bf3550d13ccb06cbb0ee608bc232d43 100644
--- a/openbook_insights/schema/types/insight.py
+++ b/openbook_insights/schema/types/insight.py
@@ -35,7 +35,10 @@ class Insight:
     date_published: strawberry.auto
     header_image: Optional[str]
     header_image_blurhash: Optional[str]
-    topic: Optional[Topic]
+    topic: Optional[Topic] = strawberry_django.field(
+        deprecation_reason="Deprecated since version 1.52, use topic_v2 instead.", default=None
+    )
+    topic_v2: Optional[str] = None
 
     @strawberry_django.field()
     def posts(self, info) -> List[Post]:
diff --git a/openbook_terms/helpers.py b/openbook_terms/helpers.py
new file mode 100644
index 0000000000000000000000000000000000000000..056d21e8a1ab05ac0dda0a6346a6453e28b214e3
--- /dev/null
+++ b/openbook_terms/helpers.py
@@ -0,0 +1,12 @@
+from typing import Iterable, List
+from openbook_terms.models import Topic, Skill
+from openbook_common.topics import slugs_to_topic_v2_slugs
+from openbook_common.skills import slugs_to_skill_v2_slugs
+
+
+def topics_to_topic_v2_slugs(topics: Iterable[Topic]) -> List[str]:
+    return slugs_to_topic_v2_slugs([topic.slug for topic in topics])
+
+
+def skills_to_skill_v2_slugs(skills: Iterable[Skill]) -> List[str]:
+    return slugs_to_skill_v2_slugs([skill.slug for skill in skills])
diff --git a/openbook_terms/schema/queries.py b/openbook_terms/schema/queries.py
index 5268fa65ca2abad9f4460f45d398b11e330afb6a..727ac9fd8cd9b4ae6b099aedc998b5fbef065e1d 100644
--- a/openbook_terms/schema/queries.py
+++ b/openbook_terms/schema/queries.py
@@ -13,7 +13,9 @@ from openbook_terms.schema.types import SDG, Skill, Topic, FeedPostCategory
 
 @strawberry.type
 class Query:
-    @strawberry_django.field()
+    @strawberry_django.field(
+        deprecation_reason="Deprecated since 1.52. Use V2 instead, which is now a static hard coded set of slugs."
+    )
     def topics(self, offset: int = 0, limit: int = 50) -> Paged[Topic]:
         return Paged.of(TopicModel.objects.all().order_by("title"), offset, limit)
 
@@ -21,7 +23,9 @@ class Query:
     def sdgs(self, offset: int = 0, limit: int = 50) -> Paged[SDG]:
         return Paged.of(SDG_MODEL.objects.all().order_by("number"), offset, limit)
 
-    @strawberry_django.field()
+    @strawberry_django.field(
+        deprecation_reason="Deprecated since 1.52. Use V2 instead, which is now a static hard coded set of slugs."
+    )
     def skills(self, offset: int = 0, limit: int = 50) -> Paged[Skill]:
         return Paged.of(SkillModel.objects.all().order_by("title"), offset, limit)
 
diff --git a/requirements.txt b/requirements.txt
index 47cb699b27f70a9515b2ddaaef73291067815d8b..42d0534515ebc0a31a519ddbf4a3bd6d1987ca14 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -7,13 +7,13 @@
 # Afterwards, some packages might need a downgrade or version tweak for the bundle fitting together and fitting the code
 adrf==0.1.9
 aiofiles==24.1.0
-aiohappyeyeballs==2.5.0
+aiohappyeyeballs==2.6.1
 aiohttp==3.11.13
 aiosignal==1.3.2
 ASGIMiddlewareStaticFile==0.6.1
 asgiref==3.8.1
 async-property==0.2.2
-attrs==25.1.0
+attrs==25.2.0
 backoff==2.2.1
 beautifulsoup4==4.13.3
 black==25.1.0
@@ -25,7 +25,7 @@ charset-normalizer==3.4.1
 colorama==0.4.6
 coverage==7.6.12
 Deprecated==1.2.18
-Django==5.0.12
+Django==5.0.13
 django-admin-rangefilter==0.13.2
 django-appconf==1.1.0
 django-cacheops==7.1
@@ -49,15 +49,15 @@ Faker==12.0.1 # mixer 7.2.2 depends on Faker<12.1 and >=5.4.0
 filelock==3.17.0
 frozenlist==1.5.0
 funcy==2.0
-google-api-core==2.24.1
+google-api-core==2.24.2
 google-auth==2.38.0
 google-cloud-pubsub==2.28.0
 google-cloud-webrisk==1.17.0
-googleapis-common-protos==1.69.0
+googleapis-common-protos==1.69.1
 graphql-core==3.2.6
 grpc-google-iam-v1==0.14.1
-grpcio==1.70.0
-grpcio-status==1.70.0
+grpcio==1.71.0
+grpcio-status==1.71.0
 h11==0.14.0
 hiredis==3.1.0
 icalendar==6.1.1
@@ -83,9 +83,9 @@ pilkit==3.0
 pillow==11.1.0
 platformdirs==4.3.6
 pluggy==1.5.0
-posthog==3.18.1
+posthog==3.19.1
 propcache==0.3.0
-proto-plus==1.26.0
+proto-plus==1.26.1
 protobuf==5.29.3
 psycopg==3.2.5
 psycopg-binary==3.2.5
@@ -115,7 +115,7 @@ rq==2.1.0
 rsa==4.9
 ruamel.yaml==0.18.10
 ruamel.yaml.clib==0.2.12
-ruff==0.9.9
+ruff==0.9.10
 sentry-sdk==2.22.0
 six==1.17.0
 soupsieve==2.6
@@ -123,7 +123,7 @@ spinners==0.0.24
 sqlparse==0.5.3
 strawberry-graphql==0.248.1
 strawberry-graphql-django==0.53.3
-structlog==25.1.0
+structlog==25.2.0
 termcolor==2.5.0
 text-unidecode==1.3
 tldextract==5.1.3
diff --git a/smoketest/main.js b/smoketest/main.js
index eb881fdb4ae7e1adfae3d246348f48f4248b133f..bbabce76b8eea96d5e75911dca86c891ae9040db 100644
--- a/smoketest/main.js
+++ b/smoketest/main.js
@@ -27,14 +27,14 @@ function forQuery(query, checkFunction) {
 
 // Define your smoketest(s) here.
 export default () => {
-  forQuery(`query{topics{totalResults}}`, (response) => {
+  forQuery('query{discoverSpacesDefaultTopicsV2}', (response) => {
     check(response, {
       'is status 200': (r) => r.status === 200,
     })
     check(JSON.parse(response.body), {
       // there can be multiple tests here, e.g.
       //"contains topics object": (r) => typeof r.data.topics != null,
-      'contains totalResults count': (r) => typeof r.data.topics.totalResults == 'number',
+      'contains topics V2': (r) => r.data.discoverSpacesDefaultTopicsV2.length > 1,
     })
   })
 }
diff --git a/terraform/common/init.tf b/terraform/common/init.tf
index 1c76b767fcd176e75b421ac88b0aaf5f003f4064..7b0a76b7346d88955a05608344f0bf193147cee4 100644
--- a/terraform/common/init.tf
+++ b/terraform/common/init.tf
@@ -4,11 +4,11 @@ terraform {
   required_providers {
     google = {
       source  = "hashicorp/google"
-      version = "6.24.0"
+      version = "6.25.0"
     }
     google-beta = {
       source  = "hashicorp/google-beta"
-      version = "6.24.0"
+      version = "6.25.0"
     }
   }
   backend "gcs" {
diff --git a/terraform/environments/deployment.tf b/terraform/environments/deployment.tf
index 53fd24a23bf8ce52d65df051b1b4650cb0aacb97..fa824a77d0cc5b8eae8031da157d02dca9d367b6 100644
--- a/terraform/environments/deployment.tf
+++ b/terraform/environments/deployment.tf
@@ -57,6 +57,7 @@ resource "google_cloud_run_v2_job" "okuna_migration" {
     task_count = 1
     template {
       max_retries     = 1
+      timeout         = "3600s" # 1 hour
       service_account = data.terraform_remote_state.holi_okuna_common_state.outputs.cloud_run_service_account_email
       containers {
         image   = "${data.terraform_remote_state.holi_infra_state.outputs.artifact_registry_location}/holi-okuna:${var.image_tag}"
diff --git a/terraform/environments/init.tf b/terraform/environments/init.tf
index 85e6590c4d2906610cf0fa111d4a73ae5b62962f..c2c50d23912822210aa7fa27be48a4e680671531 100644
--- a/terraform/environments/init.tf
+++ b/terraform/environments/init.tf
@@ -4,11 +4,11 @@ terraform {
   required_providers {
     google = {
       source  = "hashicorp/google"
-      version = "6.24.0"
+      version = "6.25.0"
     }
     google-beta = {
       source  = "hashicorp/google-beta"
-      version = "6.24.0"
+      version = "6.25.0"
     }
   }
   backend "gcs" {