Build a Full-Stack Django Starter Template with PostgreSQL, HTMX, Tailwind, and Alpine.js


Tags: django postgresql htmx tailwind alpine full-stack deployment guide python tutorial

What You’ll Build

A production-ready Django starter template featuring:

  • Django 6.0 with PostgreSQL database and authentication system
  • User management: Registration, login, logout, and profile editing
  • Item CRUD: Full create, read, update, delete operations
  • HTMX: Server-driven pagination and inline editing
  • Alpine.js: Interactive modals and dropdowns
  • Tailwind CSS: Modern, responsive styling
  • Admin dashboard: Stats and recent items overview
  • Railway deployment: One-click production configuration

Prerequisites

Before starting, make sure you have:

  • Python 3.11+ installed (download here)
  • Git installed and configured
  • Git Bash (all codeblocks in this guide are designed to be pasted into a Git Bash terminal)
  • GitHub account for code hosting
  • Railway account (sign up with our referral link for $20 free credits)

Set Up Project and Database

Create a new directory for your project:

Terminal window
mkdir django-pg-htmx

Navigate to the project directory:

Terminal window
cd django-pg-htmx

Set up a virtual environment:

Terminal window
python -m venv venv

Activate the virtual environment:

On Windows:

Terminal window
source venv/Scripts/activate

On macOS/Linux:

Terminal window
source venv/bin/activate

Install Django and other required packages:

Terminal window
pip install django psycopg2-binary django-htmx whitenoise python-decouple dj-database-url django-tailwind-cli gunicorn pillow

Create a requirements.txt file:

Terminal window
pip freeze > requirements.txt

Initialize a new Django project:

Terminal window
django-admin startproject django_pg_htmx .

This creates a Django project named django_pg_htmx in the current directory.

Create the apps we’ll need. Create the core app (for main CRUD functionality):

Terminal window
python manage.py startapp core

Create the accounts app (for authentication and profiles):

Terminal window
python manage.py startapp accounts

Your project structure should now look like this:

django-pg-htmx/
├── manage.py
├── requirements.txt
├── venv/
├── django_pg_htmx/
│ ├── __init__.py
│ ├── settings.py
│ ├── urls.py
│ ├── wsgi.py
│ └── asgi.py
├── core/
│ ├── __init__.py
│ ├── admin.py
│ ├── apps.py
│ ├── models.py
│ ├── tests.py
│ └── views.py
└── accounts/
├── __init__.py
├── admin.py
├── apps.py
├── models.py
├── tests.py
└── views.py

Set Up Railway and PostgreSQL

Install Railway CLI

Terminal window
npm install -g @railway/cli

Restart your terminal after installation.

Note: Railway CLI requires Node.js. If you don’t have Node.js installed, you can download it from nodejs.org.

Login to Railway

Terminal window
railway login

Create Railway Project

Terminal window
railway init

When prompted:

  1. Select a workspace: Choose your workspace
  2. Project Name: Empty Project for a randomly generated name or django-pg-htmx

Add PostgreSQL Database

Terminal window
railway add -d postgres

This creates a PostgreSQL database in your Railway project.

Terminal window
railway link

When prompted, make the following selections:

  1. Select a workspace: Choose your workspace
  2. Select a project: Choose the project you just created
  3. Select an environment: Choose production
  4. Select a service: Choose Postgres

Note: If you don’t see “Select a service” appear, you may need to run railway link again to ensure the Postgres service is properly linked.


Configure Django Settings

Remove the default settings file and create a settings directory:

Terminal window
rm django_pg_htmx/settings.py
mkdir django_pg_htmx/settings
touch django_pg_htmx/settings/__init__.py

Create the base settings file:

Terminal window
cat > django_pg_htmx/settings/base.py << 'EOF'
"""
Base settings for Django project.
Common settings shared across all environments.
"""
from pathlib import Path
import os
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent.parent
# Application definition
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'django_htmx',
'django_tailwind_cli',
'core',
'accounts',
]
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'whitenoise.middleware.WhiteNoiseMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
'django_htmx.middleware.HtmxMiddleware',
]
ROOT_URLCONF = 'django_pg_htmx.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [BASE_DIR / 'templates'],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
WSGI_APPLICATION = 'django_pg_htmx.wsgi.application'
# Password validation
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
]
# Internationalization
LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'UTC'
USE_I18N = True
USE_TZ = True
# Static files (CSS, JavaScript, Images)
STATIC_URL = '/static/'
STATIC_ROOT = BASE_DIR / 'staticfiles'
STATICFILES_DIRS = [BASE_DIR / 'static']
# Media files (user uploads)
MEDIA_URL = '/media/'
MEDIA_ROOT = BASE_DIR / 'media'
# Default primary key field type
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
# Login/Logout URLs
LOGIN_URL = 'accounts:login'
LOGIN_REDIRECT_URL = 'core:dashboard'
LOGOUT_REDIRECT_URL = 'accounts:login'
# Tailwind CSS configuration
TAILWIND_CLI_DIST_CSS = 'css/tailwind.css'
EOF

Create the local development settings file:

Terminal window
cat > django_pg_htmx/settings/local.py << 'EOF'
"""
Local development settings.
Uses SQLite for local development without external dependencies.
"""
from .base import *
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = 'django-insecure-local-dev-key-not-for-production'
# Allowed hosts for local development
ALLOWED_HOSTS = ['localhost', '127.0.0.1']
# Database configuration - SQLite for local development
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': BASE_DIR / 'db.sqlite3',
}
}
EOF

Create the production settings file:

Terminal window
cat > django_pg_htmx/settings/production.py << 'EOF'
"""
Production settings.
Configured for Railway deployment.
"""
from .base import *
from decouple import config
import dj_database_url
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = config('DEBUG', default=False, cast=bool)
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = config('SECRET_KEY')
# Database configuration
# Uses DATABASE_URL from Railway Postgres plugin
DATABASES = {
'default': dj_database_url.config(
conn_max_age=600,
conn_health_checks=True,
)
}
# Allowed hosts
ALLOWED_HOSTS = config('ALLOWED_HOSTS', default='', cast=lambda v: [s.strip() for s in v.split(',') if s.strip()])
# Static files storage (WhiteNoise for production)
STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage'
# Trust Railway's proxy headers for SSL
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
# Security settings for production
if not DEBUG:
SECURE_SSL_REDIRECT = True
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
SECURE_BROWSER_XSS_FILTER = True
SECURE_CONTENT_TYPE_NOSNIFF = True
X_FRAME_OPTIONS = 'DENY'
EOF

Update manage.py to use local settings by default:

cat > manage.py << 'EOF'
#!/usr/bin/env python
"""Django's command-line utility for administrative tasks."""
import os
import sys
def main():
"""Run administrative tasks."""
# Use local settings by default for development
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'django_pg_htmx.settings.local')
try:
from django.core.management import execute_from_command_line
except ImportError as exc:
raise ImportError(
"Couldn't import Django. Are you sure it's installed and "
"available on your PYTHONPATH environment variable? Did you "
"forget to activate a virtual environment?"
) from exc
execute_from_command_line(sys.argv)
if __name__ == '__main__':
main()
EOF

Create Models

Update core/models.py to create the Item model:

Terminal window
cat > core/models.py << 'EOF'
from django.db import models
from django.contrib.auth.models import User
from django.urls import reverse
class Item(models.Model):
"""Generic item model for demonstrating CRUD operations."""
name = models.CharField(max_length=200)
description = models.TextField(blank=True)
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='items')
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
ordering = ['-created_at']
indexes = [
models.Index(fields=['-created_at']),
models.Index(fields=['user', '-created_at']),
]
def __str__(self):
return self.name
def get_absolute_url(self):
return reverse('core:item_detail', kwargs={'pk': self.pk})
EOF

Create accounts/models.py with the Profile model and signals:

Terminal window
cat > accounts/models.py << 'EOF'
from django.db import models
from django.contrib.auth.models import User
from django.db.models.signals import post_save
from django.dispatch import receiver
class Profile(models.Model):
"""User profile model extending the default User model."""
user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='profile')
bio = models.TextField(blank=True, max_length=500)
avatar = models.ImageField(upload_to='avatars/', blank=True, null=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
def __str__(self):
return f"{self.user.username}'s Profile"
def get_absolute_url(self):
from django.urls import reverse
return reverse('accounts:profile')
@receiver(post_save, sender=User)
def create_user_profile(sender, instance, created, **kwargs):
"""Create a profile automatically when a user is created."""
if created:
Profile.objects.create(user=instance)
@receiver(post_save, sender=User)
def save_user_profile(sender, instance, **kwargs):
"""Save the profile when the user is saved."""
if hasattr(instance, 'profile'):
instance.profile.save()
EOF

Register models in admin. Update core/admin.py:

Terminal window
cat > core/admin.py << 'EOF'
from django.contrib import admin
from .models import Item
@admin.register(Item)
class ItemAdmin(admin.ModelAdmin):
list_display = ['name', 'user', 'created_at', 'updated_at']
list_filter = ['created_at', 'updated_at']
search_fields = ['name', 'description']
date_hierarchy = 'created_at'
EOF

Update accounts/admin.py:

Terminal window
cat > accounts/admin.py << 'EOF'
from django.contrib import admin
from .models import Profile
@admin.register(Profile)
class ProfileAdmin(admin.ModelAdmin):
list_display = ['user', 'created_at', 'updated_at']
search_fields = ['user__username', 'user__email', 'bio']
EOF

Set Up Frontend

Create the templates and static directories:

Terminal window
mkdir -p templates static/css

Create templates/base.html:

Terminal window
cat > templates/base.html << 'EOF'
{% load django_htmx tailwind_cli %}
<!DOCTYPE html>
<html lang="en" hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}Django Starter{% endblock %}</title>
{% tailwind_css %}
{% htmx_script %}
<script>
// Ensure Chart.js is loaded before Alpine initializes (if needed)
window.ChartLoaded = true;
</script>
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
</head>
<body class="bg-gray-50 min-h-screen" x-data="{ open: false, itemId: null }">
<nav class="bg-white shadow-sm border-b">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex justify-between h-16">
<div class="flex items-center">
<a href="{% url 'core:dashboard' %}" class="text-xl font-bold text-gray-900">
Django Starter
</a>
</div>
<div class="flex items-center space-x-4">
{% if user.is_authenticated %}
<span class="text-gray-700">Hello, {{ user.username }}</span>
<a href="{% url 'core:dashboard' %}" class="text-gray-700 hover:text-gray-900">Dashboard</a>
<a href="{% url 'core:item_list' %}" class="text-gray-700 hover:text-gray-900">Items</a>
<!-- User Menu Dropdown (Alpine.js) -->
<div x-data="{ open: false }" class="relative">
<button @click="open = !open" class="flex items-center text-gray-700 hover:text-gray-900 focus:outline-none">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
</svg>
</button>
<div x-show="open" @click.away="open = false" x-transition class="absolute right-0 mt-2 w-48 bg-white rounded-md shadow-lg py-1 z-50">
<a href="{% url 'accounts:profile' %}" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">Profile</a>
<a href="{% url 'accounts:logout' %}" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">Logout</a>
</div>
</div>
{% else %}
<a href="{% url 'accounts:login' %}" class="text-gray-700 hover:text-gray-900">Login</a>
<a href="{% url 'accounts:register' %}" class="bg-blue-600 text-white px-4 py-2 rounded-md hover:bg-blue-700">Sign Up</a>
{% endif %}
</div>
</div>
</div>
</nav>
<main class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
{% if messages %}
<div class="mb-4">
{% for message in messages %}
<div class="bg-{% if message.tags == 'error' %}red{% elif message.tags == 'success' %}green{% else %}blue{% endif %}-100 border border-{% if message.tags == 'error' %}red{% elif message.tags == 'success' %}green{% else %}blue{% endif %}-400 text-{% if message.tags == 'error' %}red{% elif message.tags == 'success' %}green{% else %}blue{% endif %}-700 px-4 py-3 rounded relative" role="alert">
<span class="block sm:inline">{{ message }}</span>
</div>
{% endfor %}
</div>
{% endif %}
{% block content %}
{% endblock %}
</main>
<!-- Delete Confirmation Modal (Alpine.js) -->
<div x-show="open"
x-cloak
class="fixed inset-0 z-50 overflow-y-auto"
style="display: none;"
@keydown.escape.window="open = false">
<div class="flex items-center justify-center min-h-screen px-4">
<div class="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" @click="open = false"></div>
<div class="relative bg-white rounded-lg shadow-xl max-w-md w-full p-6">
<h3 class="text-lg font-medium text-gray-900 mb-4">Confirm Delete</h3>
<p class="text-sm text-gray-500 mb-6">Are you sure you want to delete this item? This action cannot be undone.</p>
<div class="flex justify-end space-x-3">
<button @click="open = false" class="px-4 py-2 text-sm font-medium text-gray-700 bg-gray-200 rounded-md hover:bg-gray-300">
Cancel
</button>
<form :action="`/items/${itemId}/delete/`" method="post" style="display: inline;">
{% csrf_token %}
<button type="submit" class="px-4 py-2 text-sm font-medium text-white bg-red-600 rounded-md hover:bg-red-700">
Delete
</button>
</form>
</div>
</div>
</div>
</div>
<style>
[x-cloak] { display: none !important; }
</style>
</body>
</html>
EOF

Create Authentication

Create accounts/forms.py:

Terminal window
cat > accounts/forms.py << 'EOF'
from django import forms
from django.contrib.auth.forms import UserCreationForm
from django.contrib.auth.models import User
from .models import Profile
class UserRegistrationForm(UserCreationForm):
email = forms.EmailField(required=True)
class Meta:
model = User
fields = ['username', 'email', 'password1', 'password2']
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Add Tailwind classes to form fields
for field_name, field in self.fields.items():
field.widget.attrs['class'] = 'w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500'
class ProfileForm(forms.ModelForm):
class Meta:
model = Profile
fields = ['bio', 'avatar']
widgets = {
'bio': forms.Textarea(attrs={
'class': 'w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500',
'rows': 4,
}),
'avatar': forms.FileInput(attrs={
'class': 'w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500',
}),
}
EOF

Update accounts/views.py:

Terminal window
cat > accounts/views.py << 'EOF'
from django.shortcuts import render, redirect
from django.contrib.auth import login
from django.contrib.auth.decorators import login_required
from django.contrib.auth.views import LoginView, LogoutView
from django.contrib import messages
from django.views.generic import CreateView
from django.urls import reverse_lazy
from .forms import UserRegistrationForm, ProfileForm
from .models import Profile
class UserRegistrationView(CreateView):
form_class = UserRegistrationForm
template_name = 'accounts/register.html'
success_url = reverse_lazy('core:dashboard')
def form_valid(self, form):
response = super().form_valid(form)
login(self.request, self.object)
messages.success(self.request, 'Registration successful! Welcome!')
return response
class UserLoginView(LoginView):
template_name = 'accounts/login.html'
redirect_authenticated_user = True
def get_success_url(self):
return reverse_lazy('core:dashboard')
class UserLogoutView(LogoutView):
next_page = 'accounts:login'
def dispatch(self, request, *args, **kwargs):
messages.success(request, 'You have been logged out successfully.')
return super().dispatch(request, *args, **kwargs)
@login_required
def profile_view(request):
"""View user profile."""
profile = request.user.profile
return render(request, 'accounts/profile.html', {'profile': profile})
@login_required
def profile_edit(request):
"""Edit user profile."""
profile = request.user.profile
if request.method == 'POST':
form = ProfileForm(request.POST, request.FILES, instance=profile)
if form.is_valid():
form.save()
messages.success(request, 'Profile updated successfully!')
return redirect('accounts:profile')
else:
form = ProfileForm(instance=profile)
return render(request, 'accounts/profile_form.html', {'form': form})
EOF

Create the accounts templates directory:

Terminal window
mkdir -p templates/accounts

Create templates/accounts/login.html:

Terminal window
cat > templates/accounts/login.html << 'EOF'
{% extends 'base.html' %}
{% block title %}Login - Django Starter{% endblock %}
{% block content %}
<div class="max-w-md mx-auto bg-white rounded-lg shadow-md p-6">
<h2 class="text-2xl font-bold text-gray-900 mb-6">Login</h2>
<form method="post">
{% csrf_token %}
<div class="mb-4">
<label for="id_username" class="block text-sm font-medium text-gray-700 mb-2">Username</label>
<input type="text" name="username" id="id_username" required class="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500">
</div>
<div class="mb-4">
<label for="id_password" class="block text-sm font-medium text-gray-700 mb-2">Password</label>
<input type="password" name="password" id="id_password" required class="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500">
</div>
<button type="submit" class="w-full bg-blue-600 text-white px-4 py-2 rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500">
Login
</button>
</form>
<p class="mt-4 text-center text-sm text-gray-600">
Don't have an account? <a href="{% url 'accounts:register' %}" class="text-blue-600 hover:text-blue-800">Sign Up</a>
</p>
</div>
{% endblock %}
EOF

Create templates/accounts/register.html:

Terminal window
cat > templates/accounts/register.html << 'EOF'
{% extends 'base.html' %}
{% block title %}Register - Django Starter{% endblock %}
{% block content %}
<div class="max-w-md mx-auto bg-white rounded-lg shadow-md p-6">
<h2 class="text-2xl font-bold text-gray-900 mb-6">Sign Up</h2>
<form method="post">
{% csrf_token %}
{% for field in form %}
<div class="mb-4">
<label for="{{ field.id_for_label }}" class="block text-sm font-medium text-gray-700 mb-2">
{{ field.label }}
</label>
{{ field }}
{% if field.errors %}
<p class="mt-1 text-sm text-red-600">{{ field.errors.0 }}</p>
{% endif %}
{% if field.help_text %}
<p class="mt-1 text-sm text-gray-500">{{ field.help_text }}</p>
{% endif %}
</div>
{% endfor %}
<button type="submit" class="w-full bg-blue-600 text-white px-4 py-2 rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500">
Sign Up
</button>
</form>
<p class="mt-4 text-center text-sm text-gray-600">
Already have an account? <a href="{% url 'accounts:login' %}" class="text-blue-600 hover:text-blue-800">Login</a>
</p>
</div>
{% endblock %}
EOF

Create templates/accounts/profile.html:

Terminal window
cat > templates/accounts/profile.html << 'EOF'
{% extends 'base.html' %}
{% block title %}Profile - Django Starter{% endblock %}
{% block content %}
<div class="max-w-2xl mx-auto">
<div class="bg-white rounded-lg shadow-md p-6">
<div class="flex justify-between items-center mb-6">
<h2 class="text-2xl font-bold text-gray-900">Profile</h2>
<a href="{% url 'accounts:profile_edit' %}" class="bg-blue-600 text-white px-4 py-2 rounded-md hover:bg-blue-700">
Edit Profile
</a>
</div>
<div class="space-y-4">
<div>
<label class="block text-sm font-medium text-gray-700">Username</label>
<p class="mt-1 text-gray-900">{{ user.username }}</p>
</div>
<div>
<label class="block text-sm font-medium text-gray-700">Email</label>
<p class="mt-1 text-gray-900">{{ user.email }}</p>
</div>
<div>
<label class="block text-sm font-medium text-gray-700">Bio</label>
<p class="mt-1 text-gray-900">{{ profile.bio|default:"No bio yet." }}</p>
</div>
{% if profile.avatar %}
<div>
<label class="block text-sm font-medium text-gray-700">Avatar</label>
<img src="{{ profile.avatar.url }}" alt="Avatar" class="mt-2 h-24 w-24 rounded-full object-cover">
</div>
{% endif %}
<div>
<label class="block text-sm font-medium text-gray-700">Member since</label>
<p class="mt-1 text-gray-900">{{ profile.created_at|date:"F d, Y" }}</p>
</div>
</div>
</div>
</div>
{% endblock %}
EOF

Create templates/accounts/profile_form.html:

Terminal window
cat > templates/accounts/profile_form.html << 'EOF'
{% extends 'base.html' %}
{% block title %}Edit Profile - Django Starter{% endblock %}
{% block content %}
<div class="max-w-2xl mx-auto">
<div class="bg-white rounded-lg shadow-md p-6">
<h2 class="text-2xl font-bold text-gray-900 mb-6">Edit Profile</h2>
<form method="post" enctype="multipart/form-data">
{% csrf_token %}
{% for field in form %}
<div class="mb-4">
<label for="{{ field.id_for_label }}" class="block text-sm font-medium text-gray-700 mb-2">
{{ field.label }}
</label>
{{ field }}
{% if field.errors %}
<p class="mt-1 text-sm text-red-600">{{ field.errors.0 }}</p>
{% endif %}
{% if field.help_text %}
<p class="mt-1 text-sm text-gray-500">{{ field.help_text }}</p>
{% endif %}
</div>
{% endfor %}
<div class="flex space-x-4">
<button type="submit" class="bg-blue-600 text-white px-4 py-2 rounded-md hover:bg-blue-700">
Save Changes
</button>
<a href="{% url 'accounts:profile' %}" class="bg-gray-300 text-gray-700 px-4 py-2 rounded-md hover:bg-gray-400">
Cancel
</a>
</div>
</form>
</div>
</div>
{% endblock %}
EOF

Create accounts/urls.py:

Terminal window
cat > accounts/urls.py << 'EOF'
from django.urls import path
from . import views
app_name = 'accounts'
urlpatterns = [
path('register/', views.UserRegistrationView.as_view(), name='register'),
path('login/', views.UserLoginView.as_view(), name='login'),
path('logout/', views.UserLogoutView.as_view(), name='logout'),
path('profile/', views.profile_view, name='profile'),
path('profile/edit/', views.profile_edit, name='profile_edit'),
]
EOF

Create Core Application

Create core/forms.py:

Terminal window
cat > core/forms.py << 'EOF'
from django import forms
from .models import Item
class ItemForm(forms.ModelForm):
class Meta:
model = Item
fields = ['name', 'description']
widgets = {
'name': forms.TextInput(attrs={
'class': 'w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500',
}),
'description': forms.Textarea(attrs={
'class': 'w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500',
'rows': 4,
}),
}
EOF

Create core/urls.py:

Terminal window
cat > core/urls.py << 'EOF'
from django.urls import path
from . import views
app_name = 'core'
urlpatterns = [
path('', views.dashboard, name='dashboard'),
path('items/', views.ItemListView.as_view(), name='item_list'),
path('items/create/', views.ItemCreateView.as_view(), name='item_create'),
path('items/<int:pk>/', views.item_detail, name='item_detail'),
path('items/<int:pk>/edit/', views.ItemUpdateView.as_view(), name='item_update'),
path('items/<int:pk>/delete/', views.ItemDeleteView.as_view(), name='item_delete'),
]
EOF

Update django_pg_htmx/urls.py:

Terminal window
cat > django_pg_htmx/urls.py << 'EOF'
from django.contrib import admin
from django.urls import path, include
from django.conf import settings
from django.conf.urls.static import static
urlpatterns = [
path('admin/', admin.site.urls),
path('accounts/', include('accounts.urls')),
path('', include('core.urls')),
]
# Serve media files in development
if settings.DEBUG:
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
EOF

Create templates/dashboard.html:

Terminal window
cat > templates/dashboard.html << 'EOF'
{% extends 'base.html' %}
{% block title %}Dashboard - Django Starter{% endblock %}
{% block content %}
<div class="space-y-6">
<div class="flex justify-between items-center">
<h1 class="text-3xl font-bold text-gray-900">Dashboard</h1>
<a href="{% url 'core:item_create' %}" class="bg-blue-600 text-white px-4 py-2 rounded-md hover:bg-blue-700">
Create Item
</a>
</div>
<!-- Stats Cards -->
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<div class="bg-white rounded-lg shadow p-6">
<h3 class="text-sm font-medium text-gray-500">Total Items</h3>
<p class="text-2xl font-bold text-gray-900 mt-2">{{ total_items }}</p>
</div>
<div class="bg-white rounded-lg shadow p-6">
<h3 class="text-sm font-medium text-gray-500">Recent Items</h3>
<p class="text-2xl font-bold text-gray-900 mt-2">{{ recent_items|length }}</p>
</div>
<div class="bg-white rounded-lg shadow p-6">
<h3 class="text-sm font-medium text-gray-500">Quick Actions</h3>
<div class="mt-2 space-x-2">
<a href="{% url 'core:item_create' %}" class="text-blue-600 hover:text-blue-800 text-sm">Create Item</a>
<span class="text-gray-300">|</span>
<a href="{% url 'core:item_list' %}" class="text-blue-600 hover:text-blue-800 text-sm">View All</a>
</div>
</div>
</div>
<!-- Recent Items -->
<div class="bg-white rounded-lg shadow">
<div class="px-6 py-4 border-b border-gray-200">
<h2 class="text-xl font-bold text-gray-900">Recent Items</h2>
</div>
<div class="p-6">
{% if recent_items %}
<ul class="space-y-3">
{% for item in recent_items %}
<li class="flex justify-between items-center py-2 border-b border-gray-200">
<div>
<a href="{% url 'core:item_detail' item.pk %}" class="text-blue-600 hover:text-blue-800 font-medium">{{ item.name }}</a>
{% if item.description %}
<p class="text-sm text-gray-500 mt-1">{{ item.description|truncatewords:20 }}</p>
{% endif %}
</div>
<span class="text-sm text-gray-500">{{ item.created_at|date:"M d, Y" }}</span>
</li>
{% endfor %}
</ul>
<div class="mt-4">
<a href="{% url 'core:item_list' %}" class="text-blue-600 hover:text-blue-800">View all items →</a>
</div>
{% else %}
<p class="text-gray-500">No items yet. <a href="{% url 'core:item_create' %}" class="text-blue-600 hover:text-blue-800">Create your first item</a>!</p>
{% endif %}
</div>
</div>
</div>
{% endblock %}
EOF

Create the core templates directory:

Terminal window
mkdir -p templates/core templates/core/partials

Create templates/core/item_list.html:

Terminal window
cat > templates/core/item_list.html << 'EOF'
{% extends 'base.html' %}
{% block title %}Items - Django Starter{% endblock %}
{% block content %}
<div class="space-y-6">
<div class="flex justify-between items-center">
<h1 class="text-3xl font-bold text-gray-900">Items</h1>
<a href="{% url 'core:item_create' %}" class="bg-blue-600 text-white px-4 py-2 rounded-md hover:bg-blue-700">
Create Item
</a>
</div>
<!-- Items List (HTMX will swap this section) -->
<div id="item-list-container">
{% include 'core/partials/item_list_partial.html' %}
</div>
</div>
{% endblock %}
EOF

Create templates/core/partials/item_list_partial.html:

Terminal window
cat > templates/core/partials/item_list_partial.html << 'EOF'
{% if items %}
<div class="bg-white rounded-lg shadow overflow-hidden">
<table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50">
<tr>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Name</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Description</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Created</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Actions</th>
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200">
{% for item in items %}
{% include 'core/partials/item_row.html' %}
{% endfor %}
</tbody>
</table>
</div>
<!-- Pagination (HTMX-powered) -->
{% if page_obj.has_other_pages %}
<div class="mt-4 flex justify-center">
<nav class="relative z-0 inline-flex rounded-md shadow-sm -space-x-px" aria-label="Pagination">
{% if page_obj.has_previous %}
<a href="?page={{ page_obj.previous_page_number }}"
hx-get="?page={{ page_obj.previous_page_number }}"
hx-target="#item-list-container"
hx-swap="innerHTML"
class="relative inline-flex items-center px-2 py-2 rounded-l-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50">
Previous
</a>
{% endif %}
{% for num in page_obj.paginator.page_range %}
{% if page_obj.number == num %}
<span class="relative inline-flex items-center px-4 py-2 border border-gray-300 bg-blue-50 text-sm font-medium text-blue-600">
{{ num }}
</span>
{% elif num > page_obj.number|add:'-3' and num < page_obj.number|add:'3' %}
<a href="?page={{ num }}"
hx-get="?page={{ num }}"
hx-target="#item-list-container"
hx-swap="innerHTML"
class="relative inline-flex items-center px-4 py-2 border border-gray-300 bg-white text-sm font-medium text-gray-700 hover:bg-gray-50">
{{ num }}
</a>
{% endif %}
{% endfor %}
{% if page_obj.has_next %}
<a href="?page={{ page_obj.next_page_number }}"
hx-get="?page={{ page_obj.next_page_number }}"
hx-target="#item-list-container"
hx-swap="innerHTML"
class="relative inline-flex items-center px-2 py-2 rounded-r-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50">
Next
</a>
{% endif %}
</nav>
</div>
{% endif %}
{% else %}
<div class="bg-white rounded-lg shadow p-6 text-center">
<p class="text-gray-500">No items yet. <a href="{% url 'core:item_create' %}" class="text-blue-600 hover:text-blue-800">Create your first item</a>!</p>
</div>
{% endif %}
EOF

Create templates/core/partials/item_row.html for the item display:

Terminal window
cat > templates/core/partials/item_row.html << 'EOF'
<tr id="item-row-{{ item.pk }}">
<td class="px-6 py-4 whitespace-nowrap">
<div class="text-sm font-medium text-gray-900" id="item-name-{{ item.pk }}">{{ item.name }}</div>
</td>
<td class="px-6 py-4">
<div class="text-sm text-gray-500" id="item-desc-{{ item.pk }}">{{ item.description|truncatewords:15|default:"—" }}</div>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<div class="text-sm text-gray-500">{{ item.created_at|date:"M d, Y" }}</div>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium">
<div class="flex space-x-2">
<a href="{% url 'core:item_detail' item.pk %}" class="text-blue-600 hover:text-blue-900">View</a>
<button
hx-get="{% url 'core:item_update' item.pk %}"
hx-target="#item-row-{{ item.pk }}"
hx-swap="outerHTML"
class="text-indigo-600 hover:text-indigo-900">
Edit
</button>
<button
@click="open = true; itemId = {{ item.pk }}"
class="text-red-600 hover:text-red-900">
Delete
</button>
</div>
</td>
</tr>
EOF

Create a separate template for the edit form row:

Terminal window
cat > templates/core/partials/item_edit_row.html << 'EOF'
<tr id="item-row-{{ item.pk }}" class="bg-gray-50">
<td colspan="4" class="px-6 py-4">
<form hx-post="{% url 'core:item_update' item.pk %}"
hx-target="#item-row-{{ item.pk }}"
hx-swap="outerHTML">
{% csrf_token %}
<div class="space-y-3">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Name</label>
<input type="text" name="name" value="{{ item.name }}" required
class="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Description</label>
<textarea name="description" rows="3"
class="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500">{{ item.description }}</textarea>
</div>
<div class="flex space-x-2">
<button type="submit" class="bg-blue-600 text-white px-4 py-2 rounded-md hover:bg-blue-700 text-sm">
Save
</button>
<a href="{% url 'core:item_list' %}"
class="bg-gray-300 text-gray-700 px-4 py-2 rounded-md hover:bg-gray-400 text-sm">
Cancel
</a>
</div>
</div>
</form>
</td>
</tr>
EOF

Create core/views.py with views for dashboard and CRUD operations:

Terminal window
cat > core/views.py << 'EOF'
from django.shortcuts import render, get_object_or_404, redirect
from django.contrib.auth.decorators import login_required
from django.contrib import messages
from django.views.generic import ListView, CreateView, UpdateView, DeleteView
from django.urls import reverse_lazy
from django.core.paginator import Paginator
from django.http import JsonResponse
from .models import Item
from .forms import ItemForm
@login_required
def dashboard(request):
"""Simple admin dashboard with stats and recent items."""
user_items = Item.objects.filter(user=request.user)
total_items = user_items.count()
recent_items = user_items[:5]
context = {
'total_items': total_items,
'recent_items': recent_items,
}
return render(request, 'dashboard.html', context)
class ItemListView(ListView):
"""List view for items with HTMX pagination support."""
model = Item
template_name = 'core/item_list.html'
context_object_name = 'items'
paginate_by = 10
def get_queryset(self):
return Item.objects.filter(user=self.request.user).order_by('-created_at')
def get_template_names(self):
"""Return different template for HTMX requests."""
if self.request.htmx:
return ['core/partials/item_list_partial.html']
return [self.template_name]
class ItemCreateView(CreateView):
"""Create view for items."""
model = Item
form_class = ItemForm
template_name = 'core/item_form.html'
success_url = reverse_lazy('core:item_list')
def form_valid(self, form):
form.instance.user = self.request.user
messages.success(self.request, 'Item created successfully!')
return super().form_valid(form)
class ItemUpdateView(UpdateView):
"""Update view for items with HTMX inline editing support."""
model = Item
form_class = ItemForm
template_name = 'core/item_form.html'
success_url = reverse_lazy('core:item_list')
def get_queryset(self):
return Item.objects.filter(user=self.request.user)
def form_valid(self, form):
messages.success(self.request, 'Item updated successfully!')
if self.request.htmx:
# Return the updated row for HTMX
return render(self.request, 'core/partials/item_row.html', {'item': self.object})
return super().form_valid(form)
def get_template_names(self):
"""Return different template for HTMX requests."""
if self.request.htmx:
if self.request.method == 'GET':
# Show edit form
return ['core/partials/item_edit_row.html']
else:
# Return updated row
return ['core/partials/item_row.html']
return [self.template_name]
class ItemDeleteView(DeleteView):
"""Delete view for items."""
model = Item
success_url = reverse_lazy('core:item_list')
def get_queryset(self):
return Item.objects.filter(user=self.request.user)
def delete(self, request, *args, **kwargs):
messages.success(request, 'Item deleted successfully!')
return super().delete(request, *args, **kwargs)
@login_required
def item_detail(request, pk):
"""Detail view for a single item."""
item = get_object_or_404(Item, pk=pk, user=request.user)
return render(request, 'core/item_detail.html', {'item': item})
EOF

Create templates/core/item_detail.html:

Terminal window
cat > templates/core/item_detail.html << 'EOF'
{% extends 'base.html' %}
{% block title %}{{ item.name }} - Django Starter{% endblock %}
{% block content %}
<div class="max-w-2xl mx-auto">
<div class="bg-white rounded-lg shadow-md p-6">
<div class="flex justify-between items-center mb-6">
<h1 class="text-2xl font-bold text-gray-900">{{ item.name }}</h1>
<div class="flex space-x-2">
<a href="{% url 'core:item_update' item.pk %}" class="bg-blue-600 text-white px-4 py-2 rounded-md hover:bg-blue-700">
Edit
</a>
<button
@click="open = true; itemId = {{ item.pk }}"
class="bg-red-600 text-white px-4 py-2 rounded-md hover:bg-red-700">
Delete
</button>
</div>
</div>
<div class="space-y-4">
<div>
<label class="block text-sm font-medium text-gray-700">Description</label>
<p class="mt-1 text-gray-900">{{ item.description|default:"No description provided." }}</p>
</div>
<div>
<label class="block text-sm font-medium text-gray-700">Created</label>
<p class="mt-1 text-gray-900">{{ item.created_at|date:"F d, Y" }}</p>
</div>
<div>
<label class="block text-sm font-medium text-gray-700">Last Updated</label>
<p class="mt-1 text-gray-900">{{ item.updated_at|date:"F d, Y" }}</p>
</div>
</div>
<div class="mt-6">
<a href="{% url 'core:item_list' %}" class="text-blue-600 hover:text-blue-800">← Back to Items</a>
</div>
</div>
</div>
{% endblock %}
EOF

Create templates/core/item_form.html:

Terminal window
cat > templates/core/item_form.html << 'EOF'
{% extends 'base.html' %}
{% block title %}{% if object %}Edit{% else %}Create{% endif %} Item - Django Starter{% endblock %}
{% block content %}
<div class="max-w-2xl mx-auto">
<div class="bg-white rounded-lg shadow-md p-6">
<h2 class="text-2xl font-bold text-gray-900 mb-6">{% if object %}Edit{% else %}Create{% endif %} Item</h2>
<form method="post">
{% csrf_token %}
{% for field in form %}
<div class="mb-4">
<label for="{{ field.id_for_label }}" class="block text-sm font-medium text-gray-700 mb-2">
{{ field.label }}
</label>
{{ field }}
{% if field.errors %}
<p class="mt-1 text-sm text-red-600">{{ field.errors.0 }}</p>
{% endif %}
{% if field.help_text %}
<p class="mt-1 text-sm text-gray-500">{{ field.help_text }}</p>
{% endif %}
</div>
{% endfor %}
<div class="flex space-x-4">
<button type="submit" class="bg-blue-600 text-white px-4 py-2 rounded-md hover:bg-blue-700">
{% if object %}Update{% else %}Create{% endif %}
</button>
<a href="{% url 'core:item_list' %}" class="bg-gray-300 text-gray-700 px-4 py-2 rounded-md hover:bg-gray-400">
Cancel
</a>
</div>
</form>
</div>
</div>
{% endblock %}
EOF

Prepare for Deployment

Create a .gitignore file:

Terminal window
cat > .gitignore << 'EOF'
# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
venv/
env/
ENV/
.venv
# Django
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
/media
/staticfiles
# Environment variables
.env
.env.local
# IDE
.vscode/
.idea/
*.swp
*.swo
*~
# OS
.DS_Store
Thumbs.db
# Tailwind CLI
.django_tailwind_cli/
EOF

Create a Procfile for Railway deployment:

Terminal window
cat > Procfile << 'EOF'
web: mkdir -p staticfiles static/css && python manage.py tailwind build && python manage.py migrate --noinput && python manage.py collectstatic --noinput && gunicorn django_pg_htmx.wsgi:application --bind 0.0.0.0:$PORT
EOF

This Procfile runs on each deployment and:

  1. Creates necessary directories
  2. Downloads Tailwind CSS CLI and builds CSS (first time or if updated)
  3. Runs database migrations
  4. Collects all static files
  5. Starts the Gunicorn server

Generate Migrations

Generate database migrations locally using SQLite:

Terminal window
python manage.py makemigrations core
Terminal window
python manage.py makemigrations accounts

Push to GitHub

Initialize Git repository:

Terminal window
git init
Terminal window
git add .
Terminal window
git commit -m "Initial commit: Django starter template"

Create GitHub repository and push:

Terminal window
gh repo create --private --source=. --push

Prefer VS Code? Use Source Control panel → Publish to GitHub → Follow prompts

Your complete Django application (including migrations) is now on GitHub and ready for deployment.


Deploy to Railway

Connect GitHub Repository

In Railway dashboard:

  1. Click your project
  2. Click “Create”
  3. Select “GitHub Repo”
  4. Choose your repository
  5. Click the “Deploy” button to start the deployment

Generate Domain

  1. In Railway dashboard, click your django-pg-htmx service
  2. Go to SettingsNetworking
  3. Click “Generate Domain”
  4. When prompted “Enter the port your app is listening on”, enter 8080
  5. Copy the generated domain (e.g., django-pg-htmx-production.up.railway.app)

Save this domain - you’ll need it for the ALLOWED_HOSTS configuration.

Link the PostgreSQL database to your Django service:

  1. In Railway dashboard, click your django-pg-htmx service
  2. Go to “Variables” tab
  3. Click “Add Variable” next to where it says “Trying to connect a database?”
  4. From the list of available variables, select DATABASE_URL from your Postgres service
  5. Click “Add”

Configure Environment Variables

Add the required environment variables for production:

  1. In the Variables tab for the django-pg-htmx service, add these variables:
  • DJANGO_SETTINGS_MODULE: django_pg_htmx.settings.production
  • SECRET_KEY: Generate a secure secret key by running this command locally:
    Terminal window
    python -c "from django.core.management.utils import get_random_secret_key; print(get_random_secret_key())"
    Copy the output and paste it as the value for SECRET_KEY in Railway
  • DEBUG: False
  • ALLOWED_HOSTS: Use your domain from the previous step (e.g., django-pg-htmx-production.up.railway.app)

After adding these variables, click the “Deploy” button.

Create Superuser

Create an admin user for your production application.

First, wake up your Railway service by visiting your domain in a browser:

https://your-domain.up.railway.app

Wait 10-20 seconds for the service to fully start, then SSH into your Django service:

Terminal window
railway ssh --service django-pg-htmx

Once connected, run the createsuperuser command:

Terminal window
python manage.py createsuperuser

Follow the prompts to enter:

  • Username
  • Email address (optional)
  • Password (enter twice)

Exit the SSH session:

Terminal window
exit

Your superuser is now created in the production database. You can log in at your domain’s /admin/ URL.

Your Django application is now fully deployed and ready to use!


Next Steps

(Optional) Load Sample Data

To see the app with example items, you can load sample data.

Create the fixtures directory:

Terminal window
mkdir -p core/fixtures

Create sample data fixture:

Terminal window
cat > core/fixtures/sample_data.json << 'EOF'
[
{
"model": "core.item",
"pk": 1,
"fields": {
"name": "Sample Item 1",
"description": "This is a sample item to demonstrate the CRUD functionality.",
"user": 1,
"created_at": "2025-01-15T10:00:00Z",
"updated_at": "2025-01-15T10:00:00Z"
}
},
{
"model": "core.item",
"pk": 2,
"fields": {
"name": "Sample Item 2",
"description": "Another sample item with some description text.",
"user": 1,
"created_at": "2025-01-14T10:00:00Z",
"updated_at": "2025-01-14T10:00:00Z"
}
}
]
EOF

Commit and push the fixture file:

Terminal window
git add core/fixtures/sample_data.json
Terminal window
git commit -m "Add sample data fixture"
Terminal window
git push

Wait for Railway to complete the automatic deployment (check the deployment status in your Railway dashboard).

Load the sample data into your production database.

SSH into your Django service:

Terminal window
railway ssh --service django-pg-htmx

Load the fixture data:

Terminal window
python manage.py loaddata core/fixtures/sample_data.json

Exit the SSH session:

Terminal window
exit

The sample items will be attached to your superuser account (user ID 1).

To remove sample data later:

SSH into your Django service and run:

Terminal window
python manage.py shell -c "from core.models import Item; Item.objects.all().delete()"

Or delete them individually through the web UI at /items/.


Build Your Application

Now that you have a working starter template, you can:

  1. Replace or Extend the Item Model: The Item model is intentionally generic as a demonstration. You can:

    • Keep it and add your own models alongside it
    • Rename it to match your domain (requires updating views, templates, URLs, and forms)
    • Remove it entirely and create your own models from scratch
    • Use it as a reference pattern for building your own CRUD features
  2. Add Your Own Models: Create new models in the core app or create new Django apps

  3. Customize Templates: Modify the templates to match your design

  4. Add More Features: Implement additional functionality as needed

  5. Extend HTMX: Add more HTMX-powered features like search, filtering, sorting, etc.

  6. Add Alpine.js Components: Create more interactive client-side components

  7. Configure Email: Set up email for password resets and notifications

  8. Add API Endpoints: Use Django REST Framework if you need a REST API

  9. Add Tests: Write unit and integration tests for your models and views


Summary

You’ve successfully created a production-ready Django starter template with:

  • ✅ Django 6.0 configured for Railway deployment
  • ✅ PostgreSQL database on Railway
  • ✅ User authentication and profiles
  • ✅ Minimal CRUD operations (Item model)
  • ✅ HTMX integration (pagination, inline editing)
  • ✅ Alpine.js integration (modals, dropdowns)
  • ✅ Tailwind CSS for styling

This template provides a solid foundation for building any Django application with modern tools and best practices.


Don’t have Railway credits yet? Sign up with our referral link to get $20 in credits - enough for a full month on the Pro tier!