diff --git a/backend/apps/api/__init__.py b/backend/apps/api/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/backend/apps/api/admin.py b/backend/apps/api/admin.py
new file mode 100644
index 00000000..8c38f3f3
--- /dev/null
+++ b/backend/apps/api/admin.py
@@ -0,0 +1,3 @@
+from django.contrib import admin
+
+# Register your models here.
diff --git a/backend/apps/api/apps.py b/backend/apps/api/apps.py
new file mode 100644
index 00000000..ae752015
--- /dev/null
+++ b/backend/apps/api/apps.py
@@ -0,0 +1,6 @@
+from django.apps import AppConfig
+
+
+class ApiConfig(AppConfig):
+    default_auto_field = 'django.db.models.BigAutoField'
+    name = 'apps.api'
diff --git a/backend/apps/api/migrations/__init__.py b/backend/apps/api/migrations/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/backend/apps/api/models.py b/backend/apps/api/models.py
new file mode 100644
index 00000000..71a83623
--- /dev/null
+++ b/backend/apps/api/models.py
@@ -0,0 +1,3 @@
+from django.db import models
+
+# Create your models here.
diff --git a/backend/apps/api/v1/__init__.py b/backend/apps/api/v1/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/backend/apps/api/v1/urls.py b/backend/apps/api/v1/urls.py
new file mode 100644
index 00000000..caccf995
--- /dev/null
+++ b/backend/apps/api/v1/urls.py
@@ -0,0 +1,23 @@
+from django.urls import include, path
+from rest_framework_simplejwt.views import (
+    TokenObtainPairView,
+    TokenRefreshView,
+    TokenVerifyView,
+)
+
+urlpatterns = [
+    path('users/', include('apps.user.api.v1.urls')),
+    path('quiblets/', include('apps.quiblet.api.v1.urls')),
+    path('quibs/', include('apps.quib.api.v1.urls')),
+    # jwt auth
+    path(
+        'auth/',
+        include(
+            [
+                path('token/', TokenObtainPairView.as_view(), name='token_obtain_pair'),
+                path('token/refresh/', TokenRefreshView.as_view(), name='token_refresh'),
+                path('token/verify/', TokenVerifyView.as_view(), name='token_verify'),
+            ]
+        ),
+    ),
+]
diff --git a/backend/apps/quib/migrations/0001_initial.py b/backend/apps/quib/migrations/0001_initial.py
index 59a59197..9996a0c0 100644
--- a/backend/apps/quib/migrations/0001_initial.py
+++ b/backend/apps/quib/migrations/0001_initial.py
@@ -1,6 +1,5 @@
-# Generated by Django 5.1.3 on 2024-12-07 04:12
+# Generated by Django 5.1.4 on 2024-12-11 02:49
 
-import django.db.models.deletion
 import shortuuid.django_fields
 from django.db import migrations, models
 
@@ -9,10 +8,7 @@ class Migration(migrations.Migration):
 
     initial = True
 
-    dependencies = [
-        ('quiblet', '0003_delete_quib'),
-        ('user', '0001_initial'),
-    ]
+    dependencies = []
 
     operations = [
         migrations.CreateModel(
@@ -44,42 +40,6 @@ class Migration(migrations.Migration):
                     ),
                 ),
                 ('content', models.TextField(verbose_name='content')),
-                (
-                    'dislikes',
-                    models.ManyToManyField(
-                        blank=True,
-                        related_name='disliked_quibs',
-                        to='user.profile',
-                        verbose_name='dislikes',
-                    ),
-                ),
-                (
-                    'likes',
-                    models.ManyToManyField(
-                        blank=True,
-                        related_name='liked_quibs',
-                        to='user.profile',
-                        verbose_name='likes',
-                    ),
-                ),
-                (
-                    'quibber',
-                    models.ForeignKey(
-                        on_delete=django.db.models.deletion.CASCADE,
-                        related_name='quibs',
-                        to='user.profile',
-                        verbose_name='quibber',
-                    ),
-                ),
-                (
-                    'quiblet',
-                    models.ForeignKey(
-                        on_delete=django.db.models.deletion.CASCADE,
-                        related_name='quibs',
-                        to='quiblet.quiblet',
-                        verbose_name='quiblet',
-                    ),
-                ),
             ],
             options={
                 'verbose_name': 'Quib',
diff --git a/backend/apps/quib/migrations/0002_initial.py b/backend/apps/quib/migrations/0002_initial.py
new file mode 100644
index 00000000..46143a2b
--- /dev/null
+++ b/backend/apps/quib/migrations/0002_initial.py
@@ -0,0 +1,59 @@
+# Generated by Django 5.1.4 on 2024-12-11 02:49
+
+import django.db.models.deletion
+from django.conf import settings
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    initial = True
+
+    dependencies = [
+        ('quib', '0001_initial'),
+        ('quiblet', '0001_initial'),
+        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='quib',
+            name='dislikes',
+            field=models.ManyToManyField(
+                blank=True,
+                related_name='disliked_quibs',
+                to=settings.AUTH_USER_MODEL,
+                verbose_name='dislikes',
+            ),
+        ),
+        migrations.AddField(
+            model_name='quib',
+            name='likes',
+            field=models.ManyToManyField(
+                blank=True,
+                related_name='liked_quibs',
+                to=settings.AUTH_USER_MODEL,
+                verbose_name='likes',
+            ),
+        ),
+        migrations.AddField(
+            model_name='quib',
+            name='quibber',
+            field=models.ForeignKey(
+                on_delete=django.db.models.deletion.CASCADE,
+                related_name='quibs',
+                to=settings.AUTH_USER_MODEL,
+                verbose_name='quibber',
+            ),
+        ),
+        migrations.AddField(
+            model_name='quib',
+            name='quiblet',
+            field=models.ForeignKey(
+                on_delete=django.db.models.deletion.CASCADE,
+                related_name='quibs',
+                to='quiblet.quiblet',
+                verbose_name='quiblet',
+            ),
+        ),
+    ]
diff --git a/backend/apps/quib/models.py b/backend/apps/quib/models.py
index 07924f15..56f2faad 100644
--- a/backend/apps/quib/models.py
+++ b/backend/apps/quib/models.py
@@ -3,7 +3,7 @@
 from django.utils.translation import gettext_lazy as _
 
 from apps.quiblet.models import Quiblet
-from apps.user.models import Profile
+from apps.user.models import CustomUser
 from common.mixins import CreatedAtMixin, IsPublicMixin, ShortUUIDIdMixin
 
 
@@ -15,7 +15,7 @@ class Quib(CreatedAtMixin, IsPublicMixin, ShortUUIDIdMixin):
         on_delete=models.CASCADE,
     )
     quibber = models.ForeignKey(
-        Profile,
+        CustomUser,
         related_name='quibs',
         verbose_name=_('quibber'),
         on_delete=models.CASCADE,
@@ -24,10 +24,10 @@ class Quib(CreatedAtMixin, IsPublicMixin, ShortUUIDIdMixin):
     slug = models.SlugField(_('slug'), editable=False, max_length=25, blank=True)
     content = models.TextField(_('content'))
     likes = models.ManyToManyField(
-        Profile, related_name='liked_quibs', blank=True, verbose_name=_('likes')
+        CustomUser, related_name='liked_quibs', blank=True, verbose_name=_('likes')
     )
     dislikes = models.ManyToManyField(
-        Profile, related_name='disliked_quibs', blank=True, verbose_name=_('dislikes')
+        CustomUser, related_name='disliked_quibs', blank=True, verbose_name=_('dislikes')
     )
 
     def save(self, *args, **kwargs):
diff --git a/backend/apps/quiblet/migrations/0001_initial.py b/backend/apps/quiblet/migrations/0001_initial.py
index ef76e101..9b5d997f 100644
--- a/backend/apps/quiblet/migrations/0001_initial.py
+++ b/backend/apps/quiblet/migrations/0001_initial.py
@@ -1,4 +1,4 @@
-# Generated by Django 5.1.3 on 2024-12-06 15:53
+# Generated by Django 5.1.4 on 2024-12-11 02:49
 
 import dynamic_filenames
 import shortuuid.django_fields
@@ -12,42 +12,6 @@ class Migration(migrations.Migration):
     dependencies = []
 
     operations = [
-        migrations.CreateModel(
-            name='Quib',
-            fields=[
-                (
-                    'created_at',
-                    models.DateTimeField(auto_now_add=True, verbose_name='create at'),
-                ),
-                ('is_public', models.BooleanField(default=True, verbose_name='is public')),
-                (
-                    'id',
-                    shortuuid.django_fields.ShortUUIDField(
-                        alphabet='abcdefghijklmnopqrstuvwxyz0123456789',
-                        editable=False,
-                        length=7,
-                        max_length=7,
-                        prefix='',
-                        primary_key=True,
-                        serialize=False,
-                        verbose_name='id',
-                    ),
-                ),
-                ('title', models.CharField(max_length=255, verbose_name='title')),
-                (
-                    'slug',
-                    models.SlugField(
-                        blank=True, editable=False, max_length=25, verbose_name='slug'
-                    ),
-                ),
-                ('content', models.TextField(verbose_name='content')),
-            ],
-            options={
-                'verbose_name': 'Quib',
-                'verbose_name_plural': 'Quibs',
-                'ordering': ['-created_at'],
-            },
-        ),
         migrations.CreateModel(
             name='Quiblet',
             fields=[
diff --git a/backend/apps/quiblet/migrations/0002_initial.py b/backend/apps/quiblet/migrations/0002_initial.py
index 0677f133..6eec4b49 100644
--- a/backend/apps/quiblet/migrations/0002_initial.py
+++ b/backend/apps/quiblet/migrations/0002_initial.py
@@ -1,7 +1,7 @@
-# Generated by Django 5.1.3 on 2024-12-06 15:53
+# Generated by Django 5.1.4 on 2024-12-11 02:49
 
-import django.db.models.deletion
 import django.db.models.functions.text
+from django.conf import settings
 from django.db import migrations, models
 
 
@@ -11,47 +11,17 @@ class Migration(migrations.Migration):
 
     dependencies = [
         ('quiblet', '0001_initial'),
-        ('user', '0001_initial'),
+        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
     ]
 
     operations = [
-        migrations.AddField(
-            model_name='quib',
-            name='dislikes',
-            field=models.ManyToManyField(
-                blank=True,
-                related_name='disliked_quibs',
-                to='user.profile',
-                verbose_name='dislikes',
-            ),
-        ),
-        migrations.AddField(
-            model_name='quib',
-            name='likes',
-            field=models.ManyToManyField(
-                blank=True,
-                related_name='liked_quibs',
-                to='user.profile',
-                verbose_name='likes',
-            ),
-        ),
-        migrations.AddField(
-            model_name='quib',
-            name='quibber',
-            field=models.ForeignKey(
-                on_delete=django.db.models.deletion.CASCADE,
-                related_name='quibs',
-                to='user.profile',
-                verbose_name='quibber',
-            ),
-        ),
         migrations.AddField(
             model_name='quiblet',
             name='members',
             field=models.ManyToManyField(
                 blank=True,
                 related_name='joined_quiblets',
-                to='user.profile',
+                to=settings.AUTH_USER_MODEL,
                 verbose_name='members',
             ),
         ),
@@ -61,20 +31,10 @@ class Migration(migrations.Migration):
             field=models.ManyToManyField(
                 blank=True,
                 related_name='ranged_quiblets',
-                to='user.profile',
+                to=settings.AUTH_USER_MODEL,
                 verbose_name='rangers',
             ),
         ),
-        migrations.AddField(
-            model_name='quib',
-            name='quiblet',
-            field=models.ForeignKey(
-                on_delete=django.db.models.deletion.CASCADE,
-                related_name='quibs',
-                to='quiblet.quiblet',
-                verbose_name='quiblet',
-            ),
-        ),
         migrations.AddConstraint(
             model_name='quiblet',
             constraint=models.UniqueConstraint(
diff --git a/backend/apps/quiblet/migrations/0003_delete_quib.py b/backend/apps/quiblet/migrations/0003_delete_quib.py
deleted file mode 100644
index 30751f1d..00000000
--- a/backend/apps/quiblet/migrations/0003_delete_quib.py
+++ /dev/null
@@ -1,16 +0,0 @@
-# Generated by Django 5.1.3 on 2024-12-07 04:11
-
-from django.db import migrations
-
-
-class Migration(migrations.Migration):
-
-    dependencies = [
-        ('quiblet', '0002_initial'),
-    ]
-
-    operations = [
-        migrations.DeleteModel(
-            name='Quib',
-        ),
-    ]
diff --git a/backend/apps/quiblet/models.py b/backend/apps/quiblet/models.py
index b11ff5a7..d9bcd821 100644
--- a/backend/apps/quiblet/models.py
+++ b/backend/apps/quiblet/models.py
@@ -4,7 +4,7 @@
 from django.utils.translation import gettext_lazy as _
 from dynamic_filenames import FilePattern
 
-from apps.user.models import Profile
+from apps.user.models import CustomUser
 from common.mixins import AvatarMixin, CreatedAtMixin, IsPublicMixin, ShortUUIDIdMixin
 
 
@@ -18,10 +18,10 @@ class Quiblet(AvatarMixin, CreatedAtMixin, IsPublicMixin, ShortUUIDIdMixin):
         null=True,
     )
     members = models.ManyToManyField(
-        Profile, related_name='joined_quiblets', blank=True, verbose_name=_('members')
+        CustomUser, related_name='joined_quiblets', blank=True, verbose_name=_('members')
     )
     rangers = models.ManyToManyField(
-        Profile, related_name='ranged_quiblets', blank=True, verbose_name=_('rangers')
+        CustomUser, related_name='ranged_quiblets', blank=True, verbose_name=_('rangers')
     )
 
     class Meta:  # type: ignore
diff --git a/backend/apps/user/admin.py b/backend/apps/user/admin.py
index 3c9575c6..0c567695 100644
--- a/backend/apps/user/admin.py
+++ b/backend/apps/user/admin.py
@@ -2,55 +2,27 @@
 from django.contrib.auth.admin import UserAdmin
 from django.utils.translation import gettext_lazy as _
 
-from .forms import CustomUserAdminForm, ProfileAdminForm
-from .models import Profile, User
+from .models import CustomUser
 
 
-@admin.register(User)
+@admin.register(CustomUser)
 class CustomUserAdmin(UserAdmin):
-    # form = CustomUserAdminForm
-    add_form = CustomUserAdminForm
-
     fieldsets = (
-        (None, {'fields': ('email', 'password')}),
+        (None, {'fields': ('username', 'email', 'password')}),
+        (_('Personal info'), {'fields': ('first_name', 'last_name')}),
         (
-            _('permissions'),
+            _('Permissions'),
             {
-                'fields': ('is_active', 'is_staff', 'is_superuser'),
-            },  # , 'groups', 'user_permissions')},
-        ),
-        (_('important dates'), {'fields': ('date_joined',)}),
-    )
-
-    add_fieldsets = (
-        (
-            None,
-            {
-                'classes': ('wide',),
-                'fields': ('email', 'password', 'is_active', 'is_staff', 'is_superuser'),
+                'fields': (
+                    'is_active',
+                    'is_staff',
+                    'is_superuser',
+                    'groups',
+                    'user_permissions',
+                ),
             },
         ),
+        (_('Important dates'), {'fields': ('last_login', 'date_joined')}),
     )
 
-    readonly_fields = ('date_joined',)
-    list_display = ('email', 'is_active', 'is_staff', 'is_superuser', 'date_joined')
-    search_fields = ('email',)
-    ordering = ('email',)
-
-
-@admin.register(Profile)
-class ProfileAdmin(admin.ModelAdmin):
-    form = ProfileAdminForm
-
-    fieldsets = (
-        (
-            None,
-            {'fields': ('user', 'username', 'color', 'avatar', 'first_name', 'last_name')},
-        ),
-        (_('important dates'), {'fields': ('created_at',)}),
-    )
-
-    list_display = ('username', 'user__email', 'created_at')
-    search_fields = ('username', 'user__email')
-    ordering = ('-created_at',)
-    readonly_fields = ('created_at',)
+    readonly_fields = ('date_joined', 'last_login')
diff --git a/backend/apps/user/api/v1/serializers.py b/backend/apps/user/api/v1/serializers.py
index feb6a2e3..188c7ab5 100644
--- a/backend/apps/user/api/v1/serializers.py
+++ b/backend/apps/user/api/v1/serializers.py
@@ -1,33 +1,15 @@
 from rest_framework import serializers
 
-from apps.user.models import Profile, User
+from apps.user.models import CustomUser
 
 
 class UserSerializer(serializers.ModelSerializer):
     class Meta:
-        model = User
-        fields = ('id', 'email', 'password', 'date_joined')
+        model = CustomUser
+        fields = '__all__'
         read_only_fields = ('date_joined',)
         extra_kwargs = {'password': {'write_only': True}}
 
     def create(self, validated_data):
-        user = User.objects.create_user(**validated_data)  # type: ignore
+        user = CustomUser.objects.create_user(**validated_data)  # type: ignore
         return user
-
-
-class ProfileSerializer(serializers.ModelSerializer):
-    user = UserSerializer(read_only=True)
-
-    class Meta:
-        model = Profile
-        fields = '__all__'
-
-
-class AuthSerializer(serializers.ModelSerializer):
-    class Meta:
-        model = User
-        fields = ('email', 'password')
-
-
-class AuthTokenResponseSerializer(serializers.Serializer):
-    token = serializers.CharField()
diff --git a/backend/apps/user/api/v1/urls.py b/backend/apps/user/api/v1/urls.py
index bae1e891..9eef25c3 100644
--- a/backend/apps/user/api/v1/urls.py
+++ b/backend/apps/user/api/v1/urls.py
@@ -1,28 +1,19 @@
 from django.urls import include, path
-from rest_framework.routers import DefaultRouter
 
-from .views import LoginAPIView, LogoutAPIView, MeAPIView, RegisterAPIView
-from .viewsets import MyProfilesViewSet, ProfileViewSet
-
-router = DefaultRouter()
-router.register(r'profiles', ProfileViewSet)
-router.register(r'me/profiles', MyProfilesViewSet, basename='me-profile')
+# from .views import LoginAPIView, LogoutAPIView, MeAPIView, RegisterAPIView
 
 urlpatterns = [
     # auth endpoints
-    path(
-        'auth/',
-        include(
-            [
-                path('login/', LoginAPIView.as_view(), name='login'),
-                path('logout/', LogoutAPIView.as_view(), name='logout'),
-                path('register/', RegisterAPIView.as_view(), name='register'),
-            ]
-        ),
-    ),
-    # user view of requested user
-    path('me/', MeAPIView.as_view(), name='me'),
+    # path(
+    #     'auth/',
+    #     include(
+    #         [
+    #             path('login/', LoginAPIView.as_view(), name='login'),
+    #             path('logout/', LogoutAPIView.as_view(), name='logout'),
+    #             path('register/', RegisterAPIView.as_view(), name='register'),
+    #         ]
+    #     ),
+    # ),
+    # # user view of requested user
+    # path('me/', MeAPIView.as_view(), name='me'),
 ]
-
-# router urls should be placed last to prevent overriding
-urlpatterns += router.urls
diff --git a/backend/apps/user/api/v1/views.py b/backend/apps/user/api/v1/views.py
deleted file mode 100644
index f2bdc479..00000000
--- a/backend/apps/user/api/v1/views.py
+++ /dev/null
@@ -1,78 +0,0 @@
-from django.contrib.auth import authenticate
-from drf_spectacular.utils import extend_schema
-from rest_framework import exceptions, generics, permissions, views
-from rest_framework.authtoken.models import Token
-from rest_framework.response import Response
-
-from common.api.exceptions import ServerError
-from common.api.serializers import DetailResponseSerializer
-
-from .serializers import AuthSerializer, AuthTokenResponseSerializer, ProfileSerializer
-
-
-class LoginAPIView(views.APIView):
-    """
-    Customized drf basic token authentication.
-
-    This view authenticates the user using email and password credentials
-    and issues a token upon successful login.
-    """
-
-    serializer_class = AuthSerializer
-
-    @extend_schema(responses=AuthTokenResponseSerializer)
-    def post(self, request, format=None):
-        user = authenticate(
-            email=request.data.get('email'), password=request.data.get('password')
-        )
-        if user:
-            token, _ = Token.objects.get_or_create(user=user)
-            return Response({'token': token.key})
-        else:
-            raise exceptions.AuthenticationFailed()
-
-
-class LogoutAPIView(views.APIView):
-    """
-    View to handle user logout by deleting the authentication token.
-    """
-
-    permission_classes = (permissions.IsAuthenticated,)
-
-    @extend_schema(request=None, responses=DetailResponseSerializer)
-    def post(self, request, format=None):
-        try:
-            Token.objects.filter(user=request.user).delete()
-            return Response({'detail': 'Successfully logged out.'})
-
-        except Exception as e:
-            raise ServerError(f"An error occurred while logging out: {str(e)}")
-
-
-class RegisterAPIView(generics.CreateAPIView):
-    """
-    View to handle registering of new users.
-    """
-
-    serializer_class = AuthSerializer
-
-
-class MeAPIView(views.APIView):
-    """
-    View to retrieve information for the currently authenticated user.
-
-    - `get`: Returns the details of the authenticated user based on their token.
-
-    Permission:
-    - Requires user authentication.
-    """
-
-    permission_classes = (permissions.IsAuthenticated,)
-    serializer_class = ProfileSerializer
-
-    def get(self, request):
-        if request.user_profile:
-            serializer = self.serializer_class(request.user_profile)
-            return Response(serializer.data)
-        else:
-            raise exceptions.ValidationError('A valid profile must be provided.')
diff --git a/backend/apps/user/api/v1/viewsets.py b/backend/apps/user/api/v1/viewsets.py
deleted file mode 100644
index 77080756..00000000
--- a/backend/apps/user/api/v1/viewsets.py
+++ /dev/null
@@ -1,56 +0,0 @@
-from django.conf import settings
-from rest_framework import exceptions, filters, permissions, viewsets
-
-from apps.user.models import Profile
-
-from .serializers import ProfileSerializer
-
-
-class ProfileViewSet(viewsets.ReadOnlyModelViewSet):
-    """
-    ViewSet for performing read-only operations on the Profile model.
-
-    Filtering:
-    - Allows searching profiles by username.
-    """
-
-    queryset = Profile.objects.all()
-    serializer_class = ProfileSerializer
-    filter_backends = (filters.SearchFilter,)
-    search_fields = ('username',)
-
-
-class MyProfilesViewSet(viewsets.ModelViewSet):
-    """
-    ViewSet to manage profiles associated with the authenticated user.
-
-    Permissions:
-    - Requires user authentication to access and modify profiles.
-    """
-
-    permission_classes = (permissions.IsAuthenticated,)
-    serializer_class = ProfileSerializer
-
-    def get_queryset(self):  # type: ignore
-        """
-        Restrict queryset to profiles owned by the currently authenticated user.
-        """
-        # during schema generation
-        if getattr(self, 'swagger_fake_view', False):
-            return Profile.objects.none()
-        user = self.request.user
-        return user.profiles.all()  # type: ignore
-
-    def perform_create(self, serializer):
-        """
-        Create a new profile for the authenticated user, enforcing a maximum limit.
-
-        Raises:
-            ValidationError: If the user already has limited profiles.
-        """
-        user = self.request.user
-        if user.profiles.count() >= settings.PROFILE_LIMIT:  # type: ignore
-            raise exceptions.ValidationError(
-                f'A user cannot have more than {settings.PROFILE_LIMIT} profiles.'
-            )
-        serializer.save(user=user)
diff --git a/backend/apps/user/apps.py b/backend/apps/user/apps.py
index 562f008f..ff7e415e 100644
--- a/backend/apps/user/apps.py
+++ b/backend/apps/user/apps.py
@@ -4,6 +4,3 @@
 class UserConfig(AppConfig):
     default_auto_field = "django.db.models.BigAutoField"
     name = "apps.user"
-
-    def ready(self) -> None:
-        from . import signals  # noqa: F401
diff --git a/backend/apps/user/auth.py b/backend/apps/user/auth.py
deleted file mode 100644
index 025d66f5..00000000
--- a/backend/apps/user/auth.py
+++ /dev/null
@@ -1,35 +0,0 @@
-from rest_framework import exceptions
-from rest_framework.authentication import TokenAuthentication
-
-from .models import Profile
-
-
-class ExtendedTokenAuthentication(TokenAuthentication):
-    """
-    Extended drf TokenAuthentication
-    which includes 'user_profile' field on request
-    """
-
-    keyword = 'Bearer'
-
-    def authenticate(self, request):
-        user_auth_token_tuple = super().authenticate(request)
-        if not user_auth_token_tuple:
-            return None
-
-        user, auth_token = user_auth_token_tuple
-
-        profile_id = request.headers.get('Profile-Id')
-        user_profile = None
-
-        if profile_id:
-            try:
-                user_profile = Profile.objects.get(id=profile_id, user=user)
-            except Profile.DoesNotExist:
-                raise exceptions.PermissionDenied(
-                    'Profile does not exist or does not belong to the authenticated user.'
-                )
-
-        request.user_profile = user_profile
-
-        return (user, auth_token)
diff --git a/backend/apps/user/backends.py b/backend/apps/user/backends.py
deleted file mode 100644
index 0a7f4c90..00000000
--- a/backend/apps/user/backends.py
+++ /dev/null
@@ -1,27 +0,0 @@
-from django.contrib.auth import get_user_model
-from django.contrib.auth.backends import ModelBackend
-
-UserModel = get_user_model()
-
-
-class EmailAuthBackend(ModelBackend):
-    """
-    Custom Auth backend with email instead username
-    """
-
-    def authenticate(  # pyright: ignore [reportIncompatibleMethodOverride]
-        self, request, email=None, password=None, **kwargs
-    ):
-        try:
-            user = UserModel.objects.get(email=email)
-            if password and user.check_password(password):
-                return user
-            return None
-        except UserModel.DoesNotExist:
-            return None
-
-    def get_user(self, user_id: int):
-        try:
-            return UserModel.objects.get(pk=user_id)
-        except UserModel.DoesNotExist:
-            return None
diff --git a/backend/apps/user/forms.py b/backend/apps/user/forms.py
deleted file mode 100644
index 26a5a94f..00000000
--- a/backend/apps/user/forms.py
+++ /dev/null
@@ -1,41 +0,0 @@
-from django import forms
-from django.conf import settings
-from django.forms import ModelForm
-from django.utils.translation import gettext_lazy as _
-
-from .models import Profile, User
-
-
-class CustomUserAdminForm(ModelForm):
-    password = forms.CharField(widget=forms.PasswordInput, required=False)
-
-    class Meta:  # pyright: ignore [reportIncompatibleVariableOverride]
-        model = User
-        fields = '__all__'
-
-    def save(self, commit=True):
-        user = super().save(commit=False)
-        password = self.cleaned_data.get('password')
-        if password:
-            user.set_password(password)
-        if commit:
-            user.save()
-        return user
-
-
-class ProfileAdminForm(ModelForm):
-    class Meta:  # pyright: ignore [reportIncompatibleVariableOverride]
-        model = Profile
-        fields = '__all__'
-
-    def clean(self):  # pyright: ignore [reportIncompatibleVariableOverride]
-        user = self.cleaned_data.get('user')
-
-        if (
-            self.instance.pk is None
-            and user
-            and user.profiles.count() >= settings.PROFILE_LIMIT
-        ):
-            self.add_error(None, _('a user cannot have more than 5 profiles.'))
-
-        return self.cleaned_data
diff --git a/backend/apps/user/managers.py b/backend/apps/user/managers.py
deleted file mode 100644
index 2a52879f..00000000
--- a/backend/apps/user/managers.py
+++ /dev/null
@@ -1,43 +0,0 @@
-from typing import Any
-
-from django.contrib.auth.models import BaseUserManager
-
-
-class CustomUserManager(BaseUserManager):
-    """
-    Custom user manager where email is the unique identifier
-    for authentication instead usernamess.
-    """
-
-    use_in_migrations = True
-
-    def _create_user(self, email: str, password: str | None, **extra_fields: Any):
-        """
-        base function to save user with email and password (if given)
-        """
-        from .models import User  # prevent circular import
-
-        if not email:
-            raise ValueError("Email is required")
-        email = self.normalize_email(email)
-        user: User = self.model(email=email, **extra_fields)
-        user.set_password(password)
-        user.save(using=self._db)
-        return user
-
-    def create_user(self, email: str, password: None = None, **extra_fields: Any):
-        extra_fields.setdefault('is_staff', False)
-        extra_fields.setdefault('is_superuser', False)
-        return self._create_user(email, password, **extra_fields)
-
-    def create_superuser(self, email: str, password: str, **extra_fields: Any):
-        extra_fields.setdefault('is_staff', True)
-        extra_fields.setdefault('is_superuser', True)
-
-        # sanity checking
-        if extra_fields.get('is_staff') is not True:
-            raise ValueError('Superuser must have staff=True')
-        if extra_fields.get('is_superuser') is not True:
-            raise ValueError('Superuser must have superuser=True')
-
-        return self._create_user(email, password, **extra_fields)
diff --git a/backend/apps/user/migrations/0001_initial.py b/backend/apps/user/migrations/0001_initial.py
index 243e656d..01e99d39 100644
--- a/backend/apps/user/migrations/0001_initial.py
+++ b/backend/apps/user/migrations/0001_initial.py
@@ -1,13 +1,13 @@
-# Generated by Django 5.1.3 on 2024-12-06 15:53
+# Generated by Django 5.1.4 on 2024-12-11 02:49
 
 import functools
 
-import django.db.models.deletion
+import django.contrib.auth.models
+import django.contrib.auth.validators
+import django.utils.timezone
 import dynamic_filenames
-from django.conf import settings
 from django.db import migrations, models
 
-import apps.user.managers
 import common.mixins
 
 
@@ -21,7 +21,7 @@ class Migration(migrations.Migration):
 
     operations = [
         migrations.CreateModel(
-            name='User',
+            name='CustomUser',
             fields=[
                 (
                     'id',
@@ -46,14 +46,27 @@ class Migration(migrations.Migration):
                     ),
                 ),
                 (
-                    'email',
-                    models.EmailField(
-                        max_length=254, unique=True, verbose_name='email address'
+                    'username',
+                    models.CharField(
+                        error_messages={
+                            'unique': 'A user with that username already exists.'
+                        },
+                        help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.',
+                        max_length=150,
+                        unique=True,
+                        validators=[
+                            django.contrib.auth.validators.UnicodeUsernameValidator()
+                        ],
+                        verbose_name='username',
                     ),
                 ),
                 (
-                    'date_joined',
-                    models.DateTimeField(auto_now_add=True, verbose_name='date joined'),
+                    'first_name',
+                    models.CharField(blank=True, max_length=150, verbose_name='first name'),
+                ),
+                (
+                    'last_name',
+                    models.CharField(blank=True, max_length=150, verbose_name='last name'),
                 ),
                 (
                     'is_staff',
@@ -72,53 +85,11 @@ class Migration(migrations.Migration):
                     ),
                 ),
                 (
-                    'groups',
-                    models.ManyToManyField(
-                        blank=True,
-                        help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.',
-                        related_name='user_set',
-                        related_query_name='user',
-                        to='auth.group',
-                        verbose_name='groups',
-                    ),
-                ),
-                (
-                    'user_permissions',
-                    models.ManyToManyField(
-                        blank=True,
-                        help_text='Specific permissions for this user.',
-                        related_name='user_set',
-                        related_query_name='user',
-                        to='auth.permission',
-                        verbose_name='user permissions',
-                    ),
-                ),
-            ],
-            options={
-                'verbose_name': 'User',
-                'verbose_name_plural': 'Users',
-                'ordering': ['-date_joined'],
-            },
-            managers=[
-                ('objects', apps.user.managers.CustomUserManager()),
-            ],
-        ),
-        migrations.CreateModel(
-            name='Profile',
-            fields=[
-                (
-                    'id',
-                    models.BigAutoField(
-                        auto_created=True,
-                        primary_key=True,
-                        serialize=False,
-                        verbose_name='ID',
+                    'date_joined',
+                    models.DateTimeField(
+                        default=django.utils.timezone.now, verbose_name='date joined'
                     ),
                 ),
-                (
-                    'created_at',
-                    models.DateTimeField(auto_now_add=True, verbose_name='create at'),
-                ),
                 (
                     'color',
                     models.CharField(
@@ -164,34 +135,43 @@ class Migration(migrations.Migration):
                     ),
                 ),
                 (
-                    'username',
-                    models.CharField(max_length=25, unique=True, verbose_name='username'),
-                ),
-                (
-                    'first_name',
-                    models.CharField(
-                        blank=True, max_length=255, null=True, verbose_name='first name'
+                    'email',
+                    models.EmailField(
+                        error_messages={'unique': 'A user with that email already exists.'},
+                        max_length=254,
+                        unique=True,
+                        verbose_name='Email address',
                     ),
                 ),
+                ('bio', models.TextField(verbose_name='Bio')),
                 (
-                    'last_name',
-                    models.CharField(
-                        blank=True, max_length=255, null=True, verbose_name='last name'
+                    'groups',
+                    models.ManyToManyField(
+                        blank=True,
+                        help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.',
+                        related_name='user_set',
+                        related_query_name='user',
+                        to='auth.group',
+                        verbose_name='groups',
                     ),
                 ),
                 (
-                    'user',
-                    models.ForeignKey(
-                        on_delete=django.db.models.deletion.CASCADE,
-                        related_name='profiles',
-                        to=settings.AUTH_USER_MODEL,
+                    'user_permissions',
+                    models.ManyToManyField(
+                        blank=True,
+                        help_text='Specific permissions for this user.',
+                        related_name='user_set',
+                        related_query_name='user',
+                        to='auth.permission',
+                        verbose_name='user permissions',
                     ),
                 ),
             ],
             options={
-                'verbose_name': 'Profile',
-                'verbose_name_plural': 'Profiles',
-                'ordering': ['-created_at'],
+                'ordering': ['-date_joined'],
             },
+            managers=[
+                ('objects', django.contrib.auth.models.UserManager()),
+            ],
         ),
     ]
diff --git a/backend/apps/user/models.py b/backend/apps/user/models.py
index 24031c11..d3710b1f 100644
--- a/backend/apps/user/models.py
+++ b/backend/apps/user/models.py
@@ -1,49 +1,20 @@
-from django.contrib.auth.models import AbstractBaseUser, PermissionsMixin
+from django.contrib.auth.models import AbstractUser
 from django.db import models
 from django.utils.translation import gettext_lazy as _
 
-from common.mixins import AvatarMixin, ColorMixin, CreatedAtMixin
+from common.mixins import AvatarMixin, ColorMixin
 
-from .managers import CustomUserManager
 
-
-class User(AbstractBaseUser, PermissionsMixin):
-    email = models.EmailField(_('email address'), unique=True)
-    date_joined = models.DateTimeField(_('date joined'), auto_now_add=True)
-    is_staff = models.BooleanField(
-        _("staff status"),
-        default=False,
-        help_text=_("designates whether the user can log into this admin site."),
-    )
-    is_active = models.BooleanField(
-        _("active"),
-        default=True,
-        help_text=_(
-            "designates whether this user should be treated as active. unselect this instead of deleting accounts."
-        ),
+class CustomUser(AbstractUser, ColorMixin, AvatarMixin):
+    email = models.EmailField(
+        _("Email address"),
+        unique=True,
+        error_messages={'unique': _('A user with that email already exists.')},
     )
-
-    objects = CustomUserManager()
-
-    USERNAME_FIELD = 'email'
-    REQUIRED_FIELDS = []
-
-    class Meta:  # type: ignore
-        verbose_name = 'User'
-        verbose_name_plural = 'Users'
-        ordering = ['-date_joined']
-
-
-class Profile(CreatedAtMixin, ColorMixin, AvatarMixin):
-    user = models.ForeignKey(User, related_name='profiles', on_delete=models.CASCADE)
-    username = models.CharField(_('username'), unique=True, max_length=25)
-    first_name = models.CharField(_('first name'), max_length=255, blank=True, null=True)
-    last_name = models.CharField(_('last name'), max_length=255, blank=True, null=True)
+    bio = models.TextField(_('Bio'))
 
     def __str__(self):
         return f"u/{self.username}"
 
     class Meta:  # type: ignore
-        verbose_name = 'Profile'
-        verbose_name_plural = 'Profiles'
-        ordering = ['-created_at']
+        ordering = ['-date_joined']
diff --git a/backend/apps/user/signals.py b/backend/apps/user/signals.py
deleted file mode 100644
index 95214952..00000000
--- a/backend/apps/user/signals.py
+++ /dev/null
@@ -1,11 +0,0 @@
-from django.db.models.signals import post_save
-from django.dispatch import receiver
-
-from .models import Profile, User
-
-
-@receiver(post_save, sender=User)
-def create_profile(sender, instance, created, **kwargs):
-    if created:
-        username = instance.email.split('@')[0]
-        Profile.objects.create(user=instance, username=username)
diff --git a/backend/config/settings.py b/backend/config/settings.py
index 35c4b467..e15e8332 100644
--- a/backend/config/settings.py
+++ b/backend/config/settings.py
@@ -1,4 +1,5 @@
 import os
+from datetime import timedelta
 from pathlib import Path
 
 import dj_database_url
@@ -41,7 +42,9 @@
     'django_extensions',
     # rest framework
     'rest_framework',
-    'rest_framework.authtoken',
+    # jwt auth
+    'rest_framework_simplejwt',
+    'rest_framework_simplejwt.token_blacklist',
     # django filtering
     'django_filters',
     # middleware (cors)
@@ -57,6 +60,7 @@
 
 SELF_APPS = [
     'apps.user',
+    'apps.api',
     'apps.quiblet',
     'apps.quib',
 ]
@@ -72,7 +76,7 @@
 
 REST_FRAMEWORK = {
     'DEFAULT_AUTHENTICATION_CLASSES': [
-        'apps.user.auth.ExtendedTokenAuthentication',
+        'rest_framework_simplejwt.authentication.JWTAuthentication',
     ],
     'DEFAULT_FILTER_BACKENDS': [
         'django_filters.rest_framework.DjangoFilterBackend',
@@ -130,6 +134,15 @@
     ],
 }
 
+# https://django-rest-framework-simplejwt.readthedocs.io/en/latest/settings.html
+
+SIMPLE_JWT = {
+    'ACCESS_TOKEN_LIFETIME': timedelta(minutes=30),
+    'REFRESH_TOKEN_LIFETIME': timedelta(days=30),
+    'BLACKLIST_AFTER_ROTATION': True,
+    'UPDATE_LAST_LOGIN': True,
+}
+
 MIDDLEWARE = [
     'django.middleware.security.SecurityMiddleware',
     'django.contrib.sessions.middleware.SessionMiddleware',
@@ -223,13 +236,7 @@
 DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
 
 # Custom AUTH model and backends
-AUTH_USER_MODEL = 'user.User'
-
-AUTHENTICATION_BACKENDS = [
-    'django.contrib.auth.backends.ModelBackend',
-    # custom auth backend
-    'apps.user.backends.EmailAuthBackend',
-]
+AUTH_USER_MODEL = 'user.CustomUser'
 
 # django-cors-headers settins
 # https://pypi.org/project/django-cors-headers/
@@ -238,9 +245,6 @@
     'http://localhost:5173',
 ]
 
-# max no:of profiles a user can create
-PROFILE_LIMIT = 3
-
 # Static files (CSS, JavaScript, Images)
 # https://docs.djangoproject.com/en/4.2/howto/static-files/
 
diff --git a/backend/config/urls.py b/backend/config/urls.py
index eb852455..1253fe67 100644
--- a/backend/config/urls.py
+++ b/backend/config/urls.py
@@ -16,17 +16,8 @@
 urlpatterns = [
     # admin
     path('admin/', admin.site.urls),
-    # api endpoints
-    path(
-        'api/v1/',
-        include(
-            [
-                path('users/', include('apps.user.api.v1.urls')),
-                path('quiblets/', include('apps.quiblet.api.v1.urls')),
-                path('quibs/', include('apps.quib.api.v1.urls')),
-            ]
-        ),
-    ),
+    # v1 api
+    path('api/v1/', include('apps.api.v1.urls')),
     # openapi
     path('api/v1/schema/', SpectacularAPIView.as_view(api_version='v1'), name='schema'),
     path('api/v1/schema.json', SpectacularJSONAPIView.as_view(), name='schema-json'),
diff --git a/backend/poetry.lock b/backend/poetry.lock
index cd2feabe..544e151d 100644
--- a/backend/poetry.lock
+++ b/backend/poetry.lock
@@ -341,6 +341,30 @@ files = [
 [package.dependencies]
 django = ">=4.2"
 
+[[package]]
+name = "djangorestframework-simplejwt"
+version = "5.3.1"
+description = "A minimal JSON Web Token authentication plugin for Django REST Framework"
+optional = false
+python-versions = ">=3.8"
+files = [
+    {file = "djangorestframework_simplejwt-5.3.1-py3-none-any.whl", hash = "sha256:381bc966aa46913905629d472cd72ad45faa265509764e20ffd440164c88d220"},
+    {file = "djangorestframework_simplejwt-5.3.1.tar.gz", hash = "sha256:6c4bd37537440bc439564ebf7d6085e74c5411485197073f508ebdfa34bc9fae"},
+]
+
+[package.dependencies]
+django = ">=3.2"
+djangorestframework = ">=3.12"
+pyjwt = ">=1.7.1,<3"
+
+[package.extras]
+crypto = ["cryptography (>=3.3.1)"]
+dev = ["Sphinx (>=1.6.5,<2)", "cryptography", "flake8", "freezegun", "ipython", "isort", "pep8", "pytest", "pytest-cov", "pytest-django", "pytest-watch", "pytest-xdist", "python-jose (==3.3.0)", "sphinx_rtd_theme (>=0.1.9)", "tox", "twine", "wheel"]
+doc = ["Sphinx (>=1.6.5,<2)", "sphinx_rtd_theme (>=0.1.9)"]
+lint = ["flake8", "isort", "pep8"]
+python-jose = ["python-jose (==3.3.0)"]
+test = ["cryptography", "freezegun", "pytest", "pytest-cov", "pytest-django", "pytest-xdist", "tox"]
+
 [[package]]
 name = "drf-spectacular"
 version = "0.28.0"
@@ -818,6 +842,23 @@ files = [
     {file = "pyflakes-3.2.0.tar.gz", hash = "sha256:1c61603ff154621fb2a9172037d84dca3500def8c8b630657d1701f026f8af3f"},
 ]
 
+[[package]]
+name = "pyjwt"
+version = "2.10.1"
+description = "JSON Web Token implementation in Python"
+optional = false
+python-versions = ">=3.9"
+files = [
+    {file = "PyJWT-2.10.1-py3-none-any.whl", hash = "sha256:dcdd193e30abefd5debf142f9adfcdd2b58004e644f25406ffaebd50bd98dacb"},
+    {file = "pyjwt-2.10.1.tar.gz", hash = "sha256:3cc5772eb20009233caf06e9d8a0577824723b44e6648ee0a2aedb6cf9381953"},
+]
+
+[package.extras]
+crypto = ["cryptography (>=3.4.0)"]
+dev = ["coverage[toml] (==5.0.4)", "cryptography (>=3.4.0)", "pre-commit", "pytest (>=6.0.0,<7.0.0)", "sphinx", "sphinx-rtd-theme", "zope.interface"]
+docs = ["sphinx", "sphinx-rtd-theme", "zope.interface"]
+tests = ["coverage[toml] (==5.0.4)", "pytest (>=6.0.0,<7.0.0)"]
+
 [[package]]
 name = "pyparsing"
 version = "3.2.0"
@@ -1166,4 +1207,4 @@ files = [
 [metadata]
 lock-version = "2.0"
 python-versions = "^3.12"
-content-hash = "0c685c41c77a9adc7aae3fc5ebafe15cd79544b4b7bdca7ed37a7d0b50fa8601"
+content-hash = "090814607a88f532dd0d8d995354e766e9acbf76a8a173ea7f2ddf02c8efbb36"
diff --git a/backend/pyproject.toml b/backend/pyproject.toml
index e7a5eea7..55cb363a 100644
--- a/backend/pyproject.toml
+++ b/backend/pyproject.toml
@@ -33,6 +33,7 @@ drf-spectacular = {extras = ["sidecar"], version = "^0.28.0"}
 shortuuid = "^1.0.13"
 # 3rd party exception handler
 drf-standardized-errors = {extras = ["openapi"], version = "^0.14.1"}
+djangorestframework-simplejwt = "^5.3.1"
 
 [tool.poetry.group.dev.dependencies]
 # task runner
diff --git a/backend/tests/conftest.py b/backend/tests/conftest.py
index 37a99094..813811de 100644
--- a/backend/tests/conftest.py
+++ b/backend/tests/conftest.py
@@ -1,13 +1,13 @@
 import pytest
 from rest_framework.authtoken.models import Token
 
-from apps.user.models import Profile, User
+from apps.user.models import CustomUser, Profile
 
 
 @pytest.fixture
 def user():
     """Creates and returns a user."""
-    return User.objects.create_user(email='test@test.com', password='testpass')  # type: ignore
+    return CustomUser.objects.create_user(email='test@test.com', password='testpass')  # type: ignore
 
 
 @pytest.fixture