[
  {
    "path": ".dockerignore",
    "content": ".venv\n__pycache__\n*.pyc\n.env\ndb.sqlite3\n*.sqlite3\nstaticfiles\npublic/static\npublic/media\n.git\n"
  },
  {
    "path": ".editorconfig",
    "content": "# http://editorconfig.org\n\nroot = true\n\n[*]\nindent_style = space\nindent_size = 4\ncharset = utf-8\nend_of_line = lf\ninsert_final_newline = true\ntrim_trailing_whitespace = true\n\n[*.py]\nmax_line_length = 120\n\n[*.{vue,js,json,html,yml}]\nindent_size = 2\n\n[*.md]\ntrim_trailing_whitespace = false\n\n# Minified JavaScript files shouldn't be changed\n[**.min.js]\nindent_style = ignore\ninsert_final_newline = ignore\n"
  },
  {
    "path": ".gitignore",
    "content": "## Python\n*.py[co]\n__pycache__/\n*.egg*\n.coverage\n\n## Virtual environment\n.venv/\n\n## Environment\n.env\n\n## Project\nstaticfiles/\npublic/static\npublic/media\n\n## SQLite3\n*.sqlite3\n\n## OS\n.DS_Store\n._*\nThumbs.db\nDesktop.ini\n\n## Logs\n*.log\n\n## Editors and IDEs\n*.sublime-project\n*.sublime-workspace\n*.swp\n*.swo\n.idea/\n.vscode/\n*~\n\\#*\\#\n"
  },
  {
    "path": ".python-version",
    "content": "3.12\n"
  },
  {
    "path": "Dockerfile.dev",
    "content": "FROM python:3.12-slim\n\nCOPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/\n\nWORKDIR /app\n\nCOPY pyproject.toml uv.lock ./\n\nRUN uv sync --dev\n\nCOPY . .\n"
  },
  {
    "path": "LICENSE",
    "content": "Copyright (c) 2017, Fabio Souto\n\nPermission is hereby granted, free of charge, to any person\nobtaining a copy of this software and associated documentation\nfiles (the \"Software\"), to deal in the Software without\nrestriction, including without limitation the rights to use,\ncopy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the\nSoftware is furnished to do so, subject to the following\nconditions:\n\nThe above copyright notice and this permission notice shall be\nincluded in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES\nOF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\nNONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT\nHOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\nWHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\nFROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR\nOTHER DEALINGS IN THE SOFTWARE."
  },
  {
    "path": "apps/__init__.py",
    "content": ""
  },
  {
    "path": "apps/base/__init__.py",
    "content": "\"\"\"Application base, containing global templates.\"\"\"\n"
  },
  {
    "path": "apps/base/models.py",
    "content": "\"\"\"Base models\"\"\"\n"
  },
  {
    "path": "apps/base/templates/base/home.html",
    "content": "{% extends 'base.html' %}\n{% load static %}\n\n{% block title %}Django Starter Template{% endblock %}\n\n{% block content %}\n<div class=\"hero\">\n  <img src=\"{% static 'img/django-pony.png' %}\" alt=\"Django Pony\" class=\"hero-logo\">\n  <h1>Your Django app is running!</h1>\n  <p class=\"hero-subtitle\">\n    Powered by <a href=\"https://www.djangoproject.com/\" target=\"_blank\" rel=\"noopener\">Django {{ django_version }}</a>\n    &middot; Python {{ python_version }}\n  </p>\n</div>\n\n<div class=\"checklist\">\n  <h2>Status</h2>\n  <ul>\n    {% for label, hint, done in checklist %}\n    <li class=\"{% if done %}done{% endif %}\">\n      <span class=\"check\">{% if done %}&#10003;{% else %}&#10005;{% endif %}</span>\n      <div>\n        <span class=\"check-label\">{{ label }}</span>\n        {% if not done %}<span class=\"check-hint\">{{ hint|safe }}</span>{% endif %}\n      </div>\n    </li>\n    {% endfor %}\n  </ul>\n  <p class=\"hint\">To get started, edit <code>apps/base/templates/base/home.html</code></p>\n</div>\n\n<div class=\"quick-links\">\n  <a href=\"/admin/\">Admin Panel</a>\n  <a href=\"/health/\">Health Check</a>\n  <a href=\"https://docs.djangoproject.com/en/5.2/\" class=\"outline\" target=\"_blank\" rel=\"noopener\">Django Docs</a>\n  <a href=\"https://docs.railway.com/\" class=\"outline\" target=\"_blank\" rel=\"noopener\">Railway Docs</a>\n</div>\n{% endblock %}\n"
  },
  {
    "path": "apps/base/tests.py",
    "content": "import pytest\nfrom django.test import Client\n\n\n@pytest.mark.django_db\ndef test_home_page_returns_200():\n    client = Client()\n    response = client.get(\"/\")\n    assert response.status_code == 200\n\n\n@pytest.mark.django_db\ndef test_health_check_returns_200():\n    client = Client()\n    response = client.get(\"/health/\")\n    assert response.status_code == 200\n    assert response.json()[\"status\"] == \"ok\"\n"
  },
  {
    "path": "apps/base/urls.py",
    "content": "from django.urls import path\n\nfrom .views import home\n\napp_name = \"base\"\n\nurlpatterns = [\n    path(\"\", home, name=\"home\"),\n]\n"
  },
  {
    "path": "apps/base/views.py",
    "content": "import sys\n\nimport django\nfrom django.conf import settings\nfrom django.db import connection\nfrom django.http import JsonResponse\nfrom django.shortcuts import render\n\n\ndef home(request):\n    db_ok = False\n    db_is_postgres = False\n    try:\n        connection.ensure_connection()\n        db_ok = True\n        db_is_postgres = connection.vendor == \"postgresql\"\n    except Exception:\n        pass\n\n    secret_key = settings.SECRET_KEY\n    secret_changed = secret_key not in (\"django-insecure-dev-key-change-me\", \"build-time-placeholder\", \"change-me-to-a-random-string\")\n    rv = '<a href=\"https://docs.railway.com/variables\" target=\"_blank\">Railway variables</a>'\n    checklist = [\n        (\"Database\", \"Set DATABASE_URL in .env or {}\".format(rv), db_ok and (db_is_postgres or settings.DEBUG)),\n        (\"SECRET_KEY changed\", \"Set a unique key in .env or {}\".format(rv), secret_changed),\n        (\"DEBUG is off\", \"Set DEBUG=False in production\", not settings.DEBUG),\n        (\"ALLOWED_HOSTS set\", \"Add your domain to ALLOWED_HOSTS in {}\".format(rv), not settings.DEBUG and len(settings.ALLOWED_HOSTS) > 0),\n    ]\n\n    return render(\n        request,\n        \"base/home.html\",\n        {\n            \"django_version\": django.get_version(),\n            \"python_version\": f\"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}\",\n            \"db_status\": db_ok,\n            \"checklist\": checklist,\n        },\n    )\n\n\ndef health_check(request):\n    try:\n        with connection.cursor() as cursor:\n            cursor.execute(\"SELECT 1\")\n        return JsonResponse({\"status\": \"ok\", \"database\": \"ok\"})\n    except Exception as e:\n        return JsonResponse({\"status\": \"error\", \"database\": str(e)}, status=500)\n"
  },
  {
    "path": "config/__init__.py",
    "content": ""
  },
  {
    "path": "config/asgi.py",
    "content": "import os\n\nfrom django.core.asgi import get_asgi_application\n\nos.environ.setdefault(\"DJANGO_SETTINGS_MODULE\", \"config.settings.production\")\n\napplication = get_asgi_application()\n"
  },
  {
    "path": "config/settings/__init__.py",
    "content": ""
  },
  {
    "path": "config/settings/base.py",
    "content": "\"\"\"\nBase settings shared across all environments.\n\"\"\"\n\nimport sys\nfrom pathlib import Path\n\nimport environ\n\nenv = environ.Env()\n\n# PATHS\nBASE_DIR = Path(__file__).resolve().parent.parent  # config/\nPROJECT_ROOT = BASE_DIR.parent  # project root\n\nsys.path.append(str(PROJECT_ROOT / \"apps\"))\n\n# GENERAL\nDEFAULT_AUTO_FIELD = \"django.db.models.BigAutoField\"\nROOT_URLCONF = \"config.urls\"\nWSGI_APPLICATION = \"config.wsgi.application\"\n\nALLOWED_HOSTS = []\n\n# INSTALLED APPS\nINSTALLED_APPS = [\n    \"django.contrib.admin\",\n    \"django.contrib.admindocs\",\n    \"django.contrib.auth\",\n    \"django.contrib.contenttypes\",\n    \"django.contrib.sessions\",\n    \"django.contrib.messages\",\n    \"django.contrib.humanize\",\n    \"django.contrib.staticfiles\",\n    # Local apps\n    \"base\",\n]\n\n# PASSWORD HASHING\nPASSWORD_HASHERS = [\n    \"django.contrib.auth.hashers.Argon2PasswordHasher\",\n    \"django.contrib.auth.hashers.ScryptPasswordHasher\",\n    \"django.contrib.auth.hashers.PBKDF2PasswordHasher\",\n    \"django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher\",\n    \"django.contrib.auth.hashers.BCryptSHA256PasswordHasher\",\n]\n\n# DEBUG\nDEBUG = False\nINTERNAL_IPS = [\"127.0.0.1\"]\n\n# LOCALE\nTIME_ZONE = \"UTC\"\nLANGUAGE_CODE = \"en-us\"\nUSE_I18N = True\nUSE_TZ = True\n\n# MEDIA\nMEDIA_ROOT = PROJECT_ROOT / \"public\" / \"media\"\nMEDIA_URL = \"/media/\"\n\n# STATIC FILES\nSTATIC_ROOT = PROJECT_ROOT / \"staticfiles\"\nSTATIC_URL = \"/static/\"\nSTATICFILES_DIRS = [BASE_DIR / \"static\"]\nSTORAGES = {\n    \"default\": {\n        \"BACKEND\": \"django.core.files.storage.FileSystemStorage\",\n    },\n    \"staticfiles\": {\n        \"BACKEND\": \"whitenoise.storage.CompressedManifestStaticFilesStorage\",\n    },\n}\n\n# TEMPLATES\nTEMPLATES = [\n    {\n        \"BACKEND\": \"django.template.backends.django.DjangoTemplates\",\n        \"DIRS\": [BASE_DIR / \"templates\"],\n        \"APP_DIRS\": True,\n        \"OPTIONS\": {\n            \"context_processors\": [\n                \"django.contrib.auth.context_processors.auth\",\n                \"django.template.context_processors.debug\",\n                \"django.template.context_processors.request\",\n                \"django.contrib.messages.context_processors.messages\",\n            ],\n        },\n    },\n]\n\n# MIDDLEWARE\nMIDDLEWARE = [\n    \"django.middleware.security.SecurityMiddleware\",\n    \"whitenoise.middleware.WhiteNoiseMiddleware\",\n    \"django.contrib.sessions.middleware.SessionMiddleware\",\n    \"django.middleware.common.CommonMiddleware\",\n    \"django.middleware.csrf.CsrfViewMiddleware\",\n    \"django.contrib.auth.middleware.AuthenticationMiddleware\",\n    \"django.contrib.messages.middleware.MessageMiddleware\",\n    \"django.middleware.clickjacking.XFrameOptionsMiddleware\",\n]\n\n# LOGGING\nLOGGING = {\n    \"version\": 1,\n    \"disable_existing_loggers\": False,\n    \"handlers\": {\n        \"console\": {\n            \"class\": \"logging.StreamHandler\",\n        },\n    },\n    \"root\": {\n        \"handlers\": [\"console\"],\n        \"level\": \"WARNING\",\n    },\n}\n"
  },
  {
    "path": "config/settings/development.py",
    "content": "from .base import *  # noqa: F403\nfrom .base import env\n\nDEBUG = True\n\nSECRET_KEY = env(\"SECRET_KEY\", default=\"django-insecure-dev-key-change-me\")\n\nDATABASES = {\n    \"default\": env.db(\"DATABASE_URL\", default=\"sqlite:///db.sqlite3\"),\n}\n\nALLOWED_HOSTS = env.list(\"ALLOWED_HOSTS\", default=[\"localhost\", \"127.0.0.1\"])\n\nCACHES = {\n    \"default\": {\n        \"BACKEND\": \"django.core.cache.backends.dummy.DummyCache\",\n    }\n}\n\nEMAIL_BACKEND = \"django.core.mail.backends.console.EmailBackend\"\n\n# Use simple static files storage in development (no collectstatic needed)\nSTORAGES = {\n    \"default\": {\n        \"BACKEND\": \"django.core.files.storage.FileSystemStorage\",\n    },\n    \"staticfiles\": {\n        \"BACKEND\": \"django.contrib.staticfiles.storage.StaticFilesStorage\",\n    },\n}\n\n# DJANGO DEBUG TOOLBAR\nMIDDLEWARE += [\"debug_toolbar.middleware.DebugToolbarMiddleware\"]  # noqa: F405\nINSTALLED_APPS += [\"debug_toolbar\"]  # noqa: F405\n\n\ndef show_toolbar(request):\n    from django.conf import settings\n\n    return settings.DEBUG\n\n\nDEBUG_TOOLBAR_CONFIG = {\n    \"SHOW_TOOLBAR_CALLBACK\": \"config.settings.development.show_toolbar\",\n}\n"
  },
  {
    "path": "config/settings/production.py",
    "content": "from .base import *  # noqa: F403\nfrom .base import env\n\nDEBUG = False\n\n# SECRET_KEY has a build-time fallback so collectstatic can run during\n# the Docker/Railpack build phase (before env vars are injected).\n# At runtime the real key is always required via the environment variable.\nSECRET_KEY = env(\"SECRET_KEY\", default=\"build-time-placeholder\")\n\nDATABASES = {\n    \"default\": env.db(\"DATABASE_URL\", default=\"sqlite:///placeholder\"),\n}\n\nALLOWED_HOSTS = env.list(\"ALLOWED_HOSTS\", default=[\".railway.app\"])\n\nCSRF_TRUSTED_ORIGINS = env.list(\"CSRF_TRUSTED_ORIGINS\", default=[])\n\n# SECURITY\n# Railway terminates SSL at the proxy; internal traffic is HTTP.\n# Let the proxy handle HTTPS redirection, not Django.\nSECURE_SSL_REDIRECT = env.bool(\"SECURE_SSL_REDIRECT\", default=False)\nSECURE_PROXY_SSL_HEADER = (\"HTTP_X_FORWARDED_PROTO\", \"https\")\nSECURE_HSTS_SECONDS = 31536000\nSECURE_HSTS_INCLUDE_SUBDOMAINS = True\nSECURE_HSTS_PRELOAD = True\nSESSION_COOKIE_SECURE = True\nCSRF_COOKIE_SECURE = True\n"
  },
  {
    "path": "config/static/css/base.css",
    "content": ":root {\n  --color-bg: #f4f7f4;\n  --color-surface: #fff;\n  --color-text: #1b2e1b;\n  --color-muted: #5a7a5a;\n  --color-accent: #092e20;\n  --color-green: #44b78b;\n  --color-green-light: #e8f5e9;\n  --color-ok: #2e7d32;\n  --color-err: #c62828;\n  --color-border: #d5e3d5;\n  --radius: 8px;\n  --max-width: 640px;\n}\n\n*, *::before, *::after { box-sizing: border-box; margin: 0; }\n\nbody {\n  font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, \"Helvetica Neue\", sans-serif;\n  background: var(--color-bg);\n  color: var(--color-text);\n  line-height: 1.6;\n  min-height: 100vh;\n}\n\na { color: var(--color-accent); transition: color 0.15s; }\na:hover { color: var(--color-green); }\n\n.container { max-width: var(--max-width); margin: 0 auto; padding: 0 1.5rem; }\n\n/* Nav */\nnav {\n  background: var(--color-accent);\n  padding: 0.6rem 0;\n  border-bottom: 2px solid var(--color-green);\n}\nnav .container { display: flex; align-items: center; justify-content: space-between; }\n.nav-brand { color: #fff; text-decoration: none; font-weight: 700; font-size: 0.95rem; letter-spacing: 0.01em; }\n.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; }\n.nav-links a:hover { color: var(--color-green); }\n\n/* Main */\nmain { padding: 3rem 0 4rem; }\n\n/* Hero */\n.hero {\n  text-align: center;\n  margin-bottom: 2rem;\n}\n.hero-logo {\n  width: 72px;\n  height: 72px;\n  object-fit: contain;\n  margin: 1rem 0;\n}\n.hero h1 {\n  font-size: 1.4rem;\n  font-weight: 700;\n  color: var(--color-accent);\n  margin-bottom: 0.35rem;\n  letter-spacing: -0.01em;\n}\n.hero-subtitle {\n  color: var(--color-muted);\n  font-size: 0.9rem;\n}\n.hero-subtitle a {\n  color: var(--color-green);\n  text-decoration: none;\n  font-weight: 600;\n}\n.hero-subtitle a:hover { text-decoration: underline; }\n\n/* Checklist */\n.checklist {\n  background: var(--color-surface);\n  border: 1px solid var(--color-border);\n  border-radius: var(--radius);\n  padding: 1.25rem 1.5rem;\n  margin-bottom: 1.75rem;\n}\n.checklist h2 {\n  font-size: 0.75rem;\n  text-transform: uppercase;\n  letter-spacing: 0.08em;\n  color: var(--color-muted);\n  font-weight: 600;\n  margin-bottom: 0.75rem;\n}\n.checklist ul {\n  list-style: none;\n  padding: 0;\n}\n.checklist li {\n  display: flex;\n  align-items: flex-start;\n  gap: 0.6rem;\n  padding: 0.65rem 0;\n  color: var(--color-muted);\n}\n.checklist li.done { color: var(--color-text); }\n.checklist li + li { border-top: 1px solid rgba(0,0,0,0.04); }\n.check-label { display: block; font-size: 0.88rem; font-weight: 500; }\n.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; }\n.check-hint a { color: var(--color-green); text-decoration: underline; text-underline-offset: 2px; }\n.check {\n  display: inline-flex;\n  align-items: center;\n  justify-content: center;\n  width: 1.2rem;\n  height: 1.2rem;\n  border-radius: 50%;\n  background: #e0e0e0;\n  font-size: 0.6rem;\n  font-weight: 700;\n  color: #999;\n  flex-shrink: 0;\n  margin-top: 0.15rem;\n}\n.done .check {\n  background: var(--color-ok);\n  color: #fff;\n}\n\n/* Hint */\n.hint {\n  font-size: 0.72rem;\n  color: var(--color-muted);\n  font-family: ui-monospace, SFMono-Regular, \"SF Mono\", Menlo, monospace;\n  margin-top: 0.75rem;\n  padding-top: 0.75rem;\n  border-top: 1px solid var(--color-border);\n}\n.hint code {\n  background: var(--color-green-light);\n  padding: 0.1em 0.35em;\n  border-radius: 3px;\n}\n\n/* Links */\n.quick-links {\n  display: flex;\n  flex-wrap: wrap;\n  justify-content: center;\n  gap: 0.5rem;\n  margin-bottom: 2rem;\n}\n.quick-links a {\n  display: inline-block;\n  padding: 0.4rem 0.9rem;\n  background: var(--color-accent);\n  color: #fff;\n  text-decoration: none;\n  border: 1px solid var(--color-accent);\n  border-radius: 6px;\n  font-size: 0.8rem;\n  font-weight: 500;\n  transition: background 0.15s, border-color 0.15s;\n}\n.quick-links a:hover { background: #0d3b2a; border-color: #0d3b2a; color: #fff; }\n.quick-links a.outline {\n  background: transparent;\n  color: var(--color-accent);\n  border: 1px solid var(--color-border);\n}\n.quick-links a.outline:hover { border-color: var(--color-accent); background: var(--color-green-light); }\n\n"
  },
  {
    "path": "config/static/js/app.js",
    "content": ""
  },
  {
    "path": "config/templates/403.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n  <meta charset=\"utf-8\">\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n  <title>403 Forbidden</title>\n  <style>\n    body { font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, sans-serif; display: flex; align-items: center; justify-content: center; min-height: 100vh; margin: 0; background: #f8f9fa; color: #1a1a2e; }\n    .error { text-align: center; }\n    .error h1 { font-size: 8rem; font-weight: 800; margin: 0; line-height: 1; color: #e74c3c; }\n    .error p { font-size: 1.25rem; color: #555; margin: 1rem 0 2rem; }\n    .error a { color: #2563eb; text-decoration: none; border-bottom: 1px solid; }\n  </style>\n</head>\n<body>\n  <div class=\"error\">\n    <h1>403</h1>\n    <p>You are not authorized to view this page.</p>\n    <a href=\"/\">Return home</a>\n  </div>\n</body>\n</html>\n"
  },
  {
    "path": "config/templates/404.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n  <meta charset=\"utf-8\">\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n  <title>404 Not Found</title>\n  <style>\n    body { font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, sans-serif; display: flex; align-items: center; justify-content: center; min-height: 100vh; margin: 0; background: #f8f9fa; color: #1a1a2e; }\n    .error { text-align: center; }\n    .error h1 { font-size: 8rem; font-weight: 800; margin: 0; line-height: 1; color: #e74c3c; }\n    .error p { font-size: 1.25rem; color: #555; margin: 1rem 0 2rem; }\n    .error a { color: #2563eb; text-decoration: none; border-bottom: 1px solid; }\n  </style>\n</head>\n<body>\n  <div class=\"error\">\n    <h1>404</h1>\n    <p>The page you requested was not found.</p>\n    <a href=\"/\">Return home</a>\n  </div>\n</body>\n</html>\n"
  },
  {
    "path": "config/templates/500.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n  <meta charset=\"utf-8\">\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n  <title>500 Server Error</title>\n  <style>\n    body { font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, sans-serif; display: flex; align-items: center; justify-content: center; min-height: 100vh; margin: 0; background: #f8f9fa; color: #1a1a2e; }\n    .error { text-align: center; }\n    .error h1 { font-size: 8rem; font-weight: 800; margin: 0; line-height: 1; color: #e74c3c; }\n    .error p { font-size: 1.25rem; color: #555; margin: 1rem 0 2rem; }\n    .error a { color: #2563eb; text-decoration: none; border-bottom: 1px solid; }\n  </style>\n</head>\n<body>\n  <div class=\"error\">\n    <h1>500</h1>\n    <p>Something went wrong on our end.</p>\n    <a href=\"/\">Return home</a>\n  </div>\n</body>\n</html>\n"
  },
  {
    "path": "config/templates/base.html",
    "content": "{% load static %}\n<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n  <meta charset=\"utf-8\">\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n  <title>{% block title %}Django Starter{% endblock %}</title>\n  <link rel=\"stylesheet\" href=\"{% static 'css/base.css' %}\">\n  <link rel=\"icon\" href=\"{% static 'favicon.ico' %}\">\n  {% block extra_head %}{% endblock %}\n</head>\n<body>\n  <nav>\n    <div class=\"container\">\n      <a href=\"/\" class=\"nav-brand\">Django Starter Template</a>\n      <div class=\"nav-links\">\n        <a href=\"https://github.com/fasouto/django-starter-template\" target=\"_blank\" rel=\"noopener\">GitHub</a>\n      </div>\n    </div>\n  </nav>\n\n  <main class=\"container\">\n    {% block content %}{% endblock %}\n  </main>\n\n  {% block extra_js %}{% endblock %}\n</body>\n</html>\n"
  },
  {
    "path": "config/templates/robots.txt",
    "content": "User-agent: *\nDisallow: /admin"
  },
  {
    "path": "config/urls.py",
    "content": "from django.conf import settings\nfrom django.conf.urls.static import static\nfrom django.contrib import admin\nfrom django.urls import include, path\nfrom django.views.generic.base import TemplateView\n\nfrom base.views import health_check\n\nurlpatterns = [\n    path(\"\", include(\"base.urls\")),\n    path(\"admin/doc/\", include(\"django.contrib.admindocs.urls\")),\n    path(\"admin/\", admin.site.urls),\n    path(\"health/\", health_check),\n    path(\n        \"robots.txt\",\n        TemplateView.as_view(template_name=\"robots.txt\", content_type=\"text/plain\"),\n    ),\n]\n\nif settings.DEBUG:\n    from debug_toolbar.toolbar import debug_toolbar_urls\n\n    urlpatterns += debug_toolbar_urls()\n    urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)\n"
  },
  {
    "path": "config/wsgi.py",
    "content": "import os\n\nfrom django.core.wsgi import get_wsgi_application\n\nos.environ.setdefault(\"DJANGO_SETTINGS_MODULE\", \"config.settings.production\")\n\napplication = get_wsgi_application()\n"
  },
  {
    "path": "docker-compose.yml",
    "content": "services:\n  web:\n    build:\n      context: .\n      dockerfile: Dockerfile.dev\n    command: uv run python manage.py runserver 0.0.0.0:8000\n    volumes:\n      - .:/app\n    ports:\n      - \"8000:8000\"\n    env_file: .env\n    environment:\n      - DATABASE_URL=postgres://postgres:postgres@db:5432/postgres\n      - DJANGO_SETTINGS_MODULE=config.settings.development\n    depends_on:\n      db:\n        condition: service_healthy\n\n  db:\n    image: postgres:16\n    volumes:\n      - postgres_data:/var/lib/postgresql/data\n    environment:\n      - POSTGRES_PASSWORD=postgres\n    healthcheck:\n      test: [\"CMD-SHELL\", \"pg_isready -U postgres\"]\n      interval: 5s\n      timeout: 5s\n      retries: 5\n\nvolumes:\n  postgres_data:\n"
  },
  {
    "path": "manage.py",
    "content": "#!/usr/bin/env python\nimport os\nimport sys\n\nif __name__ == \"__main__\":\n    # Read .env file before setting defaults so local DJANGO_SETTINGS_MODULE is respected\n    try:\n        import environ\n\n        environ.Env.read_env(os.path.join(os.path.dirname(__file__), \".env\"))\n    except (ImportError, FileNotFoundError):\n        pass\n\n    os.environ.setdefault(\"DJANGO_SETTINGS_MODULE\", \"config.settings.production\")\n\n    from django.core.management import execute_from_command_line\n\n    execute_from_command_line(sys.argv)\n"
  },
  {
    "path": "pyproject.toml",
    "content": "[project]\nname = \"django-starter\"\nversion = \"0.1.0\"\nrequires-python = \">=3.12\"\ndependencies = [\n    \"django>=5.2.13,<5.3\",\n    \"gunicorn>=22.0\",\n    \"psycopg[binary,pool]>=3.1\",\n    \"whitenoise[brotli]>=6.7\",\n    \"django-environ>=0.11\",\n    \"argon2-cffi>=23.1\",\n]\n\n[dependency-groups]\ndev = [\n    \"django-debug-toolbar>=4.4\",\n    \"ruff>=0.6\",\n    \"pytest>=8.0\",\n    \"pytest-django>=4.8\",\n    \"coverage>=7.6\",\n]\n\n[tool.ruff]\nline-length = 120\ntarget-version = \"py312\"\n\n[tool.pytest.ini_options]\nDJANGO_SETTINGS_MODULE = \"config.settings.development\"\npython_files = [\"tests.py\", \"test_*.py\"]\n"
  },
  {
    "path": "railway.toml",
    "content": "[build]\nbuilder = \"RAILPACK\"\nbuildCommand = \"python manage.py collectstatic --noinput\"\n\n[deploy]\npreDeployCommand = \"python manage.py migrate --noinput\"\nstartCommand = \"gunicorn config.wsgi --bind 0.0.0.0:$PORT\"\nhealthcheckPath = \"/health/\"\nhealthcheckTimeout = 300\nrestartPolicyType = \"ON_FAILURE\"\nrestartPolicyMaxRetries = 10\n"
  },
  {
    "path": "readme.md",
    "content": "<p align=\"center\">\n  <img src=\"config/static/img/django-pony.png\" alt=\"Django Pony\" width=\"80\">\n</p>\n\n<h1 align=\"center\">Django Starter Template</h1>\n\nA production-ready Django 5.2 LTS starter template for [Railway](https://railway.com).\n\n[![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)\n\n## Deploy to Railway\n\nClick the button above to deploy this template. Railway will:\n\n1. Create a new Django web service\n2. Provision a PostgreSQL database\n3. Set `DATABASE_URL` and `SECRET_KEY` automatically\n4. Run migrations on deploy\n5. Start the application with gunicorn\n\nYour app will be live in under a minute.\n\n### Environment variables\n\nThese are set automatically by Railway. Override them in your service settings if needed:\n\n| Variable | Description | Default |\n|----------|-------------|---------|\n| `SECRET_KEY` | Django secret key | Auto-generated |\n| `DATABASE_URL` | PostgreSQL connection string | Provided by Railway |\n| `ALLOWED_HOSTS` | Comma-separated hostnames | `.railway.app` |\n| `CSRF_TRUSTED_ORIGINS` | Full URLs for POST requests (e.g. `https://myapp.up.railway.app`) | `[]` |\n| `DJANGO_SETTINGS_MODULE` | Settings module | `config.settings.production` |\n\n**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.\n\n## Local Development\n\n### Option A: uv (recommended)\n\nPrerequisites: [Python 3.12+](https://python.org), [uv](https://docs.astral.sh/uv/getting-started/installation/)\n\n```bash\ngit clone https://github.com/fasouto/django-starter-template.git\ncd django-starter-template\n\n# Install dependencies\nuv sync --dev\n\n# Set up environment\ncp .env.example .env\n\n# Run migrations and create admin user\nuv run python manage.py migrate\nuv run python manage.py createsuperuser\n\n# Start development server\nuv run python manage.py runserver\n```\n\nOpen [http://localhost:8000](http://localhost:8000). The admin panel is at [http://localhost:8000/admin/](http://localhost:8000/admin/).\n\n```bash\n# Run tests\nuv run pytest\n\n# Lint and format\nuv run ruff check .\nuv run ruff format .\n```\n\n### Option B: Docker Compose\n\nPrerequisites: [Docker](https://docs.docker.com/get-docker/)\n\n```bash\ngit clone https://github.com/fasouto/django-starter-template.git\ncd django-starter-template\n\ncp .env.example .env\n\n# Start Django + PostgreSQL\ndocker compose up\n\n# In another terminal:\ndocker compose exec web uv run python manage.py migrate\ndocker compose exec web uv run python manage.py createsuperuser\n```\n\nOpen [http://localhost:8000](http://localhost:8000). Code changes reload automatically.\n\n## Project Structure\n\n```\n.\n├── apps/\n│   └── base/                # Default app (home page, health check, tests)\n│       ├── templates/base/  # App templates\n│       ├── tests.py         # Example tests\n│       ├── urls.py\n│       └── views.py\n├── config/                  # Django project package\n│   ├── settings/\n│   │   ├── base.py          # Shared settings\n│   │   ├── development.py   # Dev settings (DEBUG=True, SQLite)\n│   │   └── production.py    # Production settings (Postgres, security)\n│   ├── static/              # Project-level static files\n│   │   └── css/base.css\n│   ├── templates/           # Project-level templates (base.html, error pages)\n│   ├── asgi.py\n│   ├── urls.py\n│   └── wsgi.py\n├── docker-compose.yml       # Local dev with Docker (Django + Postgres)\n├── Dockerfile.dev           # Dev container\n├── pyproject.toml           # Dependencies and tool config\n├── railway.toml             # Railway deployment config\n├── uv.lock                  # Locked dependencies\n└── manage.py\n```\n\n## What's Included\n\n- **[Django 5.2 LTS](https://docs.djangoproject.com/en/5.2/)**: supported until April 2028\n- **[PostgreSQL](https://www.postgresql.org/)** via psycopg3, modern async-capable adapter\n- **[WhiteNoise](https://whitenoise.readthedocs.io/)**: serve static files without nginx, with brotli compression\n- **[django-environ](https://django-environ.readthedocs.io/)**: configure via environment variables and `.env` files\n- **[Argon2](https://docs.djangoproject.com/en/5.2/topics/auth/passwords/#using-argon2-with-django)** password hashing (winner of the Password Hashing Competition)\n- **Split settings** for separate development and production configurations\n- **Health check** at `/health/`, returns JSON for Railway monitoring\n- **[django-debug-toolbar](https://django-debug-toolbar.readthedocs.io/)**: SQL queries, templates, cache inspection (dev only)\n- **[ruff](https://docs.astral.sh/ruff/)** for linting and formatting\n- **[pytest](https://docs.pytest.org/) + [pytest-django](https://pytest-django.readthedocs.io/)** for testing\n\n## Customization\n\n### Adding a new app\n\n```bash\nmkdir apps/myapp\nuv run python manage.py startapp myapp apps/myapp\n```\n\nThen add `\"myapp\"` to `INSTALLED_APPS` in `config/settings/base.py`.\n\n### Replacing the CSS\n\nThe included `config/static/css/base.css` is minimal and framework-free. Replace it with Bootstrap, Tailwind, or any CSS framework you prefer.\n\n### Adding Celery\n\nAdd `celery[redis]` to your dependencies, create `config/celery.py`, and add a Redis service to your Railway project or `docker-compose.yml`.\n\n## License\n\nMIT. See [LICENSE](LICENSE).\n"
  }
]