Note: Only use npm for package handling in this repo (DO NOT USE YARN).
- Global Setup - Do this FIRST before team members start local setup
- Prerequisites
- Local Setup
- AWS Configuration
- Heroku Deployment
- Other Tips
IMPORTANT: This must be done by ONE person BEFORE any team members start their local setup.
If you are just setting up your local machine as a team member, skip to Local Setup.
Replace myapp with your actual app name in the following files:
Update the following lines:
# Line 7: Change container_name
container_name: <appname>_postgres
# Line 12: Change POSTGRES_DB
POSTGRES_DB: <appname>_dbExample: If your app name is myproject, change:
myapp_postgres→myproject_postgresmyapp_db→myproject_db
If you have a .env.example file in the backend directory, update the DATABASE_URL:
DATABASE_URL=postgresql://postgres:postgres@localhost:5432/<appname>_dbExample: If your app name is myproject, change:
myapp_db→myproject_db
Make sure the database name matches what you set in docker-compose.yml.
After making these changes, commit and push them to the repository so all team members use the same app name.
-
Node.js (for frontend development)
- Version: Node.js 20.19.0 or higher, or Node.js 22.12.0 or higher
- Required by Vite and React dependencies (specified in
package.jsonenginesfield) - Check your version:
node --version - If using
nvm, runnvm usein thefrontenddirectory (uses.nvmrcfile) - Download from nodejs.org if needed
-
Docker Desktop (for local PostgreSQL database)
- Download from docker.com
- Ensure Docker is running before starting the database
This project uses PostgreSQL only (no SQLite fallback). We use Docker to run PostgreSQL locally, so you don't need to install PostgreSQL on your machine.
From the project root directory, start the PostgreSQL container:
docker-compose up -dThis will:
- Download the PostgreSQL 15 image (if not already downloaded)
- Start a PostgreSQL container named
<appname>_postgres - Create a database named
<appname>_db - Expose PostgreSQL on port
5432
Default credentials:
- Username:
postgres - Password:
postgres - Database:
<appname>_db - Port:
5432
Check that the container is running:
docker-compose psYou should see the postgres service running. You can also check the logs:
docker-compose logs postgresIf you have a .env.example file, copy it to .env:
cp backend/.env.example backend/.envThe .env file should contain:
DATABASE_URL=postgresql://postgres:postgres@localhost:5432/<appname>_dbNext Step: After configuring the database, proceed to Backend Setup to set up your backend environment and run migrations.
# Start the database
docker-compose up -d
# Stop the database
docker-compose down
# Stop and remove volumes (deletes all data)
docker-compose down -v
# View database logs
docker-compose logs -f postgres
# Check database status
docker-compose ps
# Access PostgreSQL CLI
docker-compose exec postgres psql -U postgres -d <appname>_dbWhen deploying to Heroku, the DATABASE_URL is automatically set by Heroku. See the Heroku Deployment section below.
First, cd into the backend directory and create a virtual environment:
cd backend
python3 -m venv venv
source venv/bin/activateInstall the required packages:
pip install -r requirements.txtCreate a .vscode directory in the outermost directory (where backend and frontend directories are located).
Create a settings.json file with the following contents:
{
"python.defaultInterpreterPath": "backend/venv/bin/python3",
"python.analysis.extraPaths": ["backend"]
}Then:
- Open VS Code search (
CMD+Shift+P/Ctrl+Shift+P) - Type "python interpreter" and press Enter
- Select "Use Python from python.defaultInterpreterPath setting"
- If it's not there, search "Developer: reload window" and refresh a couple of times
- Once you've selected the Python interpreter, refresh again until the import errors go away
Create a .env file in the /backend directory using .env.example as a template:
cp .env.example .envThe .env file should already contain the correct DATABASE_URL for Docker. No changes needed unless you want to customize the database credentials.
Note: Make sure you've completed the Database Setup with Docker section before proceeding with migrations.
After setting up your .env file and starting the Docker database, create and run the initial migrations:
cd backend
source venv/bin/activate # if not already activated
python manage.py makemigrations core
python manage.py migrateThis will create the necessary database tables for your Django application.
Before installing dependencies, ensure you have the correct Node.js version:
node --versionYou need Node.js 20.19.0+ or 22.12.0+.
If using nvm (Node Version Manager), you can automatically use the correct version:
cd frontend
nvm useThe required version is specified in package.json (engines field) and .nvmrc. If you see warnings about unsupported engine versions, update Node.js.
cd into the frontend directory and install dependencies:
cd frontend
npm ciImportant: Use
npm ci(NOTnpm install). If you are modifyingpackage-lock.json, you are doing it wrong.
To run both the frontend and backend in a local development environment:
cd frontend
npm run start:fullTo just run the backend:
npm run devA full list of commands can be found in package.json under the "scripts" section.
This project supports AWS S3 for file storage and AWS SES (Simple Email Service) for sending emails. Both services use the same AWS credentials.
-
Create IAM User with S3 and SES Permissions:
- Go to IAM Console
- Create a new user with programmatic access
- Attach policies for both
AmazonS3FullAccessandAmazonSESFullAccess(or create custom policies with minimal permissions) - Save the Access Key ID and Secret Access Key
-
Configure Shared Credentials in
.env:AWS_ACCESS_KEY_ID=your-access-key-id AWS_SECRET_ACCESS_KEY=your-secret-access-key
-
Create an S3 Bucket:
- Go to AWS S3 Console
- Create a new bucket
- Note the bucket name and region
-
Configure in
.env:AWS_STORAGE_BUCKET_NAME=your-bucket-name AWS_S3_REGION_NAME=us-east-1
-
Verify Your Email Domain or Email Address:
- Go to AWS SES Console
- If you're in the SES sandbox, verify your email address
- For production, verify your domain to send from any email address on that domain
-
Request Production Access (if needed):
- In SES sandbox, you can only send to verified email addresses
- Request production access to send to any email address
-
Configure in
.env:AWS_SES_REGION_NAME=us-east-1 AWS_SES_REGION_ENDPOINT=email.us-east-1.amazonaws.com SES_FROM_EMAIL=[email protected]
Note:
- SES uses the same
AWS_ACCESS_KEY_IDandAWS_SECRET_ACCESS_KEYas S3 SES_FROM_EMAILmust be a verified email address or domain in AWS SES- If AWS credentials or
SES_FROM_EMAILare not provided, the application will use the console email backend (emails printed to console) for development
- SES uses the same
-
Test Email Sending:
from django.core.mail import send_mail send_mail( 'Subject', 'Message body', '[email protected]', ['[email protected]'], fail_silently=False, )
- Install the Heroku CLI
- Create a Heroku account at heroku.com
- Login to Heroku:
heroku login
-
Create a Heroku App:
heroku create your-app-name
-
Add PostgreSQL Add-on:
heroku addons:create heroku-postgresql:mini
The
miniplan is free for development. For production, consider upgrading to a paid plan.This automatically sets the
DATABASE_URLenvironment variable. -
Set Environment Variables:
heroku config:set SECRET_KEY=your-secret-key-here heroku config:set ENVIRONMENT=production heroku config:set ALLOWED_HOSTS=your-app-name.herokuapp.com
Set any other environment variables from your
.envfile as needed:# AWS Credentials (shared for S3 and SES) heroku config:set AWS_ACCESS_KEY_ID=your-key heroku config:set AWS_SECRET_ACCESS_KEY=your-secret # AWS S3 (for file storage) heroku config:set AWS_STORAGE_BUCKET_NAME=your-bucket heroku config:set AWS_S3_REGION_NAME=us-east-1 # AWS SES (for sending emails) heroku config:set AWS_SES_REGION_NAME=us-east-1 heroku config:set AWS_SES_REGION_ENDPOINT=email.us-east-1.amazonaws.com heroku config:set [email protected]
-
Deploy Backend:
cd backend git subtree push --prefix backend heroku mainOr if using a separate Heroku git remote:
git remote add heroku https://git.heroku.com/your-app-name.git git subtree push --prefix backend heroku main
-
Run Migrations:
heroku run python manage.py migrate
-
Create Superuser (optional):
heroku run python manage.py createsuperuser
For the frontend, you can:
-
Deploy to Heroku (with a buildpack):
heroku create your-frontend-app-name heroku buildpacks:set heroku/nodejs cd frontend git subtree push --prefix frontend heroku main -
Or deploy to Vercel/Netlify:
- Connect your GitHub repository
- Set the build directory to
frontend - Configure build command:
npm ci && npm run build - Set publish directory to
frontend/dist
# View logs
heroku logs --tail
# Open app in browser
heroku open
# Run Django shell
heroku run python manage.py shell
# Check database connection
heroku pg:info
# View all config vars
heroku config
# Restart dynos
heroku restart- Build routes in
core/views.py - Add them to
config/urls.py
Example:
# core/views.py
from django.http import JsonResponse
def my_view(request):
return JsonResponse({"message": "Hello, World!"})# config/urls.py
from django.urls import path
from core.views import my_view
urlpatterns = [
path('api/my-endpoint/', my_view, name='my_view'),
]Models define your database structure. Create models in core/models.py.
from django.db import models
class MyModel(models.Model):
# Field definitions here
class Meta:
ordering = ['-created_at'] # Optional: default ordering
def __str__(self):
return self.name # String representation for admin/Django shellclass Example(models.Model):
# Text fields
name = models.CharField(max_length=100) # Short text, required
description = models.TextField() # Long text, required
bio = models.TextField(null=True, blank=True) # Optional long text
# Numbers
age = models.IntegerField() # Integer, required
price = models.DecimalField(max_digits=10, decimal_places=2) # Decimal
score = models.IntegerField(default=0) # With default value
# Boolean
is_active = models.BooleanField(default=True)
is_published = models.BooleanField(default=False)
# Dates
created_at = models.DateTimeField(auto_now_add=True) # Set on creation
updated_at = models.DateTimeField(auto_now=True) # Updated on save
# Foreign keys (see Relationships section below)
# Many-to-many (see Relationships section below)These are two different concepts that work together:
-
null=True: Allows the database column to storeNULLvalues- Database-level constraint
- Use for fields that might not have a value
-
blank=True: Allows the field to be empty in forms/admin- Validation-level constraint
- Use for fields that are optional in forms
Common combinations:
# Required field (default)
name = models.CharField(max_length=100)
# Database: NOT NULL, Forms: Required
# Optional field (can be empty in DB and forms)
description = models.TextField(null=True, blank=True)
# Database: NULL allowed, Forms: Optional
# Field with default (recommended for optional fields)
score = models.IntegerField(default=0)
# Database: NOT NULL (defaults to 0), Forms: Optional (shows 0)
# Date that's auto-set (doesn't need null/blank)
created_at = models.DateTimeField(auto_now_add=True)
# Database: NOT NULL, Forms: Hidden/auto-setWhen to use defaults:
-
Use
default=when: You want a sensible fallback valuescore = models.IntegerField(default=0) # Better than null=True is_active = models.BooleanField(default=True)
-
Use
null=True, blank=Truewhen: The field is truly optional and has no sensible defaultmiddle_name = models.CharField(max_length=50, null=True, blank=True) bio = models.TextField(null=True, blank=True)
-
Use
null=True, blank=True, default=Nonewhen: You want to explicitly track "not set" vs "empty string"# For CharField/TextField, empty string '' vs None can be different optional_text = models.CharField(max_length=100, null=True, blank=True, default=None)
A ForeignKey creates a many-to-one relationship. One model "belongs to" another.
from django.conf import settings
class Module(models.Model):
course = models.ForeignKey(
Course,
on_delete=models.CASCADE, # Delete module if course is deleted
related_name='modules' # Access via course.modules.all()
)
name = models.CharField(max_length=100)on_delete options:
CASCADE: Delete this object when parent is deleted (default for most cases)PROTECT: Prevent deletion of parent if children existSET_NULL: Set to NULL when parent is deleted (requiresnull=True)SET_DEFAULT: Set to default value when parent is deletedDO_NOTHING: Don't do anything (not recommended)
Usage:
# Create
module = Module.objects.create(course=my_course, name="Intro")
# Access parent
module.course # Returns Course object
# Access children (via related_name)
course.modules.all() # All modules for this courseCreates a many-to-many relationship. Both models can have multiple of the other.
from django.conf import settings
class Course(models.Model):
name = models.CharField(max_length=100)
# Many-to-many with User model
students = models.ManyToManyField(
settings.AUTH_USER_MODEL, # Use settings.AUTH_USER_MODEL, not 'User'
related_name='courses_as_student',
blank=True # Can have no students initially
)
teachers = models.ManyToManyField(
settings.AUTH_USER_MODEL,
related_name='courses_as_teacher',
blank=True
)Important: Always use settings.AUTH_USER_MODEL (string) or import get_user_model() for the User model, not 'User' directly.
Usage:
# Add relationships
course.students.add(user1, user2)
course.teachers.add(teacher1)
# Remove relationships
course.students.remove(user1)
# Clear all
course.students.clear()
# Check if user is in course
if user in course.students.all():
pass
# Access from User side (via related_name)
user.courses_as_student.all() # All courses where user is a student
user.courses_as_teacher.all() # All courses where user is a teacherMany-to-Many Through Table (Custom Intermediate Model):
If you need to store extra data about the relationship, use through:
class Course(models.Model):
name = models.CharField(max_length=100)
students = models.ManyToManyField(
settings.AUTH_USER_MODEL,
through='Enrollment', # Custom intermediate model
related_name='enrolled_courses'
)
class Enrollment(models.Model):
course = models.ForeignKey(Course, on_delete=models.CASCADE)
student = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
enrolled_at = models.DateTimeField(auto_now_add=True)
grade = models.IntegerField(null=True, blank=True)
class Meta:
unique_together = [['course', 'student']] # Prevent duplicate enrollmentsUsage with through:
# Create enrollment with extra data
Enrollment.objects.create(course=course, student=user, grade=95)
# Access still works
course.students.all() # All students
user.enrolled_courses.all() # All courses
# Access intermediate model
enrollment = Enrollment.objects.get(course=course, student=user)
enrollment.grade # Access extra dataCreates a one-to-one relationship. Each instance of one model relates to exactly one instance of another.
class UserProfile(models.Model):
user = models.OneToOneField(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
related_name='profile'
)
bio = models.TextField(blank=True)
avatar = models.ImageField(upload_to='avatars/', null=True, blank=True)Usage:
# Create
profile = UserProfile.objects.create(user=user, bio="Hello")
# Access
user.profile # Returns UserProfile (or raises DoesNotExist)
user.profile.bio
# Check if exists
if hasattr(user, 'profile'):
passfrom django.db import models
from django.conf import settings
from django.contrib.auth import get_user_model
User = get_user_model() # Or use settings.AUTH_USER_MODEL as string
class Course(models.Model):
name = models.CharField(max_length=100)
description = models.TextField(blank=True)
created_at = models.DateTimeField(auto_now_add=True)
# Many-to-many relationships
students = models.ManyToManyField(
User,
related_name='student_courses',
blank=True
)
teachers = models.ManyToManyField(
User,
related_name='teacher_courses',
blank=True
)
def __str__(self):
return self.name
class Module(models.Model):
course = models.ForeignKey(
Course,
on_delete=models.CASCADE,
related_name='modules'
)
title = models.CharField(max_length=200)
order = models.IntegerField(default=0)
is_published = models.BooleanField(default=False)
class Meta:
ordering = ['order']
def __str__(self):
return f"{self.course.name} - {self.title}"
class Grade(models.Model):
student = models.ForeignKey(User, on_delete=models.CASCADE)
module = models.ForeignKey(Module, on_delete=models.CASCADE)
score = models.IntegerField(default=0)
total = models.IntegerField(default=100)
graded_at = models.DateTimeField(auto_now=True)
class Meta:
unique_together = [['student', 'module']] # One grade per student per module
def __str__(self):
return f"{self.student.username} - {self.module.title}: {self.score}/{self.total}"-
Create migrations:
python manage.py makemigrations core
-
Review migrations (optional):
python manage.py sqlmigrate core 0001
-
Apply migrations:
python manage.py migrate
-
Register in admin (optional):
# core/admin.py from django.contrib import admin from .models import Course, Module, Grade admin.site.register(Course) admin.site.register(Module) admin.site.register(Grade)
- Docker not running: Ensure Docker Desktop is running before starting the database
- Container not started: Run
docker-compose up -dfrom the project root - Port conflict: If port 5432 is already in use, stop other PostgreSQL instances or change the port in
docker-compose.yml - Verify DATABASE_URL: Check that
DATABASE_URLin.envmatches your database name (e.g.,postgresql://postgres:postgres@localhost:5432/<appname>_db) - Check container status: Run
docker-compose psto see if the container is running - View logs: Run
docker-compose logs postgresto see database logs
- Reload the window:
CMD+Shift+P→ "Developer: Reload Window" - Verify Python interpreter is set correctly
- Check that
python.analysis.extraPathsincludes"backend"in.vscode/settings.json
- "Unsupported engine" warnings: Update Node.js to version 20.19.0+ or 22.12.0+
- Check current version:
node --version - Download the latest LTS version from nodejs.org
- If using
nvm, run:nvm install 20.19.0ornvm install 22.12.0 - After updating, delete
node_modulesandpackage-lock.json, then runnpm ciagain
- Check current version:
[Add your license here]