Multi-tenant Django foundation with Stripe billing, custom user model, and production-hardened settings β launch your SaaS in days.
django-saas-boilerplate/
βββ config/
β βββ settings/
β β βββ base.py # Shared Django settings
β β βββ development.py # Dev overrides
β β βββ production.py # Production hardening
β βββ urls.py # Root URL configuration
βββ apps/
β βββ accounts/
β β βββ models.py # Custom User model
β β βββ serializers.py # DRF serializers
β β βββ views.py # Auth & profile ViewSets
β β βββ urls.py # Account routes
β βββ tenants/
β β βββ models.py # Tenant & plan models
β β βββ middleware.py # Subdomain β tenant
β βββ billing/
β βββ models.py # Subscription & Invoice
β βββ webhooks.py # Stripe webhook handler
βββ templates/
β βββ base.html # HTMX + Alpine.js base
βββ docker/
β βββ Dockerfile # Multi-stage Django build
β βββ docker-compose.yml # Full stack
βββ manage.py
βββ manifest.json
The boilerplate separates settings by environment so local convenience never weakens production security. config/settings/base.py contains shared apps, middleware, REST Framework, PostgreSQL, Redis, Stripe, and path settings. development.py imports the base module and enables debug tooling; production.py adds secure cookies, HSTS, Whitenoise, S3, Redis caching, and Sentry. This explicit import pattern provides the same benefit as django-split-settings without another dependency. Set the module when starting Django:
export DJANGO_SETTINGS_MODULE=config.settings.development
python manage.py runserver
# Production processes use:
export DJANGO_SETTINGS_MODULE=config.settings.productionCopy .env.example to .env, keep it out of version control, and provide at least SECRET_KEY, database credentials, Stripe keys, and REDIS_URL. The supplied settings use os.environ; teams that prefer typed values can add django-environ (or use equivalent python-decouple calls):
# config/settings/base.py
import environ
env = environ.Env(DEBUG=(bool, False))
environ.Env.read_env(BASE_DIR / ".env") # local only
SECRET_KEY = env("SECRET_KEY")
DEBUG = env.bool("DEBUG")
DATABASES = {"default": env.db("DATABASE_URL")}
STRIPE_SECRET_KEY = env("STRIPE_SECRET_KEY", default="")Never give production secrets a usable default. In hosted environments, inject them through the platformβs secret manager rather than shipping an .env file.
Lists need deliberate parsing. For example, split ALLOWED_HOSTS and CORS_ALLOWED_ORIGINS on commas, strip whitespace, and reject empty production values. Boolean strings also need typed conversion: Python treats the nonempty string "false" as true. Fail during startup when a required value is absent, so a broken release never reaches customer traffic.
PostgreSQL is the production target and the default bundled backend. For local parity, run PostgreSQL through Docker and use DB_NAME, DB_USER, DB_PASSWORD, DB_HOST, and DB_PORT. SQLite is acceptable for quick UI work, but it will not expose PostgreSQL-specific locking, indexing, or concurrency behavior:
# config/settings/development.py
if os.environ.get("USE_SQLITE") == "1":
DATABASES = {
"default": {
"ENGINE": "django.db.backends.sqlite3",
"NAME": BASE_DIR / "db.sqlite3",
}
}Run migrations whenever models or branches change: python manage.py migrate. Use PostgreSQL in CI before merging database-sensitive work.
STATIC_ROOT receives collected assets; MEDIA_ROOT stores local uploads during development. In production, Whitenoise serves versioned static assets from the application image, while django-storages sends private user uploads to S3:
MIDDLEWARE.insert(1, "whitenoise.middleware.WhiteNoiseMiddleware")
STATICFILES_STORAGE = (
"whitenoise.storage.CompressedManifestStaticFilesStorage"
)
DEFAULT_FILE_STORAGE = "storages.backends.s3boto3.S3Boto3Storage"
AWS_DEFAULT_ACL = "private"Run python manage.py collectstatic --noinput during the image build. Do not serve customer uploads through Whitenoise; use short-lived signed S3 URLs.
For a native setup, install requirements, start PostgreSQL and Redis, migrate, then run python manage.py runserver. The bundled Compose stack is closer to production:
docker compose -f docker/docker-compose.yml up --build -d
docker compose -f docker/docker-compose.yml exec app python manage.py migrate
docker compose -f docker/docker-compose.yml exec app python manage.py createsuperuserrunserver is development-only. The Docker image should start Gunicorn behind Nginx, Cloudflare, or the hosting load balancer:
gunicorn config.wsgi:application --bind 0.0.0.0:8000 \
--workers 3 --timeout 60 --access-logfile -Before routing traffic, run python manage.py check --deploy, apply migrations as a release step, collect static files, and verify /admin/, API authentication, Redis/Celery, and the Stripe webhook endpoint. Terminate TLS at the proxy and forward X-Forwarded-Proto; the production settings already enforce HTTPS and secure cookies.
Get the full Django SaaS Boilerplate and unlock everything.
Get the complete guide with every chapter unlocked, including code samples, diagrams, and best practices.
Access all interactive tools with complete data, all workload profiles, and the full scenario library.
Downloadable source code, configuration files, and working examples from every chapter.
Free updates for life. Every new chapter, tool, and improvement included.