# Django Integration

Use KeyEnv with Django applications for secure secrets management.

Source: https://keyenv.dev/docs/sdks/django/

Integrate KeyEnv with your Django application to securely manage secrets like database credentials, API keys, and the Django secret key.

## Features

- Load secrets before Django initializes
- Environment-aware configuration (development, staging, production)
- Works with django-environ and python-decouple patterns
- Supports DATABASE_URL parsing
- Type-safe with full Python type annotations

## Installation

```bash
pip install keyenv
```

Or with poetry:

```bash
poetry add keyenv
```

For DATABASE_URL parsing, also install dj-database-url:

```bash
pip install dj-database-url
```

## Quick Start

Add KeyEnv to the top of your `settings.py`:

```python
# settings.py
from keyenv import KeyEnv
import os

# Load secrets before anything else
if os.environ.get("KEYENV_TOKEN"):
    client = KeyEnv(token=os.environ["KEYENV_TOKEN"])
    client.load_env(
        os.environ["KEYENV_PROJECT"],
        os.environ.get("DJANGO_ENV", "development")
    )

# Now use secrets in your settings
SECRET_KEY = os.environ["DJANGO_SECRET_KEY"]
DEBUG = os.environ.get("DEBUG", "false").lower() == "true"
```

## Django Settings Integration

### Basic Setup

The simplest approach loads all secrets into `os.environ` at startup:

```python
# settings.py
from keyenv import KeyEnv
import os

# Load secrets from KeyEnv
if os.environ.get("KEYENV_TOKEN"):
    client = KeyEnv(token=os.environ["KEYENV_TOKEN"])
    env = os.environ.get("DJANGO_ENV", "development")
    client.load_env(os.environ["KEYENV_PROJECT"], env)

# Django settings
SECRET_KEY = os.environ["DJANGO_SECRET_KEY"]
DEBUG = os.environ.get("DEBUG", "false").lower() == "true"
ALLOWED_HOSTS = os.environ.get("ALLOWED_HOSTS", "localhost").split(",")

# Database
DATABASES = {
    "default": {
        "ENGINE": "django.db.backends.postgresql",
        "HOST": os.environ.get("DB_HOST", "localhost"),
        "PORT": os.environ.get("DB_PORT", "5432"),
        "NAME": os.environ.get("DB_NAME", "myapp"),
        "USER": os.environ.get("DB_USER", "postgres"),
        "PASSWORD": os.environ.get("DB_PASSWORD", ""),
    }
}

# Email
EMAIL_HOST = os.environ.get("EMAIL_HOST", "smtp.gmail.com")
EMAIL_HOST_USER = os.environ.get("EMAIL_HOST_USER", "")
EMAIL_HOST_PASSWORD = os.environ.get("EMAIL_HOST_PASSWORD", "")
```

### With DATABASE_URL

Use `dj-database-url` for cleaner database configuration:

```python
# settings.py
from keyenv import KeyEnv
import dj_database_url
import os

# Load secrets
if os.environ.get("KEYENV_TOKEN"):
    client = KeyEnv(token=os.environ["KEYENV_TOKEN"])
    client.load_env(os.environ["KEYENV_PROJECT"], os.environ.get("DJANGO_ENV", "development"))

SECRET_KEY = os.environ["DJANGO_SECRET_KEY"]

# Parse DATABASE_URL automatically
DATABASES = {
    "default": dj_database_url.config(
        default="postgres://localhost/myapp",
        conn_max_age=600,
        conn_health_checks=True,
    )
}
```

Store your database URL in KeyEnv as:

```
DATABASE_URL=postgres://user:password@host:5432/dbname
```

## Environment-Aware Configuration

### Automatic Environment Detection

Detect the environment from various sources:

```python
# settings.py
from keyenv import KeyEnv
import os

def get_environment():
    """Determine environment from various sources."""
    # Check explicit setting first
    if env := os.environ.get("DJANGO_ENV"):
        return env

    # Check common deployment indicators
    if os.environ.get("RAILWAY_ENVIRONMENT"):
        return os.environ["RAILWAY_ENVIRONMENT"]
    if os.environ.get("RENDER"):
        return "production"
    if os.environ.get("HEROKU_APP_NAME"):
        return "production"

    # Default to development
    return "development"

ENVIRONMENT = get_environment()

# Load environment-specific secrets
if os.environ.get("KEYENV_TOKEN"):
    client = KeyEnv(token=os.environ["KEYENV_TOKEN"])
    client.load_env(os.environ["KEYENV_PROJECT"], ENVIRONMENT)

# Environment-specific settings
DEBUG = ENVIRONMENT == "development"
```

### Split Settings Pattern

For larger projects, use environment-specific settings files:

```python
# settings/base.py
from keyenv import KeyEnv
import os

ENVIRONMENT = os.environ.get("DJANGO_ENV", "development")

if os.environ.get("KEYENV_TOKEN"):
    client = KeyEnv(token=os.environ["KEYENV_TOKEN"])
    client.load_env(os.environ["KEYENV_PROJECT"], ENVIRONMENT)

SECRET_KEY = os.environ["DJANGO_SECRET_KEY"]

# Common settings...
INSTALLED_APPS = [...]
MIDDLEWARE = [...]
```

```python
# settings/development.py
from .base import *

DEBUG = True
ALLOWED_HOSTS = ["localhost", "127.0.0.1"]
```

```python
# settings/production.py
from .base import *

DEBUG = False
ALLOWED_HOSTS = os.environ.get("ALLOWED_HOSTS", "").split(",")

# Production-specific settings
SECURE_SSL_REDIRECT = True
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
```

## Using with django-environ

If you prefer django-environ's API:

```python
# settings.py
from keyenv import KeyEnv
import environ
import os

# Load KeyEnv secrets first
if os.environ.get("KEYENV_TOKEN"):
    client = KeyEnv(token=os.environ["KEYENV_TOKEN"])
    client.load_env(os.environ["KEYENV_PROJECT"], os.environ.get("DJANGO_ENV", "development"))

# Initialize django-environ (reads from os.environ)
env = environ.Env(
    DEBUG=(bool, False),
    ALLOWED_HOSTS=(list, ["localhost"]),
)

# Use django-environ's typed accessors
SECRET_KEY = env("DJANGO_SECRET_KEY")
DEBUG = env("DEBUG")
ALLOWED_HOSTS = env("ALLOWED_HOSTS")

DATABASES = {
    "default": env.db("DATABASE_URL", default="postgres://localhost/myapp")
}

EMAIL_CONFIG = env.email_url("EMAIL_URL", default="smtp://localhost:25")
```

## Using with python-decouple

If you prefer python-decouple's pattern:

```python
# settings.py
from keyenv import KeyEnv
from decouple import config, Csv
import os

# Load KeyEnv secrets first
if os.environ.get("KEYENV_TOKEN"):
    client = KeyEnv(token=os.environ["KEYENV_TOKEN"])
    client.load_env(os.environ["KEYENV_PROJECT"], os.environ.get("DJANGO_ENV", "development"))

# python-decouple reads from os.environ (and .env as fallback)
SECRET_KEY = config("DJANGO_SECRET_KEY")
DEBUG = config("DEBUG", default=False, cast=bool)
ALLOWED_HOSTS = config("ALLOWED_HOSTS", default="localhost", cast=Csv())

DATABASES = {
    "default": {
        "ENGINE": "django.db.backends.postgresql",
        "HOST": config("DB_HOST", default="localhost"),
        "PORT": config("DB_PORT", default=5432, cast=int),
        "NAME": config("DB_NAME", default="myapp"),
        "USER": config("DB_USER", default="postgres"),
        "PASSWORD": config("DB_PASSWORD", default=""),
    }
}
```

## Complete Example

Here's a complete `settings.py` for a production Django application:

```python
# settings.py
from keyenv import KeyEnv
from pathlib import Path
import dj_database_url
import os

# Build paths
BASE_DIR = Path(__file__).resolve().parent.parent

# Determine environment
ENVIRONMENT = os.environ.get("DJANGO_ENV", "development")

# Load secrets from KeyEnv
if token := os.environ.get("KEYENV_TOKEN"):
    client = KeyEnv(token=token)
    project_id = os.environ.get("KEYENV_PROJECT")
    if project_id:
        client.load_env(project_id, ENVIRONMENT)

# Core settings
SECRET_KEY = os.environ["DJANGO_SECRET_KEY"]
DEBUG = os.environ.get("DEBUG", "false").lower() == "true"
ALLOWED_HOSTS = os.environ.get("ALLOWED_HOSTS", "localhost").split(",")

# Application definition
INSTALLED_APPS = [
    "django.contrib.admin",
    "django.contrib.auth",
    "django.contrib.contenttypes",
    "django.contrib.sessions",
    "django.contrib.messages",
    "django.contrib.staticfiles",
    # Your apps
    "myapp",
]

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

ROOT_URLCONF = "myproject.urls"
WSGI_APPLICATION = "myproject.wsgi.application"

# Database
DATABASES = {
    "default": dj_database_url.config(
        default="postgres://localhost/myapp",
        conn_max_age=600,
        conn_health_checks=True,
    )
}

# Cache (optional)
if redis_url := os.environ.get("REDIS_URL"):
    CACHES = {
        "default": {
            "BACKEND": "django.core.cache.backends.redis.RedisCache",
            "LOCATION": redis_url,
        }
    }

# Email
EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend"
EMAIL_HOST = os.environ.get("EMAIL_HOST", "smtp.gmail.com")
EMAIL_PORT = int(os.environ.get("EMAIL_PORT", "587"))
EMAIL_USE_TLS = os.environ.get("EMAIL_USE_TLS", "true").lower() == "true"
EMAIL_HOST_USER = os.environ.get("EMAIL_HOST_USER", "")
EMAIL_HOST_PASSWORD = os.environ.get("EMAIL_HOST_PASSWORD", "")
DEFAULT_FROM_EMAIL = os.environ.get("DEFAULT_FROM_EMAIL", "noreply@example.com")

# Security (production)
if ENVIRONMENT == "production":
    SECURE_SSL_REDIRECT = True
    SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
    SESSION_COOKIE_SECURE = True
    CSRF_COOKIE_SECURE = True
    SECURE_HSTS_SECONDS = 31536000
    SECURE_HSTS_INCLUDE_SUBDOMAINS = True
    SECURE_HSTS_PRELOAD = True

# Static files
STATIC_URL = "/static/"
STATIC_ROOT = BASE_DIR / "staticfiles"
STATICFILES_STORAGE = "whitenoise.storage.CompressedManifestStaticFilesStorage"

# Third-party services
STRIPE_SECRET_KEY = os.environ.get("STRIPE_SECRET_KEY", "")
STRIPE_WEBHOOK_SECRET = os.environ.get("STRIPE_WEBHOOK_SECRET", "")
SENTRY_DSN = os.environ.get("SENTRY_DSN", "")

# Initialize Sentry
if SENTRY_DSN:
    import sentry_sdk
    sentry_sdk.init(
        dsn=SENTRY_DSN,
        environment=ENVIRONMENT,
        traces_sample_rate=0.1,
    )
```

## Setting Up Your Deployment

### Required Environment Variables

Set these in your deployment platform (not in KeyEnv):

| Variable | Description |
|----------|-------------|
| `KEYENV_TOKEN` | Your KeyEnv service token |
| `KEYENV_PROJECT` | Your KeyEnv project ID |
| `DJANGO_ENV` | Environment name (development, staging, production) |

### Secrets to Store in KeyEnv

Store these secrets in KeyEnv for each environment:

| Secret | Description | Example |
|--------|-------------|---------|
| `DJANGO_SECRET_KEY` | Django secret key | `django-insecure-abc123...` |
| `DATABASE_URL` | Database connection string | `postgres://user:pass@host/db` |
| `REDIS_URL` | Redis connection (optional) | `redis://localhost:6379/0` |
| `EMAIL_HOST_PASSWORD` | SMTP password | `app-specific-password` |
| `STRIPE_SECRET_KEY` | Stripe API key | `sk_live_...` |
| `SENTRY_DSN` | Sentry DSN | `https://...@sentry.io/...` |
| `ALLOWED_HOSTS` | Comma-separated hosts | `example.com,www.example.com` |

### Generate a Django Secret Key

```bash
python -c "from django.core.management.utils import get_random_secret_key; print(get_random_secret_key())"
```

## Local Development

For local development without KeyEnv:

```python
# settings.py - handles missing KEYENV_TOKEN gracefully
if os.environ.get("KEYENV_TOKEN"):
    client = KeyEnv(token=os.environ["KEYENV_TOKEN"])
    client.load_env(os.environ["KEYENV_PROJECT"], "development")

# Falls back to local .env file or defaults
SECRET_KEY = os.environ.get("DJANGO_SECRET_KEY", "dev-secret-key-not-for-production")
```

Or use the KeyEnv CLI to generate a `.env` file:

```bash
keyenv pull --env development --output .env
```

Then load it with python-dotenv in development:

```python
# settings.py
import os

if os.environ.get("KEYENV_TOKEN"):
    from keyenv import KeyEnv
    client = KeyEnv(token=os.environ["KEYENV_TOKEN"])
    client.load_env(os.environ["KEYENV_PROJECT"], os.environ.get("DJANGO_ENV", "development"))
elif os.path.exists(".env"):
    from dotenv import load_dotenv
    load_dotenv()
```

## wsgi.py and asgi.py

No changes needed to `wsgi.py` or `asgi.py`. Secrets are loaded when `settings.py` is imported.

## Testing

For tests, you can skip KeyEnv by not setting `KEYENV_TOKEN`:

```python
# conftest.py (pytest)
import os
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "myproject.settings")

# Test-specific secrets
os.environ["DJANGO_SECRET_KEY"] = "test-secret-key"
os.environ["DATABASE_URL"] = "postgres://localhost/test_db"
```

Or use pytest-env:

```ini
# pytest.ini
[pytest]
env =
    DJANGO_SECRET_KEY=test-secret-key
    DATABASE_URL=postgres://localhost/test_db
```

## Troubleshooting

### "KEYENV_TOKEN not set"

In production, ensure `KEYENV_TOKEN` is set in your deployment platform's environment variables (Heroku Config Vars, Railway Variables, etc.).

### "Project not found"

Verify `KEYENV_PROJECT` matches your project ID in the KeyEnv dashboard.

### "Environment not found"

Check that the environment name (development, staging, production) exists in your KeyEnv project.

### Secrets not loading

1. Check that `load_env()` is called before accessing `os.environ`
2. Verify the service token has read access to the environment
3. Check for typos in secret key names

### Performance

`load_env()` makes a single API call at startup. Secrets are cached in `os.environ` for the lifetime of the process. There's no runtime overhead after initialization.
