Django Apps skill for Codex

Django apps with real constraints and safe settings. Codex builds Django projects that keep data rules in database constraints, ship migrations only after reading the SQL, and scope every queryset to the requesting user. The admin is configured to stay responsive on large tables, side effects run through transaction.on_commit, and a production settings reference covers HTTPS behind proxies, STORAGES, connection pooling and the check --deploy gate.

  • Backend & APIs
  • $django-apps
  • 212 lines
  • 3 files
  • ~2,200 tokens
  • MIT
  • Updated

Install

curl -fsSL codexskills.dev/install.sh \  | sh -s -- django-apps

Installs to ~/.agents/skills/django-apps/ for every project on this machine.

104-line POSIX script. No sudo, no telemetry. Read install.sh

Use it in Codex

Start a prompt with $django-apps, or pick it from /skills. You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.

What Codex reads before loading it

Builds and maintains Django 5.2 LTS and 6.x projects - app boundaries, a custom user model from day one, models with database constraints, safe migrations, views and forms, admin configuration that stays fast on large tables, environment-driven settings, and check --deploy hardening. Use when starting a Django project, adding an app, model or admin, writing or reviewing migrations, splitting settings for production, or upgrading Django versions.

Use it when

  • Starting a Django project or adding an app, model, view or admin
  • Writing or reviewing Django migrations
  • Preparing settings for production or fixing check --deploy warnings
  • Upgrading Django to 5.2 LTS or 6.x

Use something else when

How to check the result

manage.py check, makemigrations --check --dry-run and the test suite pass, and check --deploy reports no unexplained warnings with production environment variables.

The mistake it prevents

Starting a project on the default auth User model; switching AUTH_USER_MODEL after the first production migration is a painful multi-step data migration.

~/.agents/skills/django-apps/SKILL.md

--- name: django-apps description: Builds and maintains Django 5.2 LTS and 6.x projects - app boundaries, a custom user model from day one, models with database constraints, safe… license: MIT ---

Done well means: data rules live in the database (constraints) and in model methods, not scattered through views; migrations are reviewed SQL, not autogenerated surprises; settings come from the environment and pass check --deploy; the admin stays usable at a million rows; and every change ships with tests.

Inspect first

  • Read AGENTS.md, then pyproject.toml/requirements*.txt for the Django version and tooling (pytest-django or manage.py test, ruff, mypy with django-stubs).
  • Find settings: rg -n "DJANGO_SETTINGS_MODULE" . and read the settings module(s).
  • Check AUTH_USER_MODEL. If it is unset on a project with production data, changing it later is a multi-step data migration: flag it, do not attempt it casually.
  • python manage.py showmigrations | rg "\[ \]" to see unapplied migrations before adding new ones.

Project layout

config/
  settings.py        # one module, values from env (or base/dev/prod if the repo already splits)
  urls.py
  asgi.py / wsgi.py
apps/
  accounts/          # custom User lives here
  orders/
    models.py
    services.py      # multi-model writes, transactions, side effects
    selectors.py     # read queries reused by views/APIs (optional convention)
    views.py
    forms.py
    admin.py
    urls.py
    tests/
manage.py

Split an app when it has its own models and could be described in one sentence; do not create one app per model. Cross-app imports should flow one way; circular imports between models.py files signal a wrong boundary (use string references "orders.Order" in relations when needed).

New project checklist

- [ ] Custom user model before the first migrate: class User(AbstractUser) + AUTH_USER_MODEL
- [ ] Settings read from environment; SECRET_KEY, DEBUG, ALLOWED_HOSTS, DATABASES from env
- [ ] DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" (the default from Django 6.0)
- [ ] PostgreSQL in dev and CI, not SQLite, if production is PostgreSQL
- [ ] pytest-django or the test runner wired into CI
- [ ] Static files: STORAGES + WhiteNoise or object storage
- [ ] python manage.py check --deploy passes with production settings

Models

from django.db import models
from django.db.models import Q, F

class Order(models.Model):
    class Status(models.TextChoices):
        PENDING = "pending"
        PAID = "paid"
        CANCELLED = "cancelled"

    customer = models.ForeignKey("accounts.User", on_delete=models.PROTECT, related_name="orders")
    status = models.CharField(max_length=16, choices=Status, default=Status.PENDING)
    total_cents = models.PositiveIntegerField()
    discount_cents = models.PositiveIntegerField(default=0)
    external_ref = models.CharField(max_length=64, blank=True)
    created_at = models.DateTimeField(auto_now_add=True, db_index=True)

    class Meta:
        constraints = [
            models.CheckConstraint(condition=Q(discount_cents__lte=F("total_cents")), name="order_discount_lte_total"),
            models.UniqueConstraint(fields=["external_ref"], condition=~Q(external_ref=""), name="order_external_ref_uniq"),
        ]
        indexes = [models.Index(fields=["customer", "-created_at"])]

    def cancel(self) -> None:
        if self.status == self.Status.PAID:
            raise ValueError("paid orders must be refunded, not cancelled")
        self.status = self.Status.CANCELLED
        self.save(update_fields=["status"])
  • Money in integer cents or DecimalField, never FloatField.
  • on_delete: PROTECT for business records, CASCADE for owned children, SET_NULL only with null=True and a reason.
  • Use CheckConstraint(condition=...); the old check= keyword is deprecated since 5.1.
  • save(update_fields=[...]) avoids overwriting concurrent changes to other columns.
  • Use UniqueConstraint, not unique_together (legacy).

Migrations

python manage.py makemigrations orders
python manage.py sqlmigrate orders 0007       # read the SQL before applying
python manage.py migrate
python manage.py makemigrations --check --dry-run   # in CI: fails if models and migrations drift
  • Review every generated migration: renames detected as drop + add lose data.
  • Large tables: adding an index on PostgreSQL should use AddIndexConcurrently in a migration with atomic = False. For column changes under load, follow $zero-downtime-migrations.
  • Data migrations use apps.get_model(), never direct model imports, and provide a reverse_code (or migrations.RunPython.noop with a comment why).
  • Never edit a migration that has run in any shared environment; add a new one.
  • Squash only when the team agrees; squashing touches every environment’s history.

Views and forms

  • Function views for one-off logic, generic class-based views for standard CRUD; follow whatever the codebase already does.
  • Validate input with forms (ModelForm with an explicit fields list, never exclude or "__all__", which expose new fields automatically).
  • Put multi-model writes in a service function wrapped in transaction.atomic(), and trigger emails, webhooks or task enqueues with transaction.on_commit(...) so they never fire for a rolled-back transaction.
  • Scope querysets to the user: Order.objects.filter(customer=request.user) then get_object_or_404(qs, pk=pk). Fetching by pk alone is an IDOR.
  • For JSON APIs, use $django-rest-framework.

Admin that scales

@admin.register(Order)
class OrderAdmin(admin.ModelAdmin):
    list_display = ["id", "customer", "status", "total_cents", "created_at"]
    list_filter = ["status", ("created_at", admin.DateFieldListFilter)]
    search_fields = ["=id", "external_ref", "customer__email"]
    list_select_related = ["customer"]
    autocomplete_fields = ["customer"]          # requires search_fields on UserAdmin
    readonly_fields = ["created_at"]
    date_hierarchy = "created_at"
    show_full_result_count = False              # avoids COUNT(*) per page on big tables
    list_per_page = 50

Foreign keys in forms render every row as a <select> option; use autocomplete_fields or raw_id_fields for any relation with more than a few hundred rows. Admin actions that modify many rows should ask for confirmation through an intermediate page.

Settings

Read the production checklist in references/production-settings.md when touching settings, deploying, or when check --deploy reports warnings.

Background work

Django 6.0 ships a tasks API (django.tasks, @task and .enqueue()), but its built-in backends only run tasks immediately or not at all; production needs a worker backend from a third-party package. Celery, Dramatiq, RQ or django-q2 remain the common choices; see $background-jobs. Enqueue inside transaction.on_commit.

Testing

  • pytest-django with @pytest.mark.django_db, or django.test.TestCase (wraps each test in a transaction). Use factories (factory_boy) over fixtures files.
  • Test model constraints by asserting IntegrityError inside transaction.atomic().
  • Assert query counts on hot views: django_assert_num_queries (pytest-django) or self.assertNumQueries(n). For N+1 fixes use $django-orm-performance.
  • TestCase.captureOnCommitCallbacks(execute=True) to test on_commit side effects.

Upgrading Django

  1. Run tests with python -Wa manage.py test (or -W error::DeprecationWarning in pytest) on the current version and fix deprecations first.
  2. Upgrade one feature release at a time (5.2 to 6.0, not 4.2 to 6.0 in one jump).
  3. Read the release notes’ “Backwards incompatible changes” section for each step; Django 6.0 requires Python 3.12+.
  4. Check third-party apps support the target version before bumping.

Verification

python manage.py check
python manage.py makemigrations --check --dry-run
python manage.py test        # or pytest
DJANGO_SETTINGS_MODULE=config.settings python manage.py check --deploy   # with production env vars

All four must pass. If makemigrations --check fails, a model change has no migration.

Gotchas

  • auto_now fields are not set by QuerySet.update(); set them explicitly there.
  • bulk_create and update() skip save(), signals and model validation.
  • full_clean() is not called by save(); constraints in Meta are your real guarantee.
  • Signals hide control flow; prefer explicit service calls for business logic.
  • Timezone: keep USE_TZ = True; compare with timezone.now(), never datetime.now().

Output template

## Django change: <summary>
- Apps/models: <added/changed fields, constraints>
- Migrations: <app.number, reviewed SQL, concurrent/online?>
- Views/forms/admin: <changes>
- Settings: <changes, env vars added>
- Tests: <cases>; checks: check, makemigrations --check, check --deploy

What the zip contains

Codex reads bundled files only when a step points to them. Every one is shown here, so you can review it before installing.

agents/openai.yamlgenerated from this page

Gives the Codex app a display name, a one-line description and a starter prompt for this skill.

interface:
  display_name: "Django Apps"
  short_description: "Django apps with real constraints and safe settings"
  default_prompt: "$django-apps Start a new Django project with a custom user model and production-ready settings"
references/production-settings.md137 lines, 5.1 KB
# Django production settings checklist

Read when editing settings, preparing a deploy, or fixing `check --deploy` warnings.
Values below assume Django 5.2 LTS or 6.x behind a TLS-terminating proxy.

## Read everything from the environment

```python
import os
from pathlib import Path

BASE_DIR = Path(__file__).resolve().parent.parent

def env_bool(name: str, default: bool = False) -> bool:
    return os.environ.get(name, str(default)).lower() in {"1", "true", "yes"}

def env_list(name: str) -> list[str]:
    return [v.strip() for v in os.environ.get(name, "").split(",") if v.strip()]

SECRET_KEY = os.environ["DJANGO_SECRET_KEY"]          # KeyError at boot is intentional
DEBUG = env_bool("DJANGO_DEBUG")
ALLOWED_HOSTS = env_list("DJANGO_ALLOWED_HOSTS")
CSRF_TRUSTED_ORIGINS = env_list("DJANGO_CSRF_TRUSTED_ORIGINS")   # full origins: https://app.example.com
```

`django-environ` or `pydantic-settings` are fine if the project already uses one. Never
commit a real `SECRET_KEY`; rotate with `SECRET_KEY_FALLBACKS` so existing sessions and
signed tokens survive the change.

## Database

```python
DATABASES = {
    "default": {
        "ENGINE": "django.db.backends.postgresql",
        "NAME": os.environ["DB_NAME"],
        "USER": os.environ["DB_USER"],
        "PASSWORD": os.environ["DB_PASSWORD"],
        "HOST": os.environ["DB_HOST"],
        "PORT": os.environ.get("DB_PORT", "5432"),
        "CONN_MAX_AGE": 60,
        "CONN_HEALTH_CHECKS": True,
        "OPTIONS": {"connect_timeout": 5},
    }
}
```

- `CONN_MAX_AGE` > 0 reuses connections per worker; with many workers, total connections
  = workers x threads x replicas. Put PgBouncer in front if that exceeds the server limit
  (transaction pooling requires `DISABLE_SERVER_SIDE_CURSORS = True`).
- Django 5.1+ supports psycopg 3 connection pools with `"OPTIONS": {"pool": True}`; do
  not combine it with `CONN_MAX_AGE` > 0.
- `ATOMIC_REQUESTS = True` wraps every request in a transaction: simple, but long requests
  hold locks. Prefer explicit `transaction.atomic()` in services for new code.

## HTTPS and cookies (behind a proxy)

```python
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")   # only if the proxy always sets it
SECURE_SSL_REDIRECT = True
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
SECURE_HSTS_SECONDS = 31536000          # start at 3600 and raise after confirming HTTPS everywhere
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
SECURE_HSTS_PRELOAD = False             # preload is hard to undo; opt in deliberately
SECURE_CONTENT_TYPE_NOSNIFF = True
SECURE_REFERRER_POLICY = "same-origin"
X_FRAME_OPTIONS = "DENY"
SESSION_COOKIE_HTTPONLY = True
```

`SECURE_PROXY_SSL_HEADER` set while the proxy does not strip client-supplied
`X-Forwarded-Proto` lets attackers fake HTTPS. Confirm the proxy overwrites it.

HSTS changes are sticky in browsers for `SECURE_HSTS_SECONDS`; confirm with the user
before raising it on a production domain.

Django 6.0 adds built-in Content Security Policy support (`SECURE_CSP` and the CSP
middleware). On 5.2, use `django-csp`.

## Static and media files

```python
STATIC_URL = "static/"
STATIC_ROOT = BASE_DIR / "staticfiles"
STORAGES = {
    "default": {"BACKEND": "django.core.files.storage.FileSystemStorage"},   # or S3 via django-storages
    "staticfiles": {"BACKEND": "whitenoise.storage.CompressedManifestStaticFilesStorage"},
}
```

Run `python manage.py collectstatic --noinput` in the build step. Manifest storage fails
the build on missing referenced files, which is what you want. `STATICFILES_STORAGE` and
`DEFAULT_FILE_STORAGE` were removed in 5.1; use `STORAGES`.

User uploads (media) never go on the app container's disk in multi-instance deploys.

## Logging

```python
LOGGING = {
    "version": 1,
    "disable_existing_loggers": False,
    "handlers": {"console": {"class": "logging.StreamHandler"}},
    "root": {"handlers": ["console"], "level": os.environ.get("LOG_LEVEL", "INFO")},
    "loggers": {"django.request": {"level": "WARNING"}, "django.db.backends": {"level": "WARNING"}},
}
```

Log to stdout; let the platform ship logs. Add a JSON formatter if the log pipeline
expects JSON. Never log request bodies on auth or payment routes.

## Caches and sessions

- Multi-instance deploys need a shared cache (`django.core.cache.backends.redis.RedisCache`)
  for rate limits, cached sessions and throttles. `LocMemCache` is per process.
- `SESSION_ENGINE = "django.contrib.sessions.backends.cached_db"` balances speed and
  durability.

## Server

- WSGI: `gunicorn config.wsgi --workers <2 x cores + 1> --timeout 30`.
- ASGI (async views, streaming, websockets via Channels): `gunicorn config.asgi -k
  uvicorn_worker.UvicornWorker` or `uvicorn config.asgi:application`. Async views on WSGI
  run without a real event loop benefit.
- Health endpoint that does not touch the DB for liveness; a separate readiness check
  that does.

## Final gate

```bash
DJANGO_SETTINGS_MODULE=config.settings DJANGO_DEBUG=0 python manage.py check --deploy --fail-level WARNING
```

Resolve each warning or document why it does not apply (for example, `security.W004`
HSTS while still rolling out). Deploying to production is a user decision: present the
checklist result and wait for confirmation.

About this skill

Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.

esc
Just launched

Codex Skills Pro is live

79 premium skills for Codex, each with scripts, references and an evidence report that proves the result.

Bought one by one$882.21
All 79 skills$49
94% off

That's $0.62 a skill, paid once.

Get all 79 for $49Or pick single skills from $4.99 →

Secure checkout by Creem