DEV Community

Cover image for Day 87 of #100DaysOfCode — DevCollab: Django Backend Setup and JWT Authentication
M Saad Ahmad
M Saad Ahmad

Posted on

Day 87 of #100DaysOfCode — DevCollab: Django Backend Setup and JWT Authentication

Planning is done. Today, building starts. Day 87 was the most important day of the entire project, not because of the features built, but because of the foundation laid. Every decision made today affects every day that follows. Custom user model, JWT authentication, environment configuration, project structure, all of it had to be right before a single feature could be built.


Why the Setup Day Matters For Today

In Django, two decisions made on day one are painful or impossible to reverse later. The first is the custom user model; Django's migration system ties itself so tightly to the user model that swapping it out after the first migration requires deleting the database and starting over. The second is the overall project structure; apps added or renamed later create messy migration histories and import paths.

Getting both right before running any migrations meant every day after this one could focus on features rather than untangling architectural mistakes.


What gets Installed and Why

The backend dependencies for DevCollab are deliberate; each one earns its place.

Django is the foundation. The ORM, admin panel, and project structure are all Django. No surprise here.

Django REST Framework is what turns Django into an API. Serializers, ViewSets, permission classes, routers, DRF is the layer that makes Django suitable for a separate frontend. Without it, building a clean REST API would mean writing a significant amount of boilerplate manually.

djangorestframework-simplejwt handles JWT token generation, validation, refresh, and blacklisting. JWT is the right authentication approach here because the frontend is a completely separate Next.js application on a different domain. Cookie-based session auth doesn't work cleanly across domains. JWT tokens sent in the Authorization header work everywhere without configuration.

django-cors-headers is required the moment a browser on one domain (the Next.js frontend) tries to make requests to another domain (the Django backend). Without CORS headers configured on the Django side, every single API request from the frontend gets blocked by the browser before it even reaches Django. This is one of those things that isn't optional; it just has to be there.

python-decouple reads environment variables from a .env file. The secret key, database URL, debug flag, and allowed hosts all live in .env and never touch the codebase. This is the right habit from day one, not something to add before deployment.

dj-database-url takes a single database connection string from the environment and converts it into the dictionary format Django's DATABASES setting expects. SQLite locally, PostgreSQL in production, the only thing that changes is the DATABASE_URL value in .env. No code changes required.

Pillow is required because the Profile model has an ImageField for user avatars. Django's ImageField uses Pillow under the hood for image processing and validation. Without it, Django raises an error the moment it loads the model.


Project Structure

After setup, the project looks like this:

devcollab-backend/
│
├── devcollab/                  ← Django project package
│   ├── __init__.py
│   ├── settings.py             ← all configuration
│   ├── urls.py                 ← root URL routing
│   ├── wsgi.py
│   └── asgi.py
│
├── accounts/                   ← handles users, profiles, auth
│   ├── __init__.py
│   ├── admin.py
│   ├── apps.py
│   ├── models.py               ← custom User and Profile models
│   ├── serializers.py          ← RegisterSerializer, ProfileSerializer
│   ├── views.py                ← RegisterView, LoginView, ProfileView
│   ├── urls.py                 ← auth and profile URL patterns
│   ├── signals.py              ← auto-create Profile on User creation
│   └── migrations/
│       └── __init__.py
│
├── projects/                   ← handles projects and collaboration requests
│   ├── __init__.py
│   ├── admin.py
│   ├── apps.py
│   ├── models.py               ← Project and CollaborationRequest models
│   ├── serializers.py
│   ├── views.py
│   ├── urls.py
│   ├── permissions.py          ← IsOwner custom permission
│   └── migrations/
│       └── __init__.py
│
├── .env                        ← secret values, never committed
├── .env.example                ← template showing required keys
├── .gitignore
├── manage.py
└── requirements.txt
Enter fullscreen mode Exit fullscreen mode

Two apps: accounts for everything user-related, projects for everything project and collaboration-related. Clean separation from day one. The projects app stays completely empty today; all the focus is on accounts and getting authentication working.


The Custom User Model Decision

Django ships with a built-in User model that works fine for simple projects. DevCollab needs a custom one for one specific reason: email-based login instead of username-based login. Django's default login uses username. Most modern apps use email. Changing this after the fact is painful.

The custom user model extends AbstractUser, which means it gets everything Django's built-in user has and overrides the login field to use email instead of username. Username is still stored because it's used for public profile URLs.

This model is defined, AUTH_USER_MODEL is set in settings.py, and then migrations are run. That order is the only correct order.


JWT Authentication Setup

JWT authentication in this project works in two layers.

The first layer is djangorestframework-simplejwt, which handles the token mechanics, generating tokens, validating them, refreshing expired access tokens, and blacklisting refresh tokens on logout.

The second layer is custom views built on top of simplejwt. The default simplejwt views take a username and password. DevCollab's login takes an email and password, so a custom LoginView validates the email, finds the user, checks the password, and returns both access and refresh tokens along with the user data the frontend needs.

Registration is a custom view entirely; it creates the User, which triggers the signal to create the Profile, and then returns tokens immediately so the user is logged in right after registering without needing a separate login step.


Settings Configuration

The settings file for a production-bound project looks different from a tutorial. Every sensitive value: SECRET_KEY, DEBUG, DATABASE_URL, ALLOWED_HOSTS, CORS_ALLOWED_ORIGINS, comes from .env via python-decouple. Nothing sensitive is hardcoded.

DRF is configured globally to use JWT as the default authentication class and to require authentication by default. Individual views that should be public override this with permission_classes = [AllowAny]. This is safer than the reverse; defaulting to open and manually protecting each view, because it means a forgotten permission class results in a 401 response rather than exposed data.

CORS is configured to allow requests from http://localhost:3000 in development. The production frontend URL gets added when deploying.


Where Things Stand After Day 87

By the end of today, the backend has a running Django project with two registered apps, a custom user model with email login, working register and login endpoints returning JWT tokens, a token refresh endpoint, a logout endpoint that blacklists the refresh token, and all sensitive configuration in environment variables.

Everything was tested in Postman: register a user, log in, refresh the token, log out, try to use the old refresh token, and confirm it's blacklisted.

The entire authentication foundation is solid. Tomorrow: the rest of the models: Profile signal, Project, CollaborationRequest, and getting all database tables created.

Thanks for reading. Feel free to share your thoughts!

Top comments (0)