Shipyard: Cómo crear un boilerplate de Django SaaS listo para producción sin la tediosa configuración inicial
Empezar un nuevo proyecto de Django para un producto SaaS suele consumir de 2 a 3 semanas solo en configurar los componentes básicos. Shipyard resuelve esto proporcionando un boilerplate listo para producción con módulos preconfigurados. Desglosamos la arquitectura y las soluciones técnicas que ahorran tiempo de desarrollo.
Pila tecnológica y estructura del proyecto
Shipyard está construido sobre una pila moderna optimizada para cargas de trabajo en producción. Componentes clave:
- Django 5 + DRF 3.15: Enfoque API-first sin plantillas HTML
- PostgreSQL 16 + Redis 7: Contenedores separados para la base de datos y el caché
- Celery 5 + Beat + Flower: Tareas asíncronas y monitoreo
- Docker Compose: Dos configuraciones —una para desarrollo y otra para producción
- Stripe Webhooks: Manejo de eventos con idempotencia
- Multitenancia con RBAC: Cadena de User → TeamMembership → Team
La estructura del proyecto es estrictamente modular:
shipyard/
├── apps/
│ ├── core/
│ ├── users/
│ ├── teams/
│ ├── billing/
│ ├── notifications/
│ └── api/
├── config/
├── docker/
└── docker-compose.yml
Cada app maneja su propia área de responsabilidad. Por ejemplo, billing contiene los modelos de suscripciones, mientras que api maneja la infraestructura de DRF sin lógica de negocio.
Arquitectura de los modelos base
El elemento clave son las clases abstractas en 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
Estos mixins se usan en todos los modelos. Se eligieron UUID en lugar de claves primarias enteras de forma deliberada: los ID predecibles en las URL son inseguros y migrar de enteros a UUID en una base de datos en producción es doloroso. TimestampedModel proporciona auditoría de cambios sin duplicar código.
La app users implementa un modelo User personalizado con el email como identificador 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"]
El campo stripe_customer_id se añade al modelo de usuario para casos en que la facturación está ligada a un individuo. La verificación y los tokens de restablecimiento de contraseña se manejan por separado mediante los modelos EmailVerificationToken y PasswordResetToken —sin caché, solo base de datos para auditoría.
Multitenancia mediante RBAC
Shipyard usa un enfoque pragmático para la multitenancia. En lugar de separación de esquemas (django-tenants) o un tenant_id global, emplea una cadena de propiedad:
User → TeamMembership → Team
TeamMembership actúa como tabla intermedia con un campo role. Team almacena límites (max_members, max_projects), desnormalizados desde Plan para verificaciones rápidas sin JOINs. Esto reduce la complejidad de las consultas al trabajar con límites.
Se implementan tres roles mediante permisos de 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()
El patrón es mecánico: todos los ViewSets filtran el queryset por team_id de la URL. Esto reduce el riesgo de fugas de datos entre inquilinos en comparación con añadir tenant_id manualmente a cada consulta.
Sistema de facturación y webhooks
El modelo Plan refleja 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()
Los límites desnormalizados en Plan permiten verificaciones instantáneas de cuotas sin consultar Stripe. Al crear un equipo, una señal post_save provisiona el cliente de Stripe —el equipo está listo para pagos de inmediato.
El manejo de webhooks se basa en idempotencia. El modelo WebhookEvent rastrea los eventos procesados:
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)
Esto evita el procesamiento duplicado de eventos en reintentos de Stripe.
Aspectos destacados
- UUID en lugar de PK entero —decisión tomada de una vez por todas; la migración en una BD en producción es imposible
- Comprobaciones de salud centralizadas —endpoints /health/ y /ready/ para integración con Docker y Kubernetes
- Infraestructura de DRF en una app separada —versionado, paginación y manejo de errores separados de la lógica de negocio
- Auditoría de emails mediante EmailLog —registro de todos los emails enviados para soporte
- Dos configuraciones de Docker Compose —configuración de desarrollo con volúmenes, producción con builds multi-etapa
La integración con Stripe requiere atención especial a la idempotencia. Cada evento de webhook se verifica por unicidad mediante event_id. Esto es crítico para operaciones como cargos, donde reprocesar llevaría a facturación doble. Shipyard usa el modelo WebhookEvent como mecanismo de bloqueo natural.
Las notificaciones cuentan con un sistema de plantillas con un diseño base. Todos los emails (bienvenida, verify_email, invitación) heredan una estructura común para un mantenimiento fácil. Las plantillas se almacenan en apps/notifications/templates/notifications/ con versiones HTML y texto separadas.
La configuración de Docker Compose para producción usa builds multi-etapa. La primera etapa instala dependencias, la segunda copia el código. Esto reduce el tamaño de la imagen final y acelera el despliegue. Para CI/CD, hay tres workflows de GitHub Actions: pruebas de lint en PRs, builds de imagen en merge a main y despliegue en tags de release.
— Editorial Team
Aún no hay comentarios.