A real story of building AskLoop - combining Medium + Dev.to + Stack Overflow with Django 5 + React 18
The Problem I Kept Seeing
I've spent a lot of time on developer platforms. And I kept noticing the same pattern everywhere.
You ask a question - it gets buried.
You share something valuable - it disappears in 48 hours.
A great discussion starts - nobody follows up.
There's always content. But rarely real depth.
There's always activity. But rarely meaningful interaction.
That bothered me enough to build something about it.
So I built AskLoop - a community platform that combines:
- βοΈ Medium-style long-form articles
- β Stack Overflow-style Q&A with voting
- π¬ Dev.to-style forum discussions
All in one place. No noise. No paywalls.
Live demo: https://askloop-here.netlify.app
What I Built
Before diving into the technical stuff, here's what the platform does:
- 3 post types - Article, Q&A, Forum Discussion
- Rich text editor with image upload (TipTap)
- Nested comments with threaded replies
- Q&A voting - upvote/downvote + accept answer
- JWT Authentication - register, login, refresh tokens
- User profiles - bio, social links, reputation
- Badge system - auto-awarded at reputation milestones
- Real-time notifications - likes, comments, follows
- Full-text search with filters
- Bookmarks, follows, reports
- Account deletion with password confirmation
Tech Stack
I wanted to use tools I could actually understand and explain - not just copy-paste a boilerplate.
Backend
Django 5.0.6
Django REST Framework 3.15.2
Simple JWT (djangorestframework-simplejwt)
django-allauth + dj-rest-auth
PostgreSQL via Supabase
Gunicorn + Nginx
Whitenoise
Frontend
React 18 + TypeScript
Vite
TipTap (rich text editor)
shadcn/ui + Tailwind CSS
Axios + React Router v6
date-fns
Infrastructure
Backend β Linode (Ubuntu 22.04)
Frontend β Netlify
Database β Supabase PostgreSQL
Total cost: $5/month (just the Linode server)
Architecture Overview
Browser
β
https://askloop-here.netlify.app (React β Netlify)
β API calls (proxied through Netlify)
http://172.105.40.228/api/ (Django β Linode)
β
Supabase PostgreSQL
One thing I learned the hard way - Netlify is HTTPS but my Linode server is HTTP. Browsers block mixed content. The fix was proxying the API through Netlify using a _redirects file:
/api/* http://your-server-ip/api/:splat 200
/* /index.html 200
Simple. Elegant. Took me 2 hours to figure out π
Django REST Framework - What I Learned
Custom User Model
Always use a custom user model from day one. Changing it later is painful.
class User(AbstractUser):
bio = models.TextField(max_length=500, blank=True)
reputation = models.IntegerField(default=0)
role = models.CharField(max_length=20, choices=Role.choices)
is_banned = models.BooleanField(default=False)
# Use email as login field
USERNAME_FIELD = "email"
REQUIRED_FIELDS = ["username"]
JWT Authentication
I used dj-rest-auth with Simple-JWT. The setup is straightforward but the token refresh flow took some work to get right.
# settings.py
SIMPLE_JWT = {
"ACCESS_TOKEN_LIFETIME": timedelta(hours=1),
"REFRESH_TOKEN_LIFETIME": timedelta(days=7),
"ROTATE_REFRESH_TOKENS": True,
}
On the frontend, I added an Axios interceptor that automatically refreshes the token on 401 responses:
api.interceptors.response.use(
(response) => response,
async (error) => {
if (error.response?.status == 401) {
// Refresh token and retry
const refresh = localStorage.getItem("refresh_token");
const res = await axios.post("/api/auth/token/refresh/",
{ refresh }
);
localStorage.setItem("access_token", res.data.access);
error.config.headers.Authorization =
`Bearer ${res.data.access}`;
return api(error.config);
}
return Promise.reject(error);
}
);
Slug-Based URLs
Posts use slugs instead of IDs for better URLs.
Auto-generated from the title on save:
class Post(models.Model):
title = models.CharField(max_length=300)
slug = models.SlugField(unique=True, blank=True)
def save(self, *args, **kwargs):
if not self.slug:
base = slugify(self.title)
slug = base
n = 1
while Post.objects.filter(slug=slug).exists():
slug = f"{base}-{n}"
n += 1
self.slug = slug
super().save(*args, **kwargs)
The Badge System
Badges auto-award when a user crosses reputation milestones. The key was overriding save() on the User model so it works regardless of how reputation is updated - even through Django admin:
def save(self, *args, **kwargs):
if self.pk:
try:
old = User.objects.get(pk=pk)
rep_changed = old.reputation != self.reputation
except User.DoesNotExist:
rep_changed = False
else:
rep_changed = False
super().save(*args, **kwargs)
if rep_changed:
self._check_reputation_badges()
def _check_reputation_badge(self):
milestones = {
100: "rising_star",
500: "contributor",
1000: "expert",
5000: "legend",
}
for threshold, slug in milestones.items():
if self.reputation >= threshold:
badge = Badge.objects.get(slug=slug)
UserBadge.objects.get_or_create(
user=self, badge=badge
)
React β Interesting Challenges
The Edited Badge Problem
Django sets created_at and updated_at in two separate operations βthey're never exactly equal even on first save. Difference is ~0.5ms.
My first check updated_at !== created_at was always true, so everything showed "edited".
Fix: Use 1 second tolerance:
{comment.updated_at && comment.created_at &&
(new Date(comment.updated_at).getTime() -
new Date(comment.created_at).getTime()) > 1000 && (
<span className="text-xs italic text-muted">edited</span>
)}
Load More Pagination
Django's paginated response returns next as an absolute URL like http://localhost:8000/api/posts/?page=2.
When I passed this to Axios, it double-prepended the base URL and got a 404.
Fix: Strip the origin before passing to Axios:
function relPath(url: string): string {
try {
const u = new URL(url);
return u.pathname + u.search;
// Returns: /api/posts/?page=2
} catch {
return url;
}
}
// Usage
const res = await api.get(relPath(nextUrl));
TipTap Image Alignment
TipTap's default Image extension doesn't support alignment. I built a custom FigureImage node that wraps <img> in <figure> with alignment controlled by CSS margins:
// Center: margin: 0 auto
// Left: margin-right: auto
// Right: margin-left: auto
Text-align on a block element doesn't work for images β margin-based alignment does.
Deployment β Things That Went Wrong
1. Gunicorn 203/EXEC Error
My first production deploy failed with status=203/EXEC. The unix socket path didn't exist. Fix: switched to TCP binding:
ExecStart=... gunicorn --bind 127.0.0.1:8000 ...
2. InconsistentMigrationHistory
Adding django.contrib.sites to INSTALLED_APPS after socialaccount was already migrated caused this error. Fix:
python manage.py migrate sites --fake-initial
python manage.py migrate
3. Mixed Content (HTTPS β HTTP)
Netlify frontend (HTTPS) calling Linode API (HTTP) gets blocked by browsers. Already covered above β Netlify proxy via _redirects solves it.
4. Email Confirmation Template Error
allauth tried to render an email confirmation template that didn't exist. For now:
ACCOUNT_EMAIL_VERIFICATION = "none"
Will add proper email confirmation later.
What I Would Do Differently
1. Plan the data models first
I added fields to models multiple times after the fact. Spending an extra hour designing models upfront saves days of migrations.
2. Use environment variables from day one
I hardcoded some values early and had to hunt them down later. Always use .env from the first commit.
3. Write API docs as you go
Documenting endpoints after the fact is painful. I should have maintained a simple list as I built.
4. Test on mobile early
The UI looked great on desktop but needed work on mobile. Test on real devices early and often.
The Numbers
Lines of code: ~8,000
Time to build: ~3 months (evenings + weekends)
Server cost: $5/month (Linode)
Domain cost: $0 (using free Netlify subdomain)
Total spent: $15 so far
What's Next
- Password reset via email
- HTTPS on the backend server
- OAuth (Google + GitHub login)
- Email notifications
- Mobile app (maybe)
Source Code Available
If you want to build something similar or just learn from the code β I've made the complete source code available:
π https://samwit.gumroad.com/l/askloop-app
Includes:
- Complete Django backend
- Complete React frontend
- Production deployment guide
- Full API documentation
Try It Live
https://askloop-here.netlify.app
Create an account, write a post, ask a question. The community is just getting started β you'd be one of the first members.
Final Thoughts
Building AskLoop taught me more than any tutorial ever could.
Every bug was a real problem to solve.
Every feature was a real decision to make.
Every deployment error was a real lesson learned.
If you're thinking about building something β stop thinking and start building.
The first version will be ugly. Ship it anyway.
Questions? Drop them in the comments below β I read and respond to every one.
Follow me for more build-in-public content.
Top comments (0)