Imported from YOYO-DR/claude-code-hosted (
legacy/AGENTS.md). Install upstream withnpx skills add YOYO-DR/claude-code-hosted --skill legacy. Copyright stays with the author.
AGENTS.md
Repo identity
This is a starter template (cookiecutter-django backend + Vite/React frontend), not a finished app. The default project name is claude-code-hosted. Before adding real features on top of a fresh clone, read START.md — it documents the mandatory find-and-replace + manual renames (kebab / snake / Title variants of the project name, Docker volume names, Celery app name, domain in backend/.envs/.production/.django, DJANGO_SECRET_KEY, POSTGRES_PASSWORD, etc.). Skipping it leaves stale references everywhere (compose volume names, pyproject.toml name, celery app label, swagger title, etc.).
The planes/ and .serena/ directories are local-only scratch space and can be deleted.
Layout
Monorepo with two independent packages — there is no shared root build.
backend/— Django 6.0 + DRF 3.17 + Celery 5.6 + Redis 7.2 + pgbouncer + Postgres 18 (cookiecutter-django fork).backend/apps/— every business app lives here (users/,custom_auth/,contrib/sites/).backend/config/— settings (base.py/local.py/test.py/production.py), rooturls.py,api_router.py,celery_app.py,asgi.py/wsgi.py.backend/.envs/.local/and.envs/.production/— env files split by concern (.django,.postgres).backend/compose/{local,production}/— Dockerfiles + start scripts (one folder per service).
frontend/— React 19 + Vite 8 + Zustand 5, no router, no UI lib. Alias@/→frontend/src/.frontend/.envcarriesVITE_API_URL.- Root:
docker-compose.local.yml(dev),docker-compose.production.yml(prod w/ traefik),docker-compose.dokploy.yml(Dokploy variant). No rootdocker-compose.yml;docker composeis invoked against the file you want. backend/tests/is a one-off (themerge_production_dotenvs_in_dotenv.pytest). Per-app tests live atbackend/apps/<app>/tests/.
Tooling
- Python:
uv(already in lockfile).backend/.python-versionpins3.14andbackend/pyproject.tomlrequires==3.14.*— match exactly. - Task runner:
just(seebackend/justfile). The justfile setsCOMPOSE_FILE=docker-compose.local.ymlfor the whole shell, so plaindocker compose ...afterjustalso works. - Frontend package manager:
pnpm(lockfile ispnpm-lock.yaml). Do not runnpm install. - Linters / formatters:
ruff(lint+format),djlint(Django templates),pyproject-fmt,django-upgradeto 6.0. All run via pre-commit (backend/.pre-commit-config.yaml) and in CI (backend/.github/workflows/ci.yml).
Commands (run from backend/ unless noted)
# Backend (always through the justfile — it handles the compose file)
just build # docker compose build
just up # bring up django, postgres, pgbouncer, redis, celery*, flower, frontend
just down # stop stack
just logs celeryworker # follow logs
just manage +args # docker compose run --rm django python manage.py <args>
just pytest +args # docker compose run --rm django pytest <args>
# IMPORTANT: pgbouncer runs in pool_mode=transaction. These MUST bypass it:
just manage-direct-db createsuperuser
just manage-direct-db makemigrations
just manage-direct-db migrate
just pytest-direct-db # use for any test that needs real DDL / schema work
The -direct-db variants pin PGB_POSTGRES_HOST=postgres PGB_POSTGRES_PORT=5432 (the real backend, not the pgbouncer :6432 listener). Do not edit pyproject.toml settings to "fix" this — the bypass is the fix.
Bare uv run works only if you have created a venv and exported DJANGO_SETTINGS_MODULE=config.settings.local; almost all the time you want the docker path.
# Frontend (from frontend/)
pnpm install --frozen-lockfile
pnpm dev # vite dev server on :5173
pnpm build # production build → dist/
pnpm lint # eslint .
Things that will silently break if you miss them
- Celery app label is
config.celery_app(set inbackend/config/celery_app.pyviaCelery("claude_code_hosted")— the constructor string is the broker label and must be renamed alongside the project, otherwise Flower/log lines look wrong). Always invoke celery ascelery -A config.celery_app ...(start scripts inbackend/compose/.../celery/*/startalready do this). - Adding a new app:
cd backend && python manage.py startapp mi_app(ordocker compose run --rm django python manage.py startapp mi_app), put it underapps/, then add"apps.mi_app"toLOCAL_APPSinbackend/config/settings/base.py.manage.pyalready appendsapps/tosys.path, so imports inside app code arefrom apps.<app>.models import …and tests/factories follow the sameapps.<app>.tests.…style. - Test settings: pytest is pinned to
--ds=config.settings.test --reuse-db --import-mode=importlibinpyproject.toml[tool.pytest]. Do not pass--ds=on the CLI; it will conflict. Test file patterns aretests.pyandtest_*.py. - Coverage (
tool.coverage.run): onlyapps/**is included.config/,migrations/, and*/tests/*are excluded. Adding code underconfig/won't move the coverage needle — that's by design. - CORS scope:
CORS_URLS_REGEX = r"^/api/.*$"inbackend/config/settings/base.py— CORS headers are only attached to/api/*. Don't put non-API routes behind DRF and expect cookies to flow. - DRF defaults:
IsAuthenticatedis the default permission;JWTAuthenticationis the default auth class. Swagger UI at/api/docs/is admin-only (SERVE_PERMISSIONS=["rest_framework.permissions.IsAdminUser"]). - Auth model (
apps.users.models.User): email is the USERNAME_FIELD,username/first_name/last_nameare allNone. Tests useapps.users.tests.factories.UserFactoryand a shareduserfixture fromapps/conftest.py(autouse_media_storageredirectsMEDIA_ROOTto tmpdir). New models that need aUserFK should referencesettings.AUTH_USER_MODEL. - JWT flow: refresh token is set as HttpOnly cookie (
JWT_COOKIE_NAME = "refresh_token",Laxsamesite) byapps.custom_auth.api.viewsets.token.CustomTokenObtainPairView; access token is returned in the body and stored client-side in the ZustandauthStore(frontend/src/store/authStore.js). Auto-refresh lives infrontend/src/api/apiClient.js. Endpoints are centralized infrontend/src/api/apiEndpoints.js. - Frontend env:
VITE_API_URLis read at build time (Vite). In dev it comes fromfrontend/.env; in prod the production Dockerfile (frontend/compose/production/Dockerfile) takes it as aVITE_API_URLbuild arg — there is no runtime override. Traefik inbackend/compose/production/traefik/traefik.template.ymlroutes/api/*→ django,/api/media/*→ nginx, everything else → frontend, so the prodVITE_API_URLis/(relative). - Production server: gunicorn/uvicorn-worker binds
0.0.0.0:5000(not:8000). Traefik hitsdjango:5000. Healthcheck/port mappings indocker-compose.production.ymlreflect this. .envs/*are committed on purpose in this template (so a new dev cloning the template can see which vars exist). The very last step ofSTART.mdis to uncomment the.envs/*ignore inbackend/.gitignoreandgit rm --cached -r backend/.envs/.production/before going to production. Don't skip it;DJANGO_SECRET_KEY,POSTGRES_PASSWORD,RESEND_API_KEYmust not leak.merge_production_dotenvs_in_dotenv.py(backend/) is the build helper that concatenatesbackend/.envs/.production/.django+.postgresinto a singlebackend/.envfor the production compose (which only mountsenv_file: ./.env).
CI
backend/.github/workflows/ci.yml is the only workflow. It (1) runs pre-commit/action, (2) builds the django + postgres images with docker/bake-action and GHA cache, (3) runs makemigrations --check then migrate then pytest inside the local compose stack, (4) tears it down. Trigger on PR/push to main; docs/** is path-ignored. There is no separate frontend CI yet.
Docs
backend/docs/ is Sphinx (make livehtml runs in the docs service from backend/docker-compose.docs.yml on port 9000).