Django boilerplate для saas: как собрать production-ready шаблон и перестать копипастить код

Как я перестал тащить одинаковый код из проекта в проект и собрал Django-boilerplate для SaaS

Запуск нового SaaS на Django у меня долго выглядел одинаково: первые дни уходят не на продукт, а на обязательную "обвязку". Сначала - кастомный пользователь, потому что стандартный `auth.User` почти всегда становится тесным. Затем - UUID вместо integer PK: один раз оставишь автоинкремент, а потом обязательно упрёшься в миграции, интеграции и неочевидные ограничения при масштабировании. Следом подтягивается JWT-аутентификация: настройка SimpleJWT, ручки регистрации/логина/логаута, refresh-токены, проверки email. Всё это уже делалось десятки раз, но лежало в других репозиториях и "просто скопировать" каждый раз не получалось без правок.

Дальше начиналась инфраструктура: Docker Compose, где нужно аккуратно развести `web`, `db`, `redis`, `celery`, `celery-beat`, `flower`, подружить их переменными окружения и сетями, а затем ещё и объяснить команде, почему "оно не стартует на Windows" или "миграции не применились". Параллельно всплывали нюансы Celery: в новых версиях меняется синтаксис конфигов и привычные примеры устаревают быстрее, чем переписываются внутренние гайды.

Отдельной строкой всегда стоял биллинг. Подписки Stripe, checkout, customer portal, а главное - webhooks. Обработка событий без идемпотентности почти гарантирует двойные списания, повторную выдачу прав или "фантомные" инвойсы после ретраев со стороны Stripe. И, конечно, мультиарендность: команды, роли, права доступа на уровне API и объектов - ещё неделя-две, если делать аккуратно.

Проблема осложнялась тем, что проекты редко были "одинаковыми по стеку": где-то хотелось Kafka вместо Redis, где-то - allauth сразу, где-то биллинг был не на команду, а на пользователя. Но при всём этом ядро повторялось: в любом случае перед первой продуктовой фичей я стабильно тратил те же самые две недели на одни и те же решения.

В какой-то момент я решил прекратить это и собрал Shipyard - не папку со сниппетами и не набор "полезных кусочков", а цельный production-ready репозиторий, который можно клонировать и сразу начинать писать бизнес-логику.

Стек и почему он такой

Django 5 + Django REST Framework 3.15. Проект изначально API-first: без попыток "потом прикрутить API к HTML". DRF выбран как де-факто стандарт - с понятной документацией и предсказуемым входом для большинства разработчиков в команде.

PostgreSQL 16 + Redis 7. Postgres - основная БД: UUID, JSONB для метаданных (например, у тарифов), расширения вроде `pg_trgm` под будущий поиск. Redis используется как брокер для Celery и как кэш, причём без желания "складывать туда вообще всё". В Shipyard это отдельный контейнер, а не абстрактный "редис на любые нужды".

Celery 5 + Celery Beat + Flower. Асинхронщина закрывает отправку писем, фоновые синхронизации (включая Stripe), периодические задачи ведёт Beat. Для мониторинга - Flower на порту 5555. Расписание хранится в базе через `django-celery-beat`, а не в хардкоде `CELERYBEAT_SCHEDULE`, чтобы управлять этим без деплоя.

Docker Compose. Два конфига: `docker-compose.yml` под разработку и `docker-compose.prod.yml` под production. В dev - монтирование кода volume'ом, в prod - multi-stage сборка образа.

CI/CD через GitHub Actions. Три пайплайна:
- `ci.yml` - линтеры и тесты на каждый push/PR,
- `build.yml` - сборка и публикация Docker-образа при мерже в `main`,
- `deploy.yml` - деплой по release-тегу.

Stripe subscriptions + webhooks. Внутри есть модели `Plan`, `Subscription`, `Invoice`, `WebhookEvent`, поддержка checkout session и customer portal, а также обработка webhook-событий с защитой от дублей.

Мультиарендность и RBAC. База - связка `User → TeamMembership → Team`. Роли: `owner`, `admin`, `member`. Права применяются через DRF permissions как на уровне view, так и на уровне конкретного объекта.

Django Unfold для админки. Это не фундаментальная часть, но стандартный админ в современном продукте часто выглядит устаревшим, а Unfold даёт опрятный UI без кастомной вёрстки.

Как устроен проект

Структура собиралась так, чтобы приложения были самостоятельными и не превращались в "общую кашу":

- `core` - базовые модели, health-check, утилиты
- `users` - кастомный пользователь, JWT, подтверждение email
- `teams` - команда, участники, роли, инвайты
- `billing` - планы, подписки, инвойсы, вебхуки
- `notifications` - Celery-задачи для писем, `EmailLog`
- `api` - DRF router, versioning, throttling
- `config/settings` - раздельные настройки `base.py`, `development.py`, `production.py`
- `config/celery.py` - единая точка настройки Celery
- `docker/` - отдельные папки под dev/prod

В `core` живут два абстрактных миксина, которые используются почти везде:

- `TimestampedModel` с `created_at` и `updated_at`
- `UUIDModel` с UUID как primary key

UUID выбран сознательно. Предсказуемые автоинкрементные ID - это лишняя "информация наружу" (например, в публичных URL или логах), плюс они осложняют некоторые сценарии интеграций и миграций, когда проект начинает жить дольше пары месяцев.

Несколько решений, которые реально экономят время

1) Мультиарендность не "как получится", а как правило.
В Shipyard многопользовательская модель заложена сразу: принадлежность к команде и роль - не факультативная фича "на потом". Это резко снижает шанс, что через месяц придётся переписывать половину API под новый контекст доступа.

2) Webhooks Stripe с идемпотентностью.
События Stripe могут приходить повторно, с задержками и в разном порядке. Поэтому сохраняется `WebhookEvent`, и перед обработкой проверяется, не проходили ли мы уже этот event. Так подписка/инвойс не "создаётся дважды", а права доступа не "мигают".

3) Docker entrypoint с проверкой БД и условными миграциями.
Одна из самых раздражающих вещей - "контейнер поднялся, но приложение упало, потому что база ещё не готова" или "миграции забыли". В Shipyard entrypoint делает старт более предсказуемым: проверяет доступность БД и запускает миграции по условиям, а не в виде ручного ритуала.

4) Настройки по окружениям вместо одного settings.py.
`base/development/production` - это меньше риска случайно включить дебаг в проде, меньше хаоса с переменными окружения и понятнее, где именно живёт конкретная настройка.

Что я добавил сверху, чтобы boilerplate был ближе к реальному SaaS

Ниже - вещи, которые логично иметь в заготовке, если цель не "завести Django", а быстрее дойти до продукта:

Единый подход к версиям API. Даже если сейчас версия одна, привычка версионировать маршруты и сериализаторы заранее спасает от болезненного рефакторинга, когда мобильное приложение или фронтенд уже в проде.

Throttling и базовая защита API от злоупотреблений. В SaaS почти всегда появляются точки давления: логин, сброс пароля, регистрация, создание инвайтов. Лимиты на уровне DRF помогают не изобретать защиту заново.

Логирование фоновых задач и писем. Наличие `EmailLog` и понятных статусов задач сокращает время расследования, когда "письмо не дошло", а на самом деле оно не отправилось из-за временной ошибки провайдера.

Нормальная точка для health checks. Когда появляется Kubernetes/балансировщик/мониторинг, проверка "приложение живо + база доступна" должна быть стандартом, а не самописом в последний момент.

Модель приглашений в команду как отдельная сущность. Инвайты - это не просто "отправить письмо". Это токены, срок жизни, повторные отправки, отзыв приглашения, защита от повторного принятия.

Планирование задач через базу. Когда расписание хранится в БД, задачи можно менять без пересборки контейнера и без риска забыть обновить конфиг на одном из инстансов.

Минимальная дисциплина по модульности. Приложения разделены по доменам: users/teams/billing/notifications. Это помогает держать границы ответственности и не превращать проект в монолитный `utils.py` на 2000 строк.

Что сознательно не вошло

Boilerplate не пытается угадать все возможные варианты. Я не стал "вшивать навсегда" альтернативные брокеры, специфичные фронтенд-шаблоны или редкие интеграции, которые нужны не каждому проекту. Цель Shipyard - закрыть повторяемое ядро SaaS, а не превратиться в комбайн, где половина кода всегда лишняя.

Итог

Shipyard появился как попытка вырезать две недели рутины из каждого нового старта: типовой пользователь и JWT, команды и роли, Stripe-подписки и webhooks с идемпотентностью, Docker-окружение, Celery с расписанием, админка с нормальным UI, раздельные настройки и базовый CI/CD. В результате новый проект начинается не с копипаста и отладок compose-файлов, а с написания реальных фич - ровно с того места, где обычно хотелось оказаться уже на второй день.

Прокрутить вверх