Repository: fasouto/django-starter-template Branch: master Commit: 6a8b9b8a35ba Files: 33 Total size: 26.4 KB Directory structure: gitextract__ab4pzx6/ ├── .dockerignore ├── .editorconfig ├── .gitignore ├── .python-version ├── Dockerfile.dev ├── LICENSE ├── apps/ │ ├── __init__.py │ └── base/ │ ├── __init__.py │ ├── models.py │ ├── templates/ │ │ └── base/ │ │ └── home.html │ ├── tests.py │ ├── urls.py │ └── views.py ├── config/ │ ├── __init__.py │ ├── asgi.py │ ├── settings/ │ │ ├── __init__.py │ │ ├── base.py │ │ ├── development.py │ │ └── production.py │ ├── static/ │ │ ├── css/ │ │ │ └── base.css │ │ └── js/ │ │ └── app.js │ ├── templates/ │ │ ├── 403.html │ │ ├── 404.html │ │ ├── 500.html │ │ ├── base.html │ │ └── robots.txt │ ├── urls.py │ └── wsgi.py ├── docker-compose.yml ├── manage.py ├── pyproject.toml ├── railway.toml └── readme.md ================================================ FILE CONTENTS ================================================ ================================================ FILE: .dockerignore ================================================ .venv __pycache__ *.pyc .env db.sqlite3 *.sqlite3 staticfiles public/static public/media .git ================================================ FILE: .editorconfig ================================================ # http://editorconfig.org root = true [*] indent_style = space indent_size = 4 charset = utf-8 end_of_line = lf insert_final_newline = true trim_trailing_whitespace = true [*.py] max_line_length = 120 [*.{vue,js,json,html,yml}] indent_size = 2 [*.md] trim_trailing_whitespace = false # Minified JavaScript files shouldn't be changed [**.min.js] indent_style = ignore insert_final_newline = ignore ================================================ FILE: .gitignore ================================================ ## Python *.py[co] __pycache__/ *.egg* .coverage ## Virtual environment .venv/ ## Environment .env ## Project staticfiles/ public/static public/media ## SQLite3 *.sqlite3 ## OS .DS_Store ._* Thumbs.db Desktop.ini ## Logs *.log ## Editors and IDEs *.sublime-project *.sublime-workspace *.swp *.swo .idea/ .vscode/ *~ \#*\# ================================================ FILE: .python-version ================================================ 3.12 ================================================ FILE: Dockerfile.dev ================================================ FROM python:3.12-slim COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/ WORKDIR /app COPY pyproject.toml uv.lock ./ RUN uv sync --dev COPY . . ================================================ FILE: LICENSE ================================================ Copyright (c) 2017, Fabio Souto Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: apps/__init__.py ================================================ ================================================ FILE: apps/base/__init__.py ================================================ """Application base, containing global templates.""" ================================================ FILE: apps/base/models.py ================================================ """Base models""" ================================================ FILE: apps/base/templates/base/home.html ================================================ {% extends 'base.html' %} {% load static %} {% block title %}Django Starter Template{% endblock %} {% block content %}

Your Django app is running!

Powered by Django {{ django_version }} · Python {{ python_version }}

Status

To get started, edit apps/base/templates/base/home.html

{% endblock %} ================================================ FILE: apps/base/tests.py ================================================ import pytest from django.test import Client @pytest.mark.django_db def test_home_page_returns_200(): client = Client() response = client.get("/") assert response.status_code == 200 @pytest.mark.django_db def test_health_check_returns_200(): client = Client() response = client.get("/health/") assert response.status_code == 200 assert response.json()["status"] == "ok" ================================================ FILE: apps/base/urls.py ================================================ from django.urls import path from .views import home app_name = "base" urlpatterns = [ path("", home, name="home"), ] ================================================ FILE: apps/base/views.py ================================================ import sys import django from django.conf import settings from django.db import connection from django.http import JsonResponse from django.shortcuts import render def home(request): db_ok = False db_is_postgres = False try: connection.ensure_connection() db_ok = True db_is_postgres = connection.vendor == "postgresql" except Exception: pass secret_key = settings.SECRET_KEY secret_changed = secret_key not in ("django-insecure-dev-key-change-me", "build-time-placeholder", "change-me-to-a-random-string") rv = 'Railway variables' checklist = [ ("Database", "Set DATABASE_URL in .env or {}".format(rv), db_ok and (db_is_postgres or settings.DEBUG)), ("SECRET_KEY changed", "Set a unique key in .env or {}".format(rv), secret_changed), ("DEBUG is off", "Set DEBUG=False in production", not settings.DEBUG), ("ALLOWED_HOSTS set", "Add your domain to ALLOWED_HOSTS in {}".format(rv), not settings.DEBUG and len(settings.ALLOWED_HOSTS) > 0), ] return render( request, "base/home.html", { "django_version": django.get_version(), "python_version": f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}", "db_status": db_ok, "checklist": checklist, }, ) def health_check(request): try: with connection.cursor() as cursor: cursor.execute("SELECT 1") return JsonResponse({"status": "ok", "database": "ok"}) except Exception as e: return JsonResponse({"status": "error", "database": str(e)}, status=500) ================================================ FILE: config/__init__.py ================================================ ================================================ FILE: config/asgi.py ================================================ import os from django.core.asgi import get_asgi_application os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings.production") application = get_asgi_application() ================================================ FILE: config/settings/__init__.py ================================================ ================================================ FILE: config/settings/base.py ================================================ """ Base settings shared across all environments. """ import sys from pathlib import Path import environ env = environ.Env() # PATHS BASE_DIR = Path(__file__).resolve().parent.parent # config/ PROJECT_ROOT = BASE_DIR.parent # project root sys.path.append(str(PROJECT_ROOT / "apps")) # GENERAL DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" ROOT_URLCONF = "config.urls" WSGI_APPLICATION = "config.wsgi.application" ALLOWED_HOSTS = [] # INSTALLED APPS INSTALLED_APPS = [ "django.contrib.admin", "django.contrib.admindocs", "django.contrib.auth", "django.contrib.contenttypes", "django.contrib.sessions", "django.contrib.messages", "django.contrib.humanize", "django.contrib.staticfiles", # Local apps "base", ] # PASSWORD HASHING PASSWORD_HASHERS = [ "django.contrib.auth.hashers.Argon2PasswordHasher", "django.contrib.auth.hashers.ScryptPasswordHasher", "django.contrib.auth.hashers.PBKDF2PasswordHasher", "django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher", "django.contrib.auth.hashers.BCryptSHA256PasswordHasher", ] # DEBUG DEBUG = False INTERNAL_IPS = ["127.0.0.1"] # LOCALE TIME_ZONE = "UTC" LANGUAGE_CODE = "en-us" USE_I18N = True USE_TZ = True # MEDIA MEDIA_ROOT = PROJECT_ROOT / "public" / "media" MEDIA_URL = "/media/" # STATIC FILES STATIC_ROOT = PROJECT_ROOT / "staticfiles" STATIC_URL = "/static/" STATICFILES_DIRS = [BASE_DIR / "static"] STORAGES = { "default": { "BACKEND": "django.core.files.storage.FileSystemStorage", }, "staticfiles": { "BACKEND": "whitenoise.storage.CompressedManifestStaticFilesStorage", }, } # TEMPLATES TEMPLATES = [ { "BACKEND": "django.template.backends.django.DjangoTemplates", "DIRS": [BASE_DIR / "templates"], "APP_DIRS": True, "OPTIONS": { "context_processors": [ "django.contrib.auth.context_processors.auth", "django.template.context_processors.debug", "django.template.context_processors.request", "django.contrib.messages.context_processors.messages", ], }, }, ] # MIDDLEWARE 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", ] # LOGGING LOGGING = { "version": 1, "disable_existing_loggers": False, "handlers": { "console": { "class": "logging.StreamHandler", }, }, "root": { "handlers": ["console"], "level": "WARNING", }, } ================================================ FILE: config/settings/development.py ================================================ from .base import * # noqa: F403 from .base import env DEBUG = True SECRET_KEY = env("SECRET_KEY", default="django-insecure-dev-key-change-me") DATABASES = { "default": env.db("DATABASE_URL", default="sqlite:///db.sqlite3"), } ALLOWED_HOSTS = env.list("ALLOWED_HOSTS", default=["localhost", "127.0.0.1"]) CACHES = { "default": { "BACKEND": "django.core.cache.backends.dummy.DummyCache", } } EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend" # Use simple static files storage in development (no collectstatic needed) STORAGES = { "default": { "BACKEND": "django.core.files.storage.FileSystemStorage", }, "staticfiles": { "BACKEND": "django.contrib.staticfiles.storage.StaticFilesStorage", }, } # DJANGO DEBUG TOOLBAR MIDDLEWARE += ["debug_toolbar.middleware.DebugToolbarMiddleware"] # noqa: F405 INSTALLED_APPS += ["debug_toolbar"] # noqa: F405 def show_toolbar(request): from django.conf import settings return settings.DEBUG DEBUG_TOOLBAR_CONFIG = { "SHOW_TOOLBAR_CALLBACK": "config.settings.development.show_toolbar", } ================================================ FILE: config/settings/production.py ================================================ from .base import * # noqa: F403 from .base import env DEBUG = False # SECRET_KEY has a build-time fallback so collectstatic can run during # the Docker/Railpack build phase (before env vars are injected). # At runtime the real key is always required via the environment variable. SECRET_KEY = env("SECRET_KEY", default="build-time-placeholder") DATABASES = { "default": env.db("DATABASE_URL", default="sqlite:///placeholder"), } ALLOWED_HOSTS = env.list("ALLOWED_HOSTS", default=[".railway.app"]) CSRF_TRUSTED_ORIGINS = env.list("CSRF_TRUSTED_ORIGINS", default=[]) # SECURITY # Railway terminates SSL at the proxy; internal traffic is HTTP. # Let the proxy handle HTTPS redirection, not Django. SECURE_SSL_REDIRECT = env.bool("SECURE_SSL_REDIRECT", default=False) SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https") SECURE_HSTS_SECONDS = 31536000 SECURE_HSTS_INCLUDE_SUBDOMAINS = True SECURE_HSTS_PRELOAD = True SESSION_COOKIE_SECURE = True CSRF_COOKIE_SECURE = True ================================================ FILE: config/static/css/base.css ================================================ :root { --color-bg: #f4f7f4; --color-surface: #fff; --color-text: #1b2e1b; --color-muted: #5a7a5a; --color-accent: #092e20; --color-green: #44b78b; --color-green-light: #e8f5e9; --color-ok: #2e7d32; --color-err: #c62828; --color-border: #d5e3d5; --radius: 8px; --max-width: 640px; } *, *::before, *::after { box-sizing: border-box; margin: 0; } body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", sans-serif; background: var(--color-bg); color: var(--color-text); line-height: 1.6; min-height: 100vh; } a { color: var(--color-accent); transition: color 0.15s; } a:hover { color: var(--color-green); } .container { max-width: var(--max-width); margin: 0 auto; padding: 0 1.5rem; } /* Nav */ nav { background: var(--color-accent); padding: 0.6rem 0; border-bottom: 2px solid var(--color-green); } nav .container { display: flex; align-items: center; justify-content: space-between; } .nav-brand { color: #fff; text-decoration: none; font-weight: 700; font-size: 0.95rem; letter-spacing: 0.01em; } .nav-links a { color: rgba(255,255,255,0.65); text-decoration: none; margin-left: 1.25rem; font-size: 0.8rem; font-weight: 500; transition: color 0.15s; } .nav-links a:hover { color: var(--color-green); } /* Main */ main { padding: 3rem 0 4rem; } /* Hero */ .hero { text-align: center; margin-bottom: 2rem; } .hero-logo { width: 72px; height: 72px; object-fit: contain; margin: 1rem 0; } .hero h1 { font-size: 1.4rem; font-weight: 700; color: var(--color-accent); margin-bottom: 0.35rem; letter-spacing: -0.01em; } .hero-subtitle { color: var(--color-muted); font-size: 0.9rem; } .hero-subtitle a { color: var(--color-green); text-decoration: none; font-weight: 600; } .hero-subtitle a:hover { text-decoration: underline; } /* Checklist */ .checklist { background: var(--color-surface); border: 1px solid var(--color-border); border-radius: var(--radius); padding: 1.25rem 1.5rem; margin-bottom: 1.75rem; } .checklist h2 { font-size: 0.75rem; text-transform: uppercase; letter-spacing: 0.08em; color: var(--color-muted); font-weight: 600; margin-bottom: 0.75rem; } .checklist ul { list-style: none; padding: 0; } .checklist li { display: flex; align-items: flex-start; gap: 0.6rem; padding: 0.65rem 0; color: var(--color-muted); } .checklist li.done { color: var(--color-text); } .checklist li + li { border-top: 1px solid rgba(0,0,0,0.04); } .check-label { display: block; font-size: 0.88rem; font-weight: 500; } .check-hint { display: block; font-size: 0.75rem; color: var(--color-muted); margin-top: 0.1rem; font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, monospace; } .check-hint a { color: var(--color-green); text-decoration: underline; text-underline-offset: 2px; } .check { display: inline-flex; align-items: center; justify-content: center; width: 1.2rem; height: 1.2rem; border-radius: 50%; background: #e0e0e0; font-size: 0.6rem; font-weight: 700; color: #999; flex-shrink: 0; margin-top: 0.15rem; } .done .check { background: var(--color-ok); color: #fff; } /* Hint */ .hint { font-size: 0.72rem; color: var(--color-muted); font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, monospace; margin-top: 0.75rem; padding-top: 0.75rem; border-top: 1px solid var(--color-border); } .hint code { background: var(--color-green-light); padding: 0.1em 0.35em; border-radius: 3px; } /* Links */ .quick-links { display: flex; flex-wrap: wrap; justify-content: center; gap: 0.5rem; margin-bottom: 2rem; } .quick-links a { display: inline-block; padding: 0.4rem 0.9rem; background: var(--color-accent); color: #fff; text-decoration: none; border: 1px solid var(--color-accent); border-radius: 6px; font-size: 0.8rem; font-weight: 500; transition: background 0.15s, border-color 0.15s; } .quick-links a:hover { background: #0d3b2a; border-color: #0d3b2a; color: #fff; } .quick-links a.outline { background: transparent; color: var(--color-accent); border: 1px solid var(--color-border); } .quick-links a.outline:hover { border-color: var(--color-accent); background: var(--color-green-light); } ================================================ FILE: config/static/js/app.js ================================================ ================================================ FILE: config/templates/403.html ================================================ 403 Forbidden

403

You are not authorized to view this page.

Return home
================================================ FILE: config/templates/404.html ================================================ 404 Not Found

404

The page you requested was not found.

Return home
================================================ FILE: config/templates/500.html ================================================ 500 Server Error

500

Something went wrong on our end.

Return home
================================================ FILE: config/templates/base.html ================================================ {% load static %} {% block title %}Django Starter{% endblock %} {% block extra_head %}{% endblock %}
{% block content %}{% endblock %}
{% block extra_js %}{% endblock %} ================================================ FILE: config/templates/robots.txt ================================================ User-agent: * Disallow: /admin ================================================ FILE: config/urls.py ================================================ from django.conf import settings from django.conf.urls.static import static from django.contrib import admin from django.urls import include, path from django.views.generic.base import TemplateView from base.views import health_check urlpatterns = [ path("", include("base.urls")), path("admin/doc/", include("django.contrib.admindocs.urls")), path("admin/", admin.site.urls), path("health/", health_check), path( "robots.txt", TemplateView.as_view(template_name="robots.txt", content_type="text/plain"), ), ] if settings.DEBUG: from debug_toolbar.toolbar import debug_toolbar_urls urlpatterns += debug_toolbar_urls() urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) ================================================ FILE: config/wsgi.py ================================================ import os from django.core.wsgi import get_wsgi_application os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings.production") application = get_wsgi_application() ================================================ FILE: docker-compose.yml ================================================ services: web: build: context: . dockerfile: Dockerfile.dev command: uv run python manage.py runserver 0.0.0.0:8000 volumes: - .:/app ports: - "8000:8000" env_file: .env environment: - DATABASE_URL=postgres://postgres:postgres@db:5432/postgres - DJANGO_SETTINGS_MODULE=config.settings.development depends_on: db: condition: service_healthy db: image: postgres:16 volumes: - postgres_data:/var/lib/postgresql/data environment: - POSTGRES_PASSWORD=postgres healthcheck: test: ["CMD-SHELL", "pg_isready -U postgres"] interval: 5s timeout: 5s retries: 5 volumes: postgres_data: ================================================ FILE: manage.py ================================================ #!/usr/bin/env python import os import sys if __name__ == "__main__": # Read .env file before setting defaults so local DJANGO_SETTINGS_MODULE is respected try: import environ environ.Env.read_env(os.path.join(os.path.dirname(__file__), ".env")) except (ImportError, FileNotFoundError): pass os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings.production") from django.core.management import execute_from_command_line execute_from_command_line(sys.argv) ================================================ FILE: pyproject.toml ================================================ [project] name = "django-starter" version = "0.1.0" requires-python = ">=3.12" dependencies = [ "django>=5.2.13,<5.3", "gunicorn>=22.0", "psycopg[binary,pool]>=3.1", "whitenoise[brotli]>=6.7", "django-environ>=0.11", "argon2-cffi>=23.1", ] [dependency-groups] dev = [ "django-debug-toolbar>=4.4", "ruff>=0.6", "pytest>=8.0", "pytest-django>=4.8", "coverage>=7.6", ] [tool.ruff] line-length = 120 target-version = "py312" [tool.pytest.ini_options] DJANGO_SETTINGS_MODULE = "config.settings.development" python_files = ["tests.py", "test_*.py"] ================================================ FILE: railway.toml ================================================ [build] builder = "RAILPACK" buildCommand = "python manage.py collectstatic --noinput" [deploy] preDeployCommand = "python manage.py migrate --noinput" startCommand = "gunicorn config.wsgi --bind 0.0.0.0:$PORT" healthcheckPath = "/health/" healthcheckTimeout = 300 restartPolicyType = "ON_FAILURE" restartPolicyMaxRetries = 10 ================================================ FILE: readme.md ================================================

Django Pony

Django Starter Template

A production-ready Django 5.2 LTS starter template for [Railway](https://railway.com). [![Deploy on Railway](https://railway.com/button.svg)](https://railway.com/deploy/django-starter-template?referralCode=iZa9TM&utm_medium=integration&utm_source=template&utm_campaign=generic) ## Deploy to Railway Click the button above to deploy this template. Railway will: 1. Create a new Django web service 2. Provision a PostgreSQL database 3. Set `DATABASE_URL` and `SECRET_KEY` automatically 4. Run migrations on deploy 5. Start the application with gunicorn Your app will be live in under a minute. ### Environment variables These are set automatically by Railway. Override them in your service settings if needed: | Variable | Description | Default | |----------|-------------|---------| | `SECRET_KEY` | Django secret key | Auto-generated | | `DATABASE_URL` | PostgreSQL connection string | Provided by Railway | | `ALLOWED_HOSTS` | Comma-separated hostnames | `.railway.app` | | `CSRF_TRUSTED_ORIGINS` | Full URLs for POST requests (e.g. `https://myapp.up.railway.app`) | `[]` | | `DJANGO_SETTINGS_MODULE` | Settings module | `config.settings.production` | **Important:** If you add a custom domain, add it to both `ALLOWED_HOSTS` and `CSRF_TRUSTED_ORIGINS` (with `https://` prefix). Without `CSRF_TRUSTED_ORIGINS`, POST requests (login, admin, forms) will return 403. ## Local Development ### Option A: uv (recommended) Prerequisites: [Python 3.12+](https://python.org), [uv](https://docs.astral.sh/uv/getting-started/installation/) ```bash git clone https://github.com/fasouto/django-starter-template.git cd django-starter-template # Install dependencies uv sync --dev # Set up environment cp .env.example .env # Run migrations and create admin user uv run python manage.py migrate uv run python manage.py createsuperuser # Start development server uv run python manage.py runserver ``` Open [http://localhost:8000](http://localhost:8000). The admin panel is at [http://localhost:8000/admin/](http://localhost:8000/admin/). ```bash # Run tests uv run pytest # Lint and format uv run ruff check . uv run ruff format . ``` ### Option B: Docker Compose Prerequisites: [Docker](https://docs.docker.com/get-docker/) ```bash git clone https://github.com/fasouto/django-starter-template.git cd django-starter-template cp .env.example .env # Start Django + PostgreSQL docker compose up # In another terminal: docker compose exec web uv run python manage.py migrate docker compose exec web uv run python manage.py createsuperuser ``` Open [http://localhost:8000](http://localhost:8000). Code changes reload automatically. ## Project Structure ``` . ├── apps/ │ └── base/ # Default app (home page, health check, tests) │ ├── templates/base/ # App templates │ ├── tests.py # Example tests │ ├── urls.py │ └── views.py ├── config/ # Django project package │ ├── settings/ │ │ ├── base.py # Shared settings │ │ ├── development.py # Dev settings (DEBUG=True, SQLite) │ │ └── production.py # Production settings (Postgres, security) │ ├── static/ # Project-level static files │ │ └── css/base.css │ ├── templates/ # Project-level templates (base.html, error pages) │ ├── asgi.py │ ├── urls.py │ └── wsgi.py ├── docker-compose.yml # Local dev with Docker (Django + Postgres) ├── Dockerfile.dev # Dev container ├── pyproject.toml # Dependencies and tool config ├── railway.toml # Railway deployment config ├── uv.lock # Locked dependencies └── manage.py ``` ## What's Included - **[Django 5.2 LTS](https://docs.djangoproject.com/en/5.2/)**: supported until April 2028 - **[PostgreSQL](https://www.postgresql.org/)** via psycopg3, modern async-capable adapter - **[WhiteNoise](https://whitenoise.readthedocs.io/)**: serve static files without nginx, with brotli compression - **[django-environ](https://django-environ.readthedocs.io/)**: configure via environment variables and `.env` files - **[Argon2](https://docs.djangoproject.com/en/5.2/topics/auth/passwords/#using-argon2-with-django)** password hashing (winner of the Password Hashing Competition) - **Split settings** for separate development and production configurations - **Health check** at `/health/`, returns JSON for Railway monitoring - **[django-debug-toolbar](https://django-debug-toolbar.readthedocs.io/)**: SQL queries, templates, cache inspection (dev only) - **[ruff](https://docs.astral.sh/ruff/)** for linting and formatting - **[pytest](https://docs.pytest.org/) + [pytest-django](https://pytest-django.readthedocs.io/)** for testing ## Customization ### Adding a new app ```bash mkdir apps/myapp uv run python manage.py startapp myapp apps/myapp ``` Then add `"myapp"` to `INSTALLED_APPS` in `config/settings/base.py`. ### Replacing the CSS The included `config/static/css/base.css` is minimal and framework-free. Replace it with Bootstrap, Tailwind, or any CSS framework you prefer. ### Adding Celery Add `celery[redis]` to your dependencies, create `config/celery.py`, and add a Redis service to your Railway project or `docker-compose.yml`. ## License MIT. See [LICENSE](LICENSE).