BaseApp Backend is a monorepo of reusable Django packages (auth, profiles, comments, reactions, follows, blocks, chats, notifications, payments, etc.). It is consumed as a git submodule by project templates.
Core stack: Django + DRF + Graphene Django (GraphQL) + Celery + PostgreSQL + Redis.
Dual API surface: Some features expose both a REST API (DRF) and a GraphQL API (Graphene Django with Relay). When adding features, default to GraphQL API, unless scoped otherwise.
Making ObjectTypes overridable: Models should expose get_graphql_object_type() so consuming projects can swap the ObjectType. Use get_object_type_for_model() throughout package code instead of importing the ObjectType directly.
Model swapping: When adding a concrete model to a package, use django-swappable-models so consuming projects can substitute their own model.
- Type-annotate all function parameters and return values.
- Add docstrings to non-trivial functions.
- All datetimes must be UTC; serialize as ISO 8601.
- Prefer queryset annotations over
@property— annotations are filterable and avoid extra DB hits. - If an annotation is reused, add it to a custom
QuerySetmethod on a customManager. related_nameonForeignKey/ManyToManyField→ plural; onOneToOneField→ singular.- Use
gettext_lazy as _for all user-facing strings (verbose_name,help_text, form labels, error messages). Never use eagergettextat module level.
- Put permissions in
permissions.py; compose with|and&rather than bundling logic into one class. - Validate request data with a
Serializer, not inline in the view. - Use
FilterSetclasses rather than manually parsingrequest.GET. ValidationErrormessages must be wrapped in an array; usenon_field_errorsfor non-field errors.- Catch exceptions, log them, and return a generic error — never expose exception details to the client.
- Serialize choice fields by returning the key, not the display value.
- Models inherit
RelayModelfrombaseapp_core.graphql. - ObjectTypes use
baseapp_core.graphql.DjangoObjectTypeand implementget_graphql_object_type(). - Mutations inherit
RelayMutation(providesclientMutationId,errors,_debug). - Always permission-check in
get_nodeto prevent unauthorized access vianode(id: ...). - Put permissions in
permissions.pyand make use of Django's permissions system, which can also be shared with DRF as well. - Consider
graphene-django-query-optimizerfor complex cases or optimize withselect_related/prefetch_relatedinget_queryset; - Use
DjangoFilterConnectionField+FilterSetfor filterable list queries.filters.pycan be shared with DRF as well. - Document queries and mutations with docstrings and
description=/deprecated_reason=fields.
Formatting: Black with line-length=100, isort with Black profile.
Each package follows this layout:
baseapp_<package>/tests/
├── integration/ # DB-dependent tests
├── unit/ # pure logic, no I/O
├── factories.py # factory-boy factories
├── fixtures.py # pytest fixtures
└── helpers.py # shared test utilities
Run these from this submodule's root (baseapp-backend/) using its own docker-compose.yml.
Do not use a consuming template's container: it runs the template's settings.* (not
testproject.settings) and its pytest config ignores baseapp-backend, so package tests won't run
there. Paths below are relative to the submodule root.
Tests must go through uv run. The image is built with uv sync --no-install-project, so the
baseapp_backend distribution is not installed and the baseapp.plugins entry points are not
registered — bare pytest sees 1 plugin instead of ~25 and fails in confusing ways. uv run
installs the project first (CI does the same). The explicit --python 3.12 is needed because
.python-version still pins 3.11 while pyproject.toml requires >=3.12.
# All tests
docker compose run --rm web uv run --python 3.12 pytest
# Specific package
docker compose run --rm web uv run --python 3.12 pytest baseapp_profiles/tests/
# Reuse the test DB across runs (skips the slow migrate/setup step)
docker compose run --rm web uv run --python 3.12 pytest baseapp_profiles/tests/ --reuse-db
# With coverage
docker compose run --rm web uv run --python 3.12 pytest --cov --cov-report=term-missing
# Code quality
docker compose run --rm web black .
docker compose run --rm web isort .
docker compose run --rm web flake8
docker compose run --rm web uv run ast-grep scan # code-guideline rules (.ast-grep/rules/)
docker compose run --rm web uv run ast-grep test # test the ast-grep rules themselves (CI runs both)
docker compose run --rm web pre-commit run --all-files- Minimum coverage: 75%
- A task is only complete when tests are written, all pass, and coverage is ≥ 75%.
- Use the
compose-a-blockskill when creating or extending a package in this repo, and for any GraphQL query-performance work (N+1s,pre_optimization_hook, annotations, connection fields). - Use the
ensure-test-coverageskill whenever implementing or modifying backend code. - Use the
run-development-commandsskill to translate intent into the correct Docker Compose commands. - Do not mark a task done until tests pass and coverage meets the threshold.