Shipyard: So erstellen Sie ein produktionsreifes Django-SaaS-Boilerplate ohne lästige Einrichtung
Das Starten eines neuen Django-Projekts für ein SaaS-Produkt kostet in der Regel 2–3 Wochen allein für die Einrichtung der Basis-Komponenten. Shipyard löst das, indem es ein produktionsreifes Boilerplate mit vorkonfigurierten Modulen liefert. Wir zerlegen die Architektur und technischen Lösungen, die Entwicklungszeit einsparen.
Stack und Projektstruktur
Shipyard basiert auf einem modernen Stack, optimiert für Produktionslasten. Wichtige Komponenten:
- Django 5 + DRF 3.15: API-first-Ansatz ohne HTML-Vorlagen
- PostgreSQL 16 + Redis 7: Separate Container für Datenbank und Cache
- Celery 5 + Beat + Flower: Asynchrone Tasks und Überwachung
- Docker Compose: Zwei Konfigurationen – eine für Entwicklung und eine für Produktion
- Stripe Webhooks: Event-Behandlung mit Idempotenz
- Multi-Tenancy mit RBAC: Kette User → TeamMembership → Team
Die Projektstruktur ist streng modular:
shipyard/
├── apps/
│ ├── core/
│ ├── users/
│ ├── teams/
│ ├── billing/
│ ├── notifications/
│ └── api/
├── config/
├── docker/
└── docker-compose.yml
Jede App übernimmt ihren eigenen Verantwortungsbereich. Zum Beispiel enthält billing Abonnement-Modelle, während api die DRF-Infrastruktur ohne Geschäftslogik handhabt.
Basis-Modelle-Architektur
Das zentrale Element sind die abstrakten Klassen in 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
Diese Mixins werden in allen Modellen verwendet. UUIDs statt ganzzahliger Primärschlüssel wurden bewusst gewählt: Vorhersehbare IDs in URLs sind unsicher, und die Migration von Integer zu UUID in einer Live-Datenbank ist schmerzhaft. TimestampedModel sorgt für Änderungsprotokollierung ohne Code-Duplikation.
Die users-App implementiert ein benutzerdefiniertes User-Modell mit E-Mail als primärem Identifier:
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"]
Das Feld stripe_customer_id wird zum User-Modell hinzugefügt für Fälle, in denen die Abrechnung an eine Einzelperson gebunden ist. Verifizierung und Passwort-Reset-Token werden separat über EmailVerificationToken- und PasswordResetToken-Modelle gehandhabt – kein Cache, nur Datenbank für Protokollierung.
Multi-Tenancy über RBAC
Shipyard verfolgt einen pragmatischen Ansatz für Multi-Tenancy. Statt Schema-Trennung (django-tenants) oder einem globalen tenant_id setzt es auf eine Eigentumskette:
User → TeamMembership → Team
TeamMembership dient als Durchgangstabelle mit einem role-Feld. Team speichert Limits (max_members, max_projects), denormalisiert aus Plan für schnelle Prüfungen ohne JOINs. Das reduziert die Abfragekomplexität bei der Arbeit mit Limits.
Drei Rollen werden über DRF-Permissions umgesetzt:
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()
Das Muster ist mechanisch: Alle ViewSets filtern das Queryset nach team_id aus der URL. Das minimiert das Risiko von Datenlecks zwischen Tenants im Vergleich zum manuellen Hinzufügen von tenant_id zu jeder Abfrage.
Abrechnungssystem und Webhooks
Das Plan-Modell spiegelt Stripe Product + Prices wider:
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()
Denormalisierte Limits in Plan ermöglichen sofortige Quotenprüfungen ohne Abfrage bei Stripe. Beim Erstellen eines Teams versorgt ein post_save-Signal den Stripe-Kunden – das Team ist sofort zahlungsbereit.
Die Webhook-Behandlung basiert auf Idempotenz. Das WebhookEvent-Modell protokolliert verarbeitete Events:
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)
Das verhindert doppelte Event-Verarbeitung bei Retries von Stripe.
Wichtige Highlights
- UUID statt Integer-PK — Entscheidung einmal und für immer; Migration in Live-DB unmöglich
- Zentralisierte Health-Checks — Endpoints /health/ und /ready/ für Integration mit Docker und Kubernetes
- DRF-Infrastruktur in separater App — Versionierung, Pagination und Error-Handling getrennt von Geschäftslogik
- E-Mail-Auditing über EmailLog — Protokollierung jeder gesendeten E-Mail für den Support
- Zwei Docker-Compose-Setups — Dev-Konfig mit Volumes, Prod mit Multi-Stage-Builds
Die Stripe-Integration erfordert besondere Aufmerksamkeit hinsichtlich Idempotenz. Jedes Webhook-Event wird auf Eindeutigkeit via event_id geprüft. Das ist entscheidend für Operationen wie Charges, bei denen eine Wiederverarbeitung zu Doppelabrechnung führen würde. Shipyard nutzt das WebhookEvent-Modell als natürlichen Locking-Mechanismus.
Notifications bieten ein Templating-System mit Basis-Layout. Alle E-Mails (welcome, verify_email, invitation) erben eine gemeinsame Struktur für einfache Wartung. Templates werden in apps/notifications/templates/notifications/ mit separaten HTML- und Text-Versionen gespeichert.
Die Produktions-Docker-Compose-Konfig verwendet Multi-Stage-Builds. Die erste Stage installiert Abhängigkeiten, die zweite kopiert den Code. Das verkleinert die finale Image-Größe und beschleunigt das Deployment. Für CI/CD sind drei GitHub-Actions-Workflows eingerichtet: Lint-Tests bei PRs, Image-Builds bei Merge zu main und Deployment bei Release-Tags.
— Editorial Team
Noch keine Kommentare.