# Shipyard: Jak stworzyć gotowy do produkcji szablon Django SaaS bez rutyny
Rozpoczęcie nowego projektu Django dla produktu SaaS zazwyczaj pochłania 2-3 tygodnie na konfigurację podstawowych komponentów. Shipyard rozwiązuje ten problem, oferując gotowy do produkcji szablon z prekonfigurowanymi modułami. Omawiamy architekturę i rozwiązania techniczne, które oszczędzają czas development.
Stos technologiczny i struktura projektu
Shipyard jest zbudowany na nowoczesnym stosie technologicznym, zorientowanym na obciążenia produkcyjne. Główne komponenty:
- Django 5 + DRF 3.15: Podejście API-first bez szablonów HTML
- PostgreSQL 16 + Redis 7: Oddzielne kontenery dla bazy danych i pamięci podręcznej
- Celery 5 + Beat + Flower: Asynchroniczne zadania i monitorowanie
- Docker Compose: Dwa pliki konfiguracyjne — dla rozwoju i produkcji
- Stripe Webhooks: Obsługa zdarzeń z idempotentnością
- Wielodostępność z RBAC: Łańcuch User → TeamMembership → Team
Struktura projektu jest ściśle modułowa:
shipyard/
├── apps/
│ ├── core/
│ ├── users/
│ ├── teams/
│ ├── billing/
│ ├── notifications/
│ └── api/
├── config/
├── docker/
└── docker-compose.yml
Każda aplikacja odpowiada za swoją strefę odpowiedzialności. Na przykład, billing zawiera modele subskrypcji, a api — infrastrukturę DRF bez logiki biznesowej.
Architektura podstawowych modeli
Kluczowym elementem są klasy abstrakcyjne w core/models.py:
python
# apps/core/models.py
import uuid
from django.db import models
class TimestampedModel(models.Model):
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
abstract = True
class UUIDModel(models.Model):
id = models.UUIDField(
primary_key=True,
default=uuid.uuid4,
editable=False,
)
class Meta:
abstract = True
Te miksiny są używane we wszystkich modelach. UUID zamiast klucza głównego typu integer wybrano świadomie: przewidywalne ID w URL są nebezpechne, a migracja z integer na UUID w działającej bazie danych jest bolesna. TimestampedModel zapewnia audyt zmian bez duplikowania kodu.
W users zaimplementowano niestandardowy model User z emailem jako głównym identyfikatorem:
python
# apps/users/models.py
class User(UUIDModel, AbstractBaseUser, PermissionsMixin):
email = models.EmailField(unique=True)
full_name = models.CharField(max_length=255, blank=True)
is_email_verified = models.BooleanField(default=False)
stripe_customer_id = models.CharField(max_length=255, blank=True, null=True)
USERNAME_FIELD = "email"
REQUIRED_FIELDS = ["full_name"]
Pole stripe_customer_id dodano do modelu użytkownika w przypadkach, gdy rozliczenia są powiązane z osobą fizyczną. Osobno zaimplementowano tokeny weryfikacji i resetu hasła za pomocą modeli EmailVerificationToken i PasswordResetToken — bez pamięci podręcznej, tylko baza danych dla audytu.
Wielodostępność przez RBAC
Shipyard stosuje kompromisowe podejście do wielodostępności. Zamiast rozdzielania schematów (django-tenants) lub globalnego tenant_id stosowany jest łańcuch własności:
User → TeamMembership → Team
TeamMembership działa jako tabela through z polem role. W Team przechowywane są limity (max_members, max_projects), zdenormalizowane z Plan dla szybkiej weryfikacji bez JOIN. To zmniejsza złożoność zapytań podczas pracy z limitami.
Trzy role są zaimplementowane przez uprawnienia DRF:
python
# apps/teams/permissions.py
class IsTeamMember(BasePermission):
def has_permission(self, request, view):
team_id = view.kwargs.get('team_id')
return TeamMembership.objects.filter(
user=request.user,
team_id=team_id
).exists()
Wzór jest mechaniczny: wszystkie ViewSet filtrują queryset po team_id z URL. To zmniejsza ryzyko wycieku danych między tenantami w porównaniu z ręcznym dodawaniem tenant_id do każdego zapytania.
System rozliczeń i webhooków
Model Plan odzwierciedla Stripe Product + Prices:
python
class Plan(models.Model):
stripe_product_id = models.CharField(max_length=255)
stripe_price_id_monthly = models.CharField(max_length=255)
stripe_price_id_yearly = models.CharField(max_length=255)
max_members = models.PositiveIntegerField()
max_projects = models.PositiveIntegerField()
Zdenormalizowane limity w Plan pozwalają na natychmiastową weryfikację limitów bez odwoływania się do Stripe. Podczas tworzenia zespołu uruchamiany jest sygnał post_save, który prowizjonuje klienta Stripe — zespół jest od razu gotowy do płatności.
Obsługa webhooków opiera się na idempotentności. Model WebhookEvent rejestruje przetworzone zdarzenia:
python
class WebhookEvent(models.Model):
event_id = models.CharField(max_length=255, unique=True)
event_type = models.CharField(max_length=255)
payload = models.JSONField()
processed_at = models.DateTimeField(null=True)
To zapobiega podwójnemu przetwarzaniu zdarzeń przy powtarzanych żądaniach ze Stripe.
Co ważne
- UUID zamiast klucza głównego typu integer — wybór zrobiony raz na zawsze, migracja w działającej bazie danych jest niemożliwa
- Centralizowany health check — endpointy /health/ i /ready/ do integracji z Docker i Kubernetes
- Infrastruktura DRF w osobnej aplikacji — wersjonowanie, paginacja i obsługa błędów wyodrębnione z logiki biznesowej
- Audyt e-maili przez EmailLog — rejestrowanie każdego wysłanego e-maila dla wsparcia
- Dwa Docker Compose — konfiguracja dev z wolumenem, prod z wieloetapową kompilacją
Integracja ze Stripe wymaga szczególnej uwagi na idempotentność. Każde zdarzenie webhooka jest sprawdzane pod kątem unikalności przez event_id. To kluczowe dla operacji takich jak pobieranie środków, gdzie powtarzane przetwarzanie doprowadzi do podwójnego obciążenia. Shipyard używa modelu WebhookEvent jako naturalnego mechanizmu blokady.
Dla powiadomień zaimplementowano silnik szablonów z podstawowym układem. Wszystkie e-maile (welcome, verify_email, invitation) dziedziczą wspólną strukturę, co ułatwia utrzymanie. Szablony są przechowywane w apps/notifications/templates/notifications/ z podziałem na wersje HTML i tekstowe.
W konfiguracji produkcyjnej Docker Compose używana jest wieloetapowa kompilacja. Pierwszy etap to instalacja zależności, drugi — kopiowanie kodu. To zmniejsza rozmiar końcowego obrazu i przyspiesza wdrożenie. Dla CI/CD skonfigurowano trzy workflow GitHub Actions: testy lint przy PR, budowa obrazu przy mergu do main, wdrożenie po tagu release.
— Editorial Team
Brak komentarzy.