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.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- django-apps Installs to ~/.agents/skills/django-apps/ for every project on this machine.
curl -fsSL codexskills.dev/cli.mjs -o codex-skills.mjsnode codex-skills.mjs install django-apps The CLI also searches, updates and removes skills. Run it with no arguments to pick from a list. Needs Node 20.12+.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- --project django-apps Installs to .agents/skills/django-apps/ in the current repository. Commit it and everyone who runs Codex there gets it.
curl -fsSLO codexskills.dev/downloads/django-apps.zipunzip -o django-apps.zip -d ~/.agents/skills/ The archive holds the skill folder with its generated agents/openai.yaml. Unzip into .agents/skills/ to scope it to a repo.
& ([scriptblock]::Create((irm codexskills.dev/install.ps1))) django-apps PowerShell 5.1 or 7. Installs to %USERPROFILE%\.agents\skills\. Add -Project for the current repo.
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
- You are building a JSON API with serializers and viewsets: use django-rest-framework
- The problem is slow ORM queries or N+1 patterns: use django-orm-performance
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.
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, thenpyproject.toml/requirements*.txtfor the Django version and tooling (pytest-django ormanage.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, neverFloatField. on_delete:PROTECTfor business records,CASCADEfor owned children,SET_NULLonly withnull=Trueand a reason.- Use
CheckConstraint(condition=...); the oldcheck=keyword is deprecated since 5.1. save(update_fields=[...])avoids overwriting concurrent changes to other columns.- Use
UniqueConstraint, notunique_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
AddIndexConcurrentlyin a migration withatomic = False. For column changes under load, follow$zero-downtime-migrations. - Data migrations use
apps.get_model(), never direct model imports, and provide areverse_code(ormigrations.RunPython.noopwith 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 (
ModelFormwith an explicitfieldslist, neverexcludeor"__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 withtransaction.on_commit(...)so they never fire for a rolled-back transaction. - Scope querysets to the user:
Order.objects.filter(customer=request.user)thenget_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-djangowith@pytest.mark.django_db, ordjango.test.TestCase(wraps each test in a transaction). Use factories (factory_boy) over fixtures files.- Test model constraints by asserting
IntegrityErrorinsidetransaction.atomic(). - Assert query counts on hot views:
django_assert_num_queries(pytest-django) orself.assertNumQueries(n). For N+1 fixes use$django-orm-performance. TestCase.captureOnCommitCallbacks(execute=True)to teston_commitside effects.
Upgrading Django
- Run tests with
python -Wa manage.py test(or-W error::DeprecationWarningin pytest) on the current version and fix deprecations first. - Upgrade one feature release at a time (5.2 to 6.0, not 4.2 to 6.0 in one jump).
- Read the release notes’ “Backwards incompatible changes” section for each step; Django 6.0 requires Python 3.12+.
- 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_nowfields are not set byQuerySet.update(); set them explicitly there.bulk_createandupdate()skipsave(), signals and model validation.full_clean()is not called bysave(); constraints inMetaare your real guarantee.- Signals hide control flow; prefer explicit service calls for business logic.
- Timezone: keep
USE_TZ = True; compare withtimezone.now(), neverdatetime.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.yaml
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.md
# 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.