Skip to content

Django Settings Management

Overview

This document describes the theoretical foundations and practical implementation patterns for Django settings management in multi-environment deployments. The approach follows Django 5.2+ best practices (2025) and demonstrates production-grade configuration patterns based on real-world multi-tenant, multi-database applications.

Settings Organization Philosophy

Separation of Concerns

Django settings management follows a hierarchical composition pattern where:

  1. Base settings define application-wide defaults and constants
  2. Environment-specific settings override or extend base configuration
  3. Database configurations isolate connection logic from application logic
  4. Utility modules provide reusable configuration components (logging, caching, etc.)

This separation ensures:

  • Clear distinction between environment-agnostic and environment-specific configuration
  • Reduced duplication through inheritance and composition
  • Isolated changes when modifying environment-specific behavior
  • Testable configuration through predictable import patterns

Import Hierarchy

Settings files form a directed acyclic graph (DAG) of dependencies:

base.py (foundation)
    ├── development.py
    │   └── development_databases.py
    ├── production.py
    │   └── production_databases.py
    │       └── pconfig (AWS SSM)
    └── production-local.py
        ├── production.py
        └── base.py

testing_databases.py ← development_databases.py
logging_config.py (standalone utility)

The import pattern uses wildcard imports (from .base import *) followed by selective overrides, allowing child settings to inherit all base configuration while explicitly modifying specific values.

Base + Environment-Specific Pattern

Base Settings (base.py)

The base settings file establishes the foundation for all environments. It contains:

Application Configuration

INSTALLED_APPS = [
    # Django core apps
    "django.contrib.admin",
    "django.contrib.auth",
    "django.contrib.contenttypes",
    "django.contrib.sessions",
    "django.contrib.messages",
    "django.contrib.staticfiles",

    # Third-party apps
    "rest_framework",
    "corsheaders",
    "django_filters",
    "waffle",
    "social_django",
    "storages",

    # Project apps (last)
    "poseidon.apps.PoseidonConfig",
]

Theory: Installed apps are ordered from most foundational (Django core) to most specific (project apps). Third-party apps that modify core Django behavior (like corsheaders) should be positioned appropriately in middleware order.

Middleware Stack

MIDDLEWARE = [
    "django.middleware.security.SecurityMiddleware",
    "django.contrib.sessions.middleware.SessionMiddleware",
    "corsheaders.middleware.CorsMiddleware",  # Before CommonMiddleware
    "django.middleware.common.CommonMiddleware",
    "django.middleware.csrf.CsrfViewMiddleware",
    "poseidon.middleware.sso_tenant_middleware.SSOTenantMiddleware",
    "django.contrib.auth.middleware.AuthenticationMiddleware",
    "hijack.middleware.HijackUserMiddleware",
    "django.contrib.messages.middleware.MessageMiddleware",
    "django.middleware.clickjacking.XFrameOptionsMiddleware",
    "waffle.middleware.WaffleMiddleware",
]

Theory: Middleware order matters. Request processing flows top-to-bottom; response processing flows bottom-to-top. Security middleware should be early to protect all subsequent processing. Authentication middleware must precede any middleware requiring user context.

Template Configuration

TEMPLATES = [
    {
        "BACKEND": "django.template.backends.django.DjangoTemplates",
        "DIRS": [os.path.join(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",
            ],
        },
    }
]

Theory: Template directories follow a precedence order: explicit DIRS paths override APP_DIRS. Context processors inject variables into all template contexts, enabling DRY principles for commonly-used data (user, request, messages).

REST Framework Configuration

REST_FRAMEWORK = {
    "DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination",
    "PAGE_SIZE": 100,
    "DEFAULT_AUTHENTICATION_CLASSES": ("poseidon.models.BearerAuthentication",),
    "DEFAULT_PERMISSION_CLASSES": ("rest_framework.permissions.IsAuthenticated",),
    "DEFAULT_FILTER_BACKENDS": ("django_filters.rest_framework.DjangoFilterBackend",),
    "DEFAULT_RENDERER_CLASSES": [
        "rest_framework.renderers.JSONRenderer",
        "rest_framework.renderers.BrowsableAPIRenderer",
    ],
    "DEFAULT_THROTTLE_CLASSES": [
        "rest_framework.throttling.AnonRateThrottle",
        "rest_framework.throttling.UserRateThrottle",
    ],
    "DEFAULT_THROTTLE_RATES": {"anon": "100/day", "user": "2500/minute"},
}

Theory: DRF defaults establish API-wide conventions. Authentication and permission classes define the security model. Throttling prevents abuse while allowing legitimate high-volume usage. Pagination prevents resource exhaustion from unbounded queries.

SESSION_ENGINE = "django.contrib.sessions.backends.db"
SESSION_EXPIRE_AT_BROWSER_CLOSE = False
SESSION_COOKIE_AGE = 60 * 60 * 24 * 7 * 2  # 2 weeks
SESSION_COOKIE_SECURE = True
SESSION_COOKIE_SAMESITE = "None"

CSRF_COOKIE_SAMESITE = "None"
CSRF_COOKIE_SECURE = True

Theory: Session configuration balances security and user experience. Database-backed sessions enable server-side session management. SameSite=None with Secure=True enables cross-origin authentication flows (required for SSO and API integrations). The 2-week timeout balances security (shorter is safer) with UX (longer reduces re-authentication).

CORS Configuration

CORS_ALLOWED_ORIGIN_REGEXES = [
    r"^https://[-\w]+\.planion\.com$",
    r"^http://localhost:\d+$",
    r"^http://127\.0\.0\.1:\d+$",
]

CORS_ALLOW_HEADERS = (
    *default_headers,
    "ps-user-session",  # Custom header
)

Theory: CORS configuration enables controlled cross-origin access. Regex patterns provide flexible domain matching while maintaining security. Custom headers must be explicitly allowed. The pattern supports both production subdomains and local development.

Sentry Integration

IN_AWS_FARGATE = "ECS_CONTAINER_METADATA_URI" in os.environ

# Disable by default
sentry_sdk.init(
    dsn=None,
    integrations=[],
    default_integrations=False,
    auto_enabling_integrations=False,
)

# Enable in production
if IN_AWS_FARGATE:
    sentry_sdk.init(
        dsn="https://...",
        integrations=[DjangoIntegration()],
        traces_sample_rate=0.5,
        send_default_pii=True,
    )

Theory: Environment detection enables conditional service initialization. Sentry is disabled by default (safe for development) and explicitly enabled in production via environment detection. This prevents development errors from polluting production monitoring.

Static Files Versioning

def get_tailwind_static_version():
    BASE_DIR = Path(__file__).resolve().parent.parent
    version_file = BASE_DIR / "static/css/version.txt"
    if version_file.exists():
        return version_file.read_text().strip()
    return ""

TAILWIND_STATIC_VERSION = get_tailwind_static_version()

Theory: Build-time versioning enables cache busting for static assets. The version is embedded at build time and read at Django initialization. This approach eliminates the need for URL query parameters or content hashing in URLs.

Development Settings (development.py)

Development settings optimize for developer productivity and debugging capabilities.

Import Pattern

import logging
import os
import boto3
from .base import *
from .base import BASE_DIR, INSTALLED_APPS, MIDDLEWARE, SILENCED_SYSTEM_CHECKS
from .development_databases import DATABASES

Theory: Explicit imports after wildcard ensure variables are available for modification. Re-importing INSTALLED_APPS and MIDDLEWARE allows in-place list modification (+=) rather than replacement.

Debug Mode

DEBUG = True
LOGGING["loggers"][""]["level"] = "INFO"

Theory: DEBUG=True enables detailed error pages, automatic template reloading, and SQL query logging. Log level is lowered to INFO for development verbosity without DEBUG-level noise.

Local Service Mocking

ssm = boto3.client(
    "ssm",
    region_name="us-east-1",
    endpoint_url="http://localhost:4566",  # LocalStack
    aws_access_key_id="dummy",
    aws_secret_access_key="dummy",
    aws_session_token="dummy",
)

Theory: LocalStack provides local AWS service emulation. This enables development without AWS credentials or network access, improving iteration speed and enabling offline development.

Cache Configuration

CACHES = {
    "default": {
        "BACKEND": "django.core.cache.backends.redis.RedisCache",
        "LOCATION": "redis://redis:6379/1",
    }
}

Theory: Local Redis in Docker Compose provides production-like caching behavior in development. Using database 1 (not 0) isolates cache from other potential Redis usage.

Development-Only Apps

INSTALLED_APPS += ["debug_toolbar"]
MIDDLEWARE += ["debug_toolbar.middleware.DebugToolbarMiddleware"]

Theory: Debug toolbar provides SQL query inspection, template rendering analysis, and request/response debugging. It's added via += to preserve base app order while appending development tools.

Security Relaxation

SILENCED_SYSTEM_CHECKS += [
    "security.W004",  # SECURE_HSTS_SECONDS
    "security.W008",  # SECURE_SSL_REDIRECT
    "security.W012",  # SESSION_COOKIE_SECURE
    "security.W018",  # CSRF_COOKIE_SECURE
]

SESSION_COOKIE_SECURE = False  # Allow HTTP
CSRF_COOKIE_SECURE = False
SESSION_COOKIE_SAMESITE = "Lax"
CSRF_COOKIE_SAMESITE = "Lax"

Theory: Development often runs over HTTP (not HTTPS). Security checks are silenced to prevent noise. Cookie settings are relaxed (Lax instead of None, Secure=False) to support local development workflows.

Security Implications

These relaxed settings are appropriate only for local development. Never deploy to production with DEBUG=True or SECURE=False cookie settings.

CSRF Trusted Origins

CSRF_TRUSTED_ORIGINS = [
    "https://*.planion.com",
    "http://*.planion.com",
    "http://localhost",
    "https://localhost",
    "http://localhost:8000",
    "https://localhost:8000",
    "http://localhost:443",
    "https://localhost:443",
    "http://127.0.0.1",
    "https://127.0.0.1",
    "https://planion-demoauth.auth0.com",
]

Theory: Extensive localhost variations accommodate different development setups (direct Django, nginx proxy, HTTPS development certificates). Auth0 domain enables SSO testing in development.

Environment Secrets

SECRET_KEY = os.getenv("DJANGO_SECRET_KEY")
STAFF_TOKEN_SECRET = os.getenv("STAFF_TOKEN_SECRET")
GENERAL_TOKEN_SECRET = os.getenv("GENERAL_TOKEN_SECRET")
SOCIAL_AUTH_AUTH0_KEY = os.getenv("SOCIAL_AUTH_AUTH0_KEY")
SOCIAL_AUTH_AUTH0_SECRET = os.getenv("SOCIAL_AUTH_AUTH0_SECRET")

Theory: Even in development, secrets are loaded from environment variables (not hardcoded). This establishes the pattern for production and prevents accidental secret commits.

Email Configuration

EMAIL_HOST = "mailhog"
EMAIL_PORT = 1025
EMAIL_HOST_PASSWORD = "test"

Theory: MailHog captures all outgoing emails for local inspection without sending real emails. This prevents accidental email sends during development and enables email flow testing.

Static Files

STATIC_URL = "/static/"
STATIC_ROOT = os.path.join(BASE_DIR, "static_root")
TAILWIND_STATIC_VERSION = ""  # No cache busting in development

Theory: Development serves static files directly via Django's development server. Cache busting is disabled (empty version string) to simplify debugging and allow immediate CSS refresh.

Production Settings (production.py)

Production settings prioritize security, performance, and operational visibility.

Import Pattern

from .base import *
from .production_databases import DATABASES
from poseidon.commons.config.ps_config import pconfig

Theory: Production imports from AWS SSM-backed configuration manager (pconfig). Database configuration is isolated to enable independent scaling and security hardening.

Security Hardening

DEBUG = False

SECURE_SSL_REDIRECT = False  # Load balancer handles this
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
SECURE_REFERRER_POLICY = "same-origin"
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
SECURE_BROWSER_XSS_FILTER = True
SECURE_CONTENT_TYPE_NOSNIFF = True
X_FRAME_OPTIONS = "DENY"
SECURE_CROSS_ORIGIN_OPENER_POLICY = None

Theory: Security headers protect against common web vulnerabilities. SECURE_SSL_REDIRECT=False because AWS load balancer handles SSL termination and redirection (doing it twice causes redirect loops). SECURE_PROXY_SSL_HEADER tells Django to trust the load balancer's X-Forwarded-Proto header.

AWS S3 Static Files

AWS_STORAGE_BUCKET_NAME = "planstone-poseidon-static-files"
AWS_DEFAULT_ACL = "public-read"
AWS_LOCATION = "static-v2"
AWS_S3_CUSTOM_DOMAIN = f"{AWS_STORAGE_BUCKET_NAME}.s3.amazonaws.com"
AWS_S3_OBJECT_PARAMETERS = {"CacheControl": "max-age=86400"}

STATIC_URL = f"https://{AWS_S3_CUSTOM_DOMAIN}/{AWS_LOCATION}/"

STORAGES = {
    "default": {
        "BACKEND": "storages.backends.s3.S3Storage",
        "OPTIONS": {
            "bucket_name": AWS_STORAGE_BUCKET_NAME,
            "location": "",  # Media files at root
            "default_acl": AWS_DEFAULT_ACL,
            "object_parameters": AWS_S3_OBJECT_PARAMETERS,
            "custom_domain": AWS_S3_CUSTOM_DOMAIN,
            # IAM role provides credentials
        },
    },
    "staticfiles": {
        "BACKEND": "storages.backends.s3.S3Storage",
        "OPTIONS": {
            "bucket_name": AWS_STORAGE_BUCKET_NAME,
            "location": AWS_LOCATION,  # static-v2 prefix
            "default_acl": AWS_DEFAULT_ACL,
            "object_parameters": AWS_S3_OBJECT_PARAMETERS,
            "custom_domain": AWS_S3_CUSTOM_DOMAIN,
        },
    },
}

Theory: S3 serves static and media files for scalability and reliability. Separate storage backends enable different path prefixes (static vs media). Cache headers (max-age=86400) enable CDN and browser caching. IAM role authentication (no hardcoded credentials) follows AWS security best practices.

Database Routing

DATABASE_ROUTERS = ["poseidon.db_routers.DBRouter"]

Theory: Custom database router enables multi-database, multi-tenant architecture. The router directs queries to appropriate tenant databases and implements read/write splitting (read replicas for queries, primary for writes).

Production-Local Settings (production-local.py)

Production-local creates a hybrid environment: production configuration with development debugging tools.

from .base import *
from .base import INSTALLED_APPS, MIDDLEWARE
from .production import *

DEBUG = True
SECURE_SSL_REDIRECT = False

INSTALLED_APPS += ["debug_toolbar"]
MIDDLEWARE += ["debug_toolbar.middleware.DebugToolbarMiddleware"]

Theory: This environment is useful for:

  1. Production debugging: Investigate issues with production-like configuration
  2. Performance profiling: Profile with production settings but debug toolbar visibility
  3. Pre-deployment validation: Test production config without deploying

The import order is critical: baseproduction → overrides. This ensures production settings are active but selectively relaxed.

Security Warning

Never use production-local settings in actual production deployments. DEBUG=True exposes sensitive information and disables security features.

Database Configuration by Environment

Development Databases (development_databases.py)

DATABASES = {
    "default": {
        "ENGINE": "django.db.backends.mysql",
        "NAME": "poseidon",
        "HOST": "db",  # Docker Compose service name
        "USER": "root",
        "PASSWORD": "Vanadium23",  # Local development only
        "OPTIONS": {"charset": "utf8mb4"},
    },
    "poseidon": { ... },
    "accounts": { ... },
    "accounts_RO": { ... },  # Read replica (same as primary in dev)
    # Tenant databases
    "AAPT": { ... },
    "AOSSM": { ... },
    "ACMG": { ... },
    # ... more tenants
}

Theory: Development uses a single MySQL instance with multiple databases. Read-only database aliases point to the same instance (no actual replication in development). This simplifies local setup while maintaining production-like database routing code.

Production Databases (production_databases.py)

from poseidon.commons.config.ps_config import pconfig

POSEIDON_DATABASE_USERNAME = pconfig.get_param("POSEIDON_DATABASE_USERNAME")
POSEIDON_DATABASE_PASSWORD = pconfig.get_param("POSEIDON_DATABASE_PASSWORD")
DATABASE_RO_CONNECTION_STRING = "planion-proxy-1-read-only.endpoint.proxy-..."
DATABASE_RW_CONNECTION_STRING = "planion-proxy-1.proxy-..."
TENANTS = pconfig.get_param("PLANSTONE_TENANTS_LIST").split(",")

DATABASES = {
    "default": {
        "ENGINE": "django.db.backends.mysql",
        "NAME": "poseidon",
        "USER": POSEIDON_DATABASE_USERNAME,
        "PASSWORD": POSEIDON_DATABASE_PASSWORD,
        "HOST": DATABASE_RW_CONNECTION_STRING,
    },
    "poseidon_RO": {
        "ENGINE": "django.db.backends.mysql",
        "NAME": "poseidon",
        "USER": POSEIDON_DATABASE_USERNAME,
        "PASSWORD": POSEIDON_DATABASE_PASSWORD,
        "HOST": DATABASE_RO_CONNECTION_STRING,  # Read replica
    },
    # ... base databases
}

# Dynamic tenant database configuration
for tenant in TENANTS:
    tenant = tenant.strip().upper()
    if tenant:
        DATABASES[f"{tenant}"] = {
            "ENGINE": "django.db.backends.mysql",
            "NAME": tenant,
            "USER": POSEIDON_DATABASE_USERNAME,
            "PASSWORD": POSEIDON_DATABASE_PASSWORD,
            "HOST": DATABASE_RW_CONNECTION_STRING,
            "OPTIONS": {"charset": "utf8mb4"},
        }
        DATABASES[f"{tenant}_RO"] = {
            "ENGINE": "django.db.backends.mysql",
            "NAME": tenant,
            "USER": POSEIDON_DATABASE_USERNAME,
            "PASSWORD": POSEIDON_DATABASE_PASSWORD,
            "HOST": DATABASE_RO_CONNECTION_STRING,
        }

Theory: Production uses:

  1. AWS RDS Proxy: Connection pooling and failover (DATABASE_RW_CONNECTION_STRING)
  2. Read replicas: Separate endpoints for read-only queries (DATABASE_RO_CONNECTION_STRING)
  3. Dynamic tenant configuration: Tenant list from SSM enables adding tenants without code changes
  4. Credential management: Database credentials from SSM (never hardcoded)

The _RO suffix convention signals read-only usage to the database router.

Testing Databases (testing_databases.py)

from .development_databases import DATABASES as DEV_DATABASES
import logging

logger = logging.getLogger(__name__)

DATABASES = {}

DEFAULT_TEST_DB = "test_poseidon"
TENANT_TEST_DB = "test_testclient"

for alias, config in DEV_DATABASES.items():
    DATABASES[alias] = config.copy()

    if alias in ("TESTCLIENT", "TESTCLIENT_RO"):
        DATABASES[alias].setdefault("TEST", {})["NAME"] = TENANT_TEST_DB
    elif alias == "default":
        DATABASES[alias].setdefault("TEST", {})["NAME"] = DEFAULT_TEST_DB
    else:
        # All other databases mirror the default test database
        DATABASES[alias].setdefault("TEST", {})["MIRROR"] = "default"

    DATABASES[alias].setdefault("TEST", {})["SERIALIZE"] = False
    DATABASES[alias].setdefault("TEST", {})["CREATE_DB"] = True

Theory: Test database configuration optimizes for speed and isolation:

  1. Database mirroring: Most database aliases share a single test database (MIRROR = "default"), reducing creation overhead
  2. Selective isolation: Only TESTCLIENT gets a separate database (for tenant-specific tests)
  3. Parallel execution: SERIALIZE = False enables parallel test execution
  4. Explicit creation: CREATE_DB = True ensures Django creates test databases

This reduces test database creation from N databases to 2, dramatically improving test startup time.

Environment Variable Loading

Development Pattern

import os

SECRET_KEY = os.getenv("DJANGO_SECRET_KEY")
STAFF_TOKEN_SECRET = os.getenv("STAFF_TOKEN_SECRET")

Theory: os.getenv() reads from the process environment. In development:

  • Docker Compose sets variables via environment: or env_file:
  • .env files provide local overrides
  • No default values (fail fast if missing)

Production Pattern

from poseidon.commons.config.ps_config import pconfig

SECRET_KEY = pconfig.get_param("DJANGO_SECRET_KEY")
STAFF_TOKEN_SECRET = pconfig.get_param("STAFF_TOKEN_SECRET")

Theory: pconfig is a custom configuration manager that:

  1. Retrieves parameters from AWS Systems Manager (SSM) Parameter Store
  2. Validates parameter names against an allow list
  3. Provides dynamic parameter support (e.g., RD_API_USERNAME_ prefix matching)
  4. Fails fast with descriptive errors if parameters are missing

The PlanstoneConfig class maintains a whitelist of known parameters:

class PlanstoneConfig:
    _params = [
        "DJANGO_SECRET_KEY",
        "EMAIL_HOST_PASSWORD",
        "POSEIDON_DATABASE_USERNAME",
        "POSEIDON_DATABASE_PASSWORD",
        "SOCIAL_AUTH_AUTH0_KEY",
        "SOCIAL_AUTH_AUTH0_SECRET",
        # ... 100+ parameters
    ]

    def get_param(self, param_name, allow_dynamic=True):
        if param_name not in self._params:
            if not allow_dynamic:
                raise ValueError(f"Unknown parameter: {param_name}")

            # Check dynamic patterns (e.g., RD_API_USERNAME_*)
            if not any(param_name.startswith(base) for base in self._params):
                raise ValueError(f"Unknown parameter: {param_name}")

        return self.ssm_manager._get_parameter_from_ssm(param_name)

Theory: The whitelist approach provides:

  1. Security: Only expected parameters are retrieved (prevents typos or malicious parameter access)
  2. Documentation: The _params list documents all external configuration
  3. Validation: Fail fast if requesting non-existent parameters
  4. Flexibility: Dynamic patterns support tenant-specific configuration

Secret Management

Critical Security Rule

Secrets must never be committed to version control or hardcoded in settings files.

Development Secret Management

Development uses environment variables defined in:

  1. Docker Compose (docker-compose.yml):

    environment:
      DJANGO_SECRET_KEY: dev-secret-key-change-in-production
      SOCIAL_AUTH_AUTH0_KEY: ${AUTH0_KEY}
    

  2. Local .env file (gitignored):

    DJANGO_SECRET_KEY=dev-key-12345
    AUTH0_KEY=real-auth0-key-for-testing
    

  3. IDE/Shell environment: Manually exported variables

Theory: This layered approach allows: - Shared defaults in docker-compose.yml - Personal overrides in .env (never committed) - Temporary overrides via shell exports

Production Secret Management

Production uses AWS Systems Manager Parameter Store:

# Instead of:
SECRET_KEY = "hardcoded-secret"  # NEVER DO THIS

# Use:
SECRET_KEY = pconfig.get_param("DJANGO_SECRET_KEY")

Parameters are stored in AWS SSM as:

  • SecureString type: Encrypted at rest with KMS
  • IAM-controlled access: Only authorized services can read
  • Audit logging: CloudTrail logs all parameter access
  • Versioning: Parameter changes are versioned

Best Practices:

  1. Use descriptive parameter names (SOCIAL_AUTH_AUTH0_SECRET, not SECRET_1)
  2. Store all secrets in SSM, even if "not that sensitive"
  3. Use separate parameters for separate environments (dev vs production)
  4. Rotate secrets regularly
  5. Never log secret values
  6. Use # nosec comments to suppress Bandit warnings on URLs (not actual secrets)

Example of safe URL annotation:

AASLD_INTERNAL_LOOKUP_ENDPOINT = "https://aasld.my.salesforce.com/services/..."  # nosec

The # nosec comment tells security scanners this is a public URL (not a secret), preventing false positives.

Middleware and Apps Organization

Middleware Ordering Principles

Middleware processes requests top-to-bottom and responses bottom-to-top. This creates a layered architecture where early middleware wraps later middleware.

MIDDLEWARE = [
    "django.middleware.security.SecurityMiddleware",        # 1. Security headers
    "django.contrib.sessions.middleware.SessionMiddleware", # 2. Load session
    "corsheaders.middleware.CorsMiddleware",                # 3. CORS (before Common)
    "django.middleware.common.CommonMiddleware",            # 4. URL normalization
    "django.middleware.csrf.CsrfViewMiddleware",            # 5. CSRF protection
    "poseidon.middleware.sso_tenant_middleware.SSOTenantMiddleware",  # 6. Custom tenant
    "django.contrib.auth.middleware.AuthenticationMiddleware",  # 7. User authentication
    "hijack.middleware.HijackUserMiddleware",               # 8. User impersonation
    "django.contrib.messages.middleware.MessageMiddleware", # 9. Flash messages
    "django.middleware.clickjacking.XFrameOptionsMiddleware",  # 10. Clickjacking
    "waffle.middleware.WaffleMiddleware",                   # 11. Feature flags
]

Theory: Each middleware's position is deliberate:

  1. SecurityMiddleware: First, so security headers apply to all responses
  2. SessionMiddleware: Early, so session is available to subsequent middleware
  3. CorsMiddleware: Before CommonMiddleware to handle preflight requests
  4. CsrfViewMiddleware: After session (needs session) but before views
  5. Custom middleware: After auth if it needs request.user
  6. AuthenticationMiddleware: After session (needs session data)
  7. MessageMiddleware: After session (stores messages in session)
  8. WaffleMiddleware: Late, as feature flags are checked in views

Installed Apps Ordering

INSTALLED_APPS = [
    # Core Django apps (override-able by later apps)
    "django.contrib.admin",
    "django.contrib.auth",
    "django.contrib.contenttypes",
    "django.contrib.sessions",
    "django.contrib.messages",

    # Third-party apps that modify Django behavior
    "social_django",  # Adds authentication backends
    "django_ace",     # Modifies admin widgets

    # Project apps (highest template/static precedence)
    "poseidon.apps.PoseidonConfig",

    # Django static files (last, lowest precedence)
    "django.contrib.staticfiles",

    # Third-party apps (alphabetical if order doesn't matter)
    "rest_framework",
    "storages",
    "corsheaders",
    "django_filters",
    "waffle",
    "hijack",
]

Theory: App order determines:

  1. Template precedence: Earlier apps' templates override later apps
  2. Static file precedence: Earlier apps' static files override later apps
  3. Database migration dependencies: Apps are migrated in order

Best practice: Project apps come after Django core but before utilities, ensuring project templates override third-party templates but use Django's admin/auth as a base.

Security Settings

Core Security Configuration

DEBUG = False  # Production only

ALLOWED_HOSTS = ["*"]  # Handled by load balancer
CSRF_TRUSTED_ORIGINS = ["https://*.planion.com", "http://*.planion.com"]

SECRET_KEY = pconfig.get_param("DJANGO_SECRET_KEY")  # Never hardcode

Theory:

  • DEBUG: Must be False in production. DEBUG=True exposes stack traces, SQL queries, and settings to users
  • ALLOWED_HOSTS: ["*"] is safe when behind a load balancer that validates hosts. Otherwise, use explicit domain list
  • SECRET_KEY: Used for cryptographic signing (sessions, CSRF tokens, password reset). Must be random, secret, and stable

SSL/HTTPS Configuration

# Production
SECURE_SSL_REDIRECT = False  # Load balancer handles SSL redirect
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True

# Development
SECURE_SSL_REDIRECT = False
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")  # For local HTTPS
SESSION_COOKIE_SECURE = False  # Allow HTTP
CSRF_COOKIE_SECURE = False

Theory:

  • SECURE_SSL_REDIRECT: Django redirects HTTP to HTTPS. Disabled when load balancer handles this (prevents double redirect)
  • SECURE_PROXY_SSL_HEADER: Tells Django to trust X-Forwarded-Proto header from load balancer
  • Cookie SECURE flags: Cookies only sent over HTTPS when True. Relaxed in development for HTTP testing

Cross-Site Protection

CSRF_COOKIE_SAMESITE = "None"
SESSION_COOKIE_SAMESITE = "None"
X_FRAME_OPTIONS = "DENY"
SECURE_BROWSER_XSS_FILTER = True
SECURE_CONTENT_TYPE_NOSNIFF = True
SECURE_CROSS_ORIGIN_OPENER_POLICY = None

Theory:

  • SameSite=None: Required for cross-origin authentication (SSO, embedded apps). Must be paired with Secure=True
  • X-Frame-Options=DENY: Prevents page from being embedded in iframes (clickjacking protection)
  • XSS filter: Enables browser's XSS filter
  • Content-Type Nosniff: Prevents MIME type sniffing attacks
  • COOP=None: Allows cross-origin window interactions (needed for OAuth popups)

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"},
]

Theory: Layered validation prevents common weak passwords:

  1. Similarity: Password can't be too similar to username/email
  2. Length: Minimum length requirement (default 8)
  3. Common: Rejects commonly-used passwords (from wordlist)
  4. Numeric: Prevents all-numeric passwords

Security Checks

# Development: Silence expected warnings
SILENCED_SYSTEM_CHECKS += [
    "security.W004",  # SECURE_HSTS_SECONDS not set
    "security.W008",  # SECURE_SSL_REDIRECT not True
    "security.W012",  # SESSION_COOKIE_SECURE not True
    "security.W018",  # CSRF_COOKIE_SECURE not True
]

Theory: Django's system checks identify security issues. In development, some checks are expected to fail (no HTTPS, relaxed cookies). Silencing prevents noise while preserving critical warnings.

Production Security

Never silence security checks in production. If a check fails, fix the underlying issue.

Third-Party Package Configuration

Django REST Framework

REST_FRAMEWORK = {
    "DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination",
    "PAGE_SIZE": 100,
    "DEFAULT_AUTHENTICATION_CLASSES": ("poseidon.models.BearerAuthentication",),
    "DEFAULT_PERMISSION_CLASSES": ("rest_framework.permissions.IsAuthenticated",),
    "DEFAULT_FILTER_BACKENDS": ("django_filters.rest_framework.DjangoFilterBackend",),
    "DEFAULT_THROTTLE_CLASSES": [
        "rest_framework.throttling.AnonRateThrottle",
        "rest_framework.throttling.UserRateThrottle",
    ],
    "DEFAULT_THROTTLE_RATES": {"anon": "100/day", "user": "2500/minute"},
}

Configuration Decisions:

  • Pagination: PageNumberPagination with 100 items per page balances payload size and request count
  • Authentication: Custom BearerAuthentication for token-based API access
  • Permissions: Require authentication by default (secure by default)
  • Throttling: 100/day for anonymous (prevents abuse), 2500/minute for authenticated (high ceiling for legitimate use)

CORS Headers

CORS_ALLOWED_ORIGIN_REGEXES = [
    r"^https://[-\w]+\.planion\.com$",  # Production subdomains
    r"^http://localhost:\d+$",           # Development
]

CORS_ALLOW_HEADERS = (
    *default_headers,
    "ps-user-session",  # Custom application header
)

Configuration Decisions:

  • Regex origins: Flexible subdomain matching without hardcoding each subdomain
  • Custom headers: Explicitly allow application-specific headers
  • Default headers: Spread operator includes standard headers (Authorization, Content-Type, etc.)

Social Auth (OAuth)

AUTHENTICATION_BACKENDS = [
    "poseidon.auth0backend.Auth0",  # OAuth via Auth0
    "django.contrib.auth.backends.ModelBackend",  # Fallback to Django auth
]

SOCIAL_AUTH_AUTH0_DOMAIN = "planion-demoauth.auth0.com"
SOCIAL_AUTH_AUTH0_KEY = pconfig.get_param("SOCIAL_AUTH_AUTH0_KEY")
SOCIAL_AUTH_AUTH0_SECRET = pconfig.get_param("SOCIAL_AUTH_AUTH0_SECRET")
SOCIAL_AUTH_TRAILING_SLASH = False
SOCIAL_AUTH_AUTH0_SCOPE = ["openid", "profile"]

LOGIN_URL = "/login/auth0"
LOGIN_REDIRECT_URL = "/planion/myaccounts"
LOGOUT_REDIRECT_URL = "/"

Configuration Decisions:

  • Multiple backends: Auth0 for SSO, Django ModelBackend as fallback
  • Scopes: Request minimum required scopes (openid, profile)
  • Trailing slash: Disabled to match Auth0 URL expectations
  • Redirects: Clear separation of login, post-login, and logout destinations

Waffle (Feature Flags)

INSTALLED_APPS = [
    # ...
    "waffle",
]

MIDDLEWARE = [
    # ...
    "waffle.middleware.WaffleMiddleware",  # Late in middleware stack
]

Configuration Decisions:

  • Middleware position: After authentication (feature flags may check user attributes)
  • No additional configuration: Waffle uses database for flag storage (no settings needed)

Django Storages (S3)

# Production only
AWS_STORAGE_BUCKET_NAME = "planstone-poseidon-static-files"
AWS_DEFAULT_ACL = "public-read"
AWS_LOCATION = "static-v2"
AWS_S3_OBJECT_PARAMETERS = {"CacheControl": "max-age=86400"}

STORAGES = {
    "default": {
        "BACKEND": "storages.backends.s3.S3Storage",
        "OPTIONS": {
            "bucket_name": AWS_STORAGE_BUCKET_NAME,
            "location": "",  # Media files at bucket root
        },
    },
    "staticfiles": {
        "BACKEND": "storages.backends.s3.S3Storage",
        "OPTIONS": {
            "bucket_name": AWS_STORAGE_BUCKET_NAME,
            "location": AWS_LOCATION,  # Static files in static-v2/
        },
    },
}

Configuration Decisions:

  • Separate storages: Different backends for media (user uploads) vs static (CSS/JS)
  • Cache headers: 24-hour cache for static files (balance freshness and CDN efficiency)
  • Public ACL: Static files are publicly accessible
  • IAM authentication: No credentials in settings (uses EC2/ECS IAM role)

When to Add New Settings Files

Create a New Settings File When:

  1. New deployment environment
  2. Example: staging.py for pre-production testing
  3. Pattern: Import from base.py, override environment-specific values

  4. Specialized testing scenario

  5. Example: integration_testing.py for integration tests with external services
  6. Pattern: Import from development.py, enable service mocks

  7. Temporary debugging environment

  8. Example: production-debug.py for production troubleshooting
  9. Pattern: Import from production.py, add debug tools

  10. Isolated subsystem configuration

  11. Example: celery.py for Celery worker configuration
  12. Pattern: Import base settings, add Celery-specific configuration

Do NOT Create New Settings Files For:

  1. Feature flags → Use Waffle or Django settings variables
  2. Tenant-specific configuration → Use database-backed configuration
  3. User preferences → Use user profile models
  4. Runtime configuration → Use environment variables
  5. Temporary experiments → Use environment variables or feature flags

Settings File Naming Conventions

  • Environment-specific: {environment}.py (e.g., production.py, staging.py)
  • Hybrid environments: {base}-{modifier}.py (e.g., production-local.py)
  • Subsystem configuration: {subsystem}_config.py (e.g., logging_config.py, celery_config.py)
  • Database configuration: {environment}_databases.py (e.g., production_databases.py)

Settings File Template

When creating a new environment settings file:

"""
Settings for {ENVIRONMENT_NAME} environment.

Description: {What this environment is used for}
Usage: DJANGO_SETTINGS_MODULE=poseidon.settings.{filename}
"""

from .base import *
from .base import INSTALLED_APPS, MIDDLEWARE  # Explicit imports for modification
from .{environment}_databases import DATABASES

DEBUG = {True|False}

# Environment-specific overrides
SETTING_NAME = "value"

# Conditional configuration
if SOME_CONDITION:
    INSTALLED_APPS += ["extra_app"]

# Import secrets
SECRET_KEY = os.getenv("DJANGO_SECRET_KEY")  # Development
# OR
SECRET_KEY = pconfig.get_param("DJANGO_SECRET_KEY")  # Production

Environment Variable Pattern

Set the active settings module via DJANGO_SETTINGS_MODULE:

# Development
export DJANGO_SETTINGS_MODULE=poseidon.settings.development

# Production
export DJANGO_SETTINGS_MODULE=poseidon.settings.production

# Testing
export DJANGO_SETTINGS_MODULE=poseidon.settings.testing

In Docker Compose:

environment:
  DJANGO_SETTINGS_MODULE: poseidon.settings.development

In AWS ECS task definitions:

{
  "environment": [
    {
      "name": "DJANGO_SETTINGS_MODULE",
      "value": "poseidon.settings.production"
    }
  ]
}

Summary of Best Practices

  1. Base + Environment Pattern: Establish shared configuration in base.py, override in environment-specific files
  2. Explicit Imports: Use from .base import INSTALLED_APPS to modify lists in-place
  3. Database Isolation: Separate database configuration from application configuration
  4. Secret Management: Never hardcode secrets; use environment variables (dev) or SSM (production)
  5. Security Defaults: Secure by default in base settings, explicitly relax in development
  6. Import Hierarchy: Understand import order when using multiple inheritance (baseproductionproduction-local)
  7. Environment Detection: Use environment variables or AWS metadata to detect runtime environment
  8. Fail Fast: Validate configuration at startup; don't defer errors to runtime
  9. Document Decisions: Comment non-obvious configuration choices
  10. Version Control: Never commit secrets; use .env files (gitignored) for local overrides

References