# Shipyard : Comment créer un boilerplate Django SaaS prêt pour la production sans la configuration fastidieuse
Démarrer un nouveau projet Django pour un produit SaaS prend généralement 2 à 3 semaines rien que pour configurer les composants de base. Shipyard résout cela en fournissant un boilerplate prêt pour la production avec des modules préconfigurés. Nous décomposons l'architecture et les solutions techniques qui permettent d'économiser du temps de développement.
Pile technologique et structure du projet
Shipyard est construit sur une pile moderne optimisée pour les charges de production. Composants clés :
- Django 5 + DRF 3.15 : approche API-first sans templates HTML
- PostgreSQL 16 + Redis 7 : conteneurs séparés pour la base de données et le cache
- Celery 5 + Beat + Flower : tâches asynchrones et surveillance
- Docker Compose : deux configurations — une pour le développement et une pour la production
- Stripe Webhooks : gestion des événements avec idempotence
- Multilocataires avec RBAC : chaîne User → TeamMembership → Team
La structure du projet est strictement modulaire :
shipyard/
├── apps/
│ ├── core/
│ ├── users/
│ ├── teams/
│ ├── billing/
│ ├── notifications/
│ └── api/
├── config/
├── docker/
└── docker-compose.yml
Chaque app gère sa propre zone de responsabilité. Par exemple, billing contient les modèles d'abonnement, tandis que api gère l'infrastructure DRF sans logique métier.
Architecture des modèles de base
L'élément clé est les classes abstraites dans 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
Ces mixins sont utilisés dans tous les modèles. Les UUID au lieu des clés primaires entières ont été choisis délibérément : les ID prévisibles dans les URL sont insecure, et migrer d'entier vers UUID dans une base de données en production est douloureux. TimestampedModel fournit un audit des changements sans duplication de code.
L'app users implémente un modèle User personnalisé avec l'email comme identifiant principal :
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"]
Le champ stripe_customer_id est ajouté au modèle user pour les cas où la facturation est liée à un individu. La vérification et les tokens de réinitialisation de mot de passe sont gérés séparément via les modèles EmailVerificationToken et PasswordResetToken — pas de cache, juste la base de données pour l'audit.
Multilocataires via RBAC
Shipyard adopte une approche pragmatique pour le multilocataires. Au lieu de la séparation de schémas (django-tenants) ou d'un tenant_id global, il utilise une chaîne de propriété :
User → TeamMembership → Team
TeamMembership agit comme une table de liaison avec un champ role. Team stocke les limites (max_members, max_projects), dénormalisées depuis Plan pour des vérifications rapides sans JOIN. Cela réduit la complexité des requêtes lors du travail avec les limites.
Trois rôles sont implémentés via les permissions 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()
Le pattern est mécanique : tous les ViewSets filtrent le queryset par team_id depuis l'URL. Cela réduit le risque de fuites de données entre locataires par rapport à l'ajout manuel de tenant_id à chaque requête.
Système de facturation et webhooks
Le modèle Plan reflète 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()
Les limites dénormalisées dans Plan permettent des vérifications de quotas instantanées sans interroger Stripe. Lors de la création d'une équipe, un signal post_save provisionne le client Stripe — l'équipe est prête pour le paiement immédiatement.
La gestion des webhooks est basée sur l'idempotence. Le modèle WebhookEvent suit les événements traités :
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)
Cela empêche le traitement en double des événements lors des retries de Stripe.
Points forts clés
- UUID au lieu de PK entier — décision prise une fois pour toutes ; la migration en base live est impossible
- Vérifications de santé centralisées — endpoints /health/ et /ready/ pour l'intégration avec Docker et Kubernetes
- Infrastructure DRF dans une app séparée — versionnage, pagination et gestion d'erreurs séparés de la logique métier
- Audit des emails via EmailLog — journalisation de chaque email envoyé pour le support
- Deux configurations Docker Compose — config dev avec volumes, prod avec builds multi-étapes
L'intégration Stripe nécessite une attention particulière à l'idempotence. Chaque événement webhook est vérifié pour unicité via event_id. C'est critique pour les opérations comme les charges, où un reprocessing mènerait à une facturation double. Shipyard utilise le modèle WebhookEvent comme mécanisme de verrouillage naturel.
Les notifications disposent d'un système de templating avec un layout de base. Tous les emails (bienvenue, verify_email, invitation) héritent d'une structure commune pour une maintenance facile. Les templates sont stockés dans apps/notifications/templates/notifications/ avec des versions HTML et texte séparées.
La configuration Docker Compose de production utilise des builds multi-étapes. La première étape installe les dépendances, la seconde copie le code. Cela réduit la taille de l'image finale et accélère le déploiement. Pour CI/CD, trois workflows GitHub Actions sont configurés : tests lint sur PR, builds d'images sur merge vers main, et déploiement sur tags de release.
— Editorial Team
Aucun commentaire pour le moment.