Zpět na domů

Django SaaS boilerplate Shipyard: production-šablona bez rutiny

Analýza Shipyard — production-ready Django boilerplate pro SaaS-aplikace. Rozbor architektury multitenance, integrace Stripe a technických řešení, které ušetří 2 týdny vývoje. Příklady kódu a best practices.

Shipyard: Připravený k production Django-šablona pro SaaS
Advertisement 728x90

# Shipyard: Jak vytvořit šablonu Django SaaS připravenou pro produkci bez rutiny

Spouštění nového Django projektu pro SaaS produkt obvykle spotřebuje 2–3 týdny na nastavení základních komponent. Shipyard tento problém řeší tím, že poskytuje šablonu připravenou pro produkci s přednastavenými moduly. Probereme architekturu a technická řešení, která šetří čas vývoje.

Stak a struktura projektu

Shipyard je postavený na moderním staku zaměřeném na produkční zátěž. Hlavní komponenty:

  • Django 5 + DRF 3.15: Přístup API-first bez HTML šablon
  • PostgreSQL 16 + Redis 7: Samostatné kontejnery pro databázi a cache
  • Celery 5 + Beat + Flower: Asynchronní úkoly a monitorování
  • Docker Compose: Dva konfigy – pro vývoj a produkci
  • Stripe Webhooks: Zpracování událostí s idempotencí
  • Multi-tenancy s RBAC: Řetězec User → TeamMembership → Team

Struktura projektu je striktně modulární:

Google AdInline article slot
shipyard/
├── apps/
│   ├── core/
│   ├── users/
│   ├── teams/
│   ├── billing/
│   ├── notifications/
│   └── api/
├── config/
├── docker/
└── docker-compose.yml

Každá aplikace odpovídá za svou oblast odpovědnosti. Například billing obsahuje modely předplatného, zatímco api infrastrukturu DRF bez business logiky.

Architektura základních modelů

Klíčový prvek tvoří abstraktní třídy v 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

Tyto mixiny se používají ve všech modelech. UUID místo integer PK bylo zvoleno záměrně: předvídatelné ID v URL nejsou bezpečné a migrace z integer na UUID v provozní databázi je bolestivá. TimestampedModel zajišťuje audit změn bez duplikování kódu.

Google AdInline article slot

V users je implementována vlastní model User s e-mailem jako hlavním identifikátorem:

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 je přidáno na model uživatele pro případy, kdy je billing vázaný na fyzickou osobu. Samostatně jsou implementovány tokeny pro ověření e-mailu a reset hesla přes modely EmailVerificationToken a PasswordResetToken – bez cache, pouze databáze pro audit.

Multi-tenancy přes RBAC

Shipyard používá kompromisní přístup k multi-tenancy. Místo rozdělení schémat (django-tenants) nebo globálního tenant_id se aplikuje řetězec vlastnictví:

Google AdInline article slot
User → TeamMembership → Team

TeamMembership slouží jako through-tabuka s polem role. Na Team se ukládají limity (max_members, max_projects), denormalizované z Plan pro rychlou kontrolu bez JOIN. To snižuje složitost dotazů při práci s limity.

Tři role jsou implementovány přes DRF permissions:

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()

Pattern je mechanický: všechny ViewSet filtrují queryset podle team_id z URL. To snižuje riziko úniku dat mezi tenanty ve srovnání s ručním přidáváním tenant_id do každého dotazu.

Systém billingu a webhooků

Model Plan zrcadlí 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()

Denormalizované limity v Plan umožňují okamžitou kontrolu kvót bez volání Stripe. Při vytváření týmu se spustí signál post_save, který provizuje Stripe customer – tým je ihned připravený k platbě.

Zpracování webhooků je postavené na idempotenci. Model WebhookEvent zaznamenává zpracované události:

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 zabraňuje dvojitému zpracování událostí při opakovaných požadavcích ze Stripe.

Co je důležité

  • UUID místo integer PK – volba je udělaná jednou provždy, migrace v provozní databázi není možná
  • Centralizovaný health check – endpointy /health/ a /ready/ pro integraci s Docker a Kubernetes
  • DRF infrastruktura v samostatné app – verziování, paginace a zpracování chyb jsou vynechány z business logiky
  • Audit e-mailů přes EmailLog – zaznamenání každého odeslaného e-mailu pro podporu
  • Dva Docker Compose – dev konfig s volume, prod s multi-stage buildem

Integrace se Stripe vyžaduje zvláštní pozornost k idempotenci. Každá událost webhooku se kontroluje na unikátnost přes event_id. To je kritické pro operace jako inkaso plateb, kde opakované zpracování vede k dvojitému inkasu. Shipyard používá model WebhookEvent jako přirozený mechanismus blokování.

Pro notifikace je implementován šablonátor s základním layoutem. Všechny e-maily (welcome, verify_email, invitation) dědí společnou strukturu, což usnadňuje údržbu. Šablony jsou uložené v apps/notifications/templates/notifications/ s oddělením HTML a textových verzí.

V prod konfigu Docker Compose se používá multi-stage build. První stage – instalace závislostí, druhý – kopírování kódu. To snižuje velikost finálního image a zrychluje deploy. Pro CI/CD jsou nastaveny tři GitHub Actions workflow: lint testy při PR, build image při mergi do main, deploy podle release tagu.

— Editorial Team

Advertisement 728x90

Číst dál