Как я перестал тащить одинаковый код из проекта в проект и собрал 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-файлов, а с написания реальных фич - ровно с того места, где обычно хотелось оказаться уже на второй день.



