
Migrating from Custom Django to SaaS Pegasus
Transform your handcrafted Django app into a production-ready SaaS with Pegasus's battle-tested patterns for auth, teams, billing, and deployment.
Prerequisites
- •Existing Django application
- •Understanding of Django models and views
- •Basic knowledge of Stripe or payment integration
Migrating from Custom Django to SaaS Pegasus
SaaS Pegasus is a Django boilerplate that provides a complete foundation for subscription-based SaaS applications. If you've been building Django from scratch and want to adopt proven patterns for auth, teams, and billing, this guide is for you.
Why Migrate to Pegasus?#
Migrate if:
- Your custom auth implementation is becoming a maintenance burden
- You need teams/multi-tenancy and don't want to build it
- Stripe subscription handling is complex and error-prone
- You want a standardized deployment pipeline
- You're spending too much time on infrastructure vs. features
Keep your custom setup if:
- Your authentication needs are truly unique
- You've already solved these problems well
- Pegasus's opinions conflict with your architecture
- You prefer complete control over all dependencies
What Pegasus Provides#
| Your Custom Code | SaaS Pegasus |
|---|---|
| DIY authentication | django-allauth with social providers |
| Custom team models | Proven team/membership architecture |
| Stripe integration | Complete subscription handling |
| Email sending | Configurable email backends |
| Celery setup | Ready-to-use background tasks |
| Frontend | React, Vue, HTMX, or Alpine options |
Step 1: Generate Your Pegasus Project#
Pegasus uses a configuration wizard to generate customized code:
# Visit saaspegasus.com and configure your project
# Download the generated codebase
# Or use their CLI
pip install pegasus-installer
pegasus create my-project --config config.yaml
Configuration Options#
# config.yaml example
project_name: "MyApp"
team_model: true
billing: stripe
frontend: react
email_backend: anymail
background_tasks: celery
database: postgresql
Step 2: Understand the Project Structure#
pegasus-project/
├── apps/
│ ├── users/ # User authentication
│ ├── teams/ # Team/organization model
│ ├── subscriptions/ # Billing integration
│ └── web/ # Main app pages
├── assets/ # Frontend code
├── pegasus/ # Core Pegasus code
└── templates/ # HTML templates
Step 3: Migrate Your Database#
Compare Models#
# Your custom User model
class User(AbstractUser):
company = models.CharField(max_length=100, blank=True)
phone = models.CharField(max_length=20, blank=True)
stripe_customer_id = models.CharField(max_length=100, blank=True)
# Pegasus User model
class CustomUser(AbstractUser):
# Pegasus uses teams for organization
# Avatar handling
avatar = models.FileField(upload_to='avatars/', blank=True)
# Subscription is on Team, not User
Create Migration Script#
# scripts/migrate_to_pegasus.py
import os
import django
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'myproject.settings')
django.setup()
from django.db import connection
from apps.users.models import CustomUser
from apps.teams.models import Team, Membership
def migrate_users():
"""Migrate users from old schema to Pegasus schema."""
with connection.cursor() as cursor:
# Get old users
cursor.execute("""
SELECT id, email, username, first_name, last_name,
company, phone, stripe_customer_id,
date_joined, is_active
FROM old_users_user
""")
old_users = cursor.fetchall()
for row in old_users:
(old_id, email, username, first_name, last_name,
company, phone, stripe_customer_id, date_joined, is_active) = row
# Create user in Pegasus
user, created = CustomUser.objects.get_or_create(
email=email,
defaults={
'username': username,
'first_name': first_name,
'last_name': last_name,
'date_joined': date_joined,
'is_active': is_active,
}
)
if created:
# Create personal team
team = Team.objects.create(
name=company if company else f"{first_name}'s Team",
slug=email.split('@')[0].lower()[:30],
)
# Add as owner
Membership.objects.create(
team=team,
user=user,
role=Membership.ROLE_ADMIN,
)
# Migrate Stripe customer to team
if stripe_customer_id:
team.stripe_customer_id = stripe_customer_id
team.save()
print(f"Migrated user: {email}")
if __name__ == '__main__':
migrate_users()
print("Migration complete!")
Migrate Custom Models#
# Your custom model
class Project(models.Model):
user = models.ForeignKey('users.User', on_delete=models.CASCADE)
name = models.CharField(max_length=200)
description = models.TextField(blank=True)
created_at = models.DateTimeField(auto_now_add=True)
# Pegasus team-scoped model
class Project(models.Model):
team = models.ForeignKey('teams.Team', on_delete=models.CASCADE)
created_by = models.ForeignKey(
'users.CustomUser',
on_delete=models.SET_NULL,
null=True
)
name = models.CharField(max_length=200)
description = models.TextField(blank=True)
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
ordering = ['-created_at']
Step 4: Migrate Authentication#
Your Custom Auth Views#
# Your custom login view (probably)
class CustomLoginView(LoginView):
template_name = 'auth/login.html'
def get_success_url(self):
return reverse('dashboard')
Pegasus Auth (django-allauth)#
Pegasus uses django-allauth which handles:
- Email/password authentication
- Social authentication (Google, GitHub, etc.)
- Email verification
- Password reset
# settings.py (Pegasus configured)
AUTHENTICATION_BACKENDS = [
'django.contrib.auth.backends.ModelBackend',
'allauth.account.auth_backends.AuthenticationBackend',
]
ACCOUNT_EMAIL_REQUIRED = True
ACCOUNT_EMAIL_VERIFICATION = 'mandatory'
ACCOUNT_AUTHENTICATION_METHOD = 'email'
# Social auth
SOCIALACCOUNT_PROVIDERS = {
'google': {
'APP': {
'client_id': os.environ.get('GOOGLE_CLIENT_ID'),
'secret': os.environ.get('GOOGLE_CLIENT_SECRET'),
},
'SCOPE': ['profile', 'email'],
}
}
Update OAuth Callbacks#
In Google Cloud Console, update callback URLs:
https://your-domain.com/accounts/google/login/callback/
Step 5: Migrate Views to Team Context#
Before: User-Scoped Views#
# Your custom view
class ProjectListView(LoginRequiredMixin, ListView):
model = Project
template_name = 'projects/list.html'
def get_queryset(self):
return Project.objects.filter(user=self.request.user)
After: Team-Scoped Views#
# Pegasus team-scoped view
from apps.teams.mixins import TeamMixin
class ProjectListView(TeamMixin, ListView):
model = Project
template_name = 'projects/list.html'
def get_queryset(self):
# TeamMixin provides self.team
return Project.objects.filter(team=self.team)
Pegasus Team Mixins#
# apps/teams/mixins.py (provided by Pegasus)
class TeamMixin:
"""Mixin that adds team context to views."""
def dispatch(self, request, *args, **kwargs):
# Get team from URL or session
self.team = get_object_or_404(
Team,
slug=kwargs.get('team_slug')
)
# Verify membership
membership = Membership.objects.filter(
team=self.team,
user=request.user
).first()
if not membership:
raise PermissionDenied("Not a member of this team")
self.membership = membership
return super().dispatch(request, *args, **kwargs)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['team'] = self.team
context['membership'] = self.membership
return context
Step 6: Migrate Billing#
Your Custom Stripe Integration#
# Your custom checkout
import stripe
def create_checkout_session(request):
stripe.api_key = settings.STRIPE_SECRET_KEY
session = stripe.checkout.Session.create(
customer_email=request.user.email,
mode='subscription',
line_items=[{'price': 'price_xxx', 'quantity': 1}],
success_url=f'{settings.BASE_URL}/success/',
cancel_url=f'{settings.BASE_URL}/pricing/',
metadata={'user_id': request.user.id},
)
return redirect(session.url)
Pegasus Subscription Handling#
# Pegasus provides subscription models and views
from djstripe.models import Subscription
# In your view
class DashboardView(TeamMixin, TemplateView):
template_name = 'dashboard.html'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
# Pegasus integrates with dj-stripe
context['subscription'] = self.team.subscription
context['is_subscribed'] = self.team.has_active_subscription()
return context
Checkout with Pegasus#
# apps/subscriptions/views.py (Pegasus pattern)
from apps.subscriptions.helpers import create_stripe_checkout_session
@login_required
@team_required
def create_checkout(request, team_slug):
team = get_object_or_404(Team, slug=team_slug)
price_id = request.POST.get('price_id')
# Pegasus helper handles customer creation
checkout_session = create_stripe_checkout_session(
team=team,
user=request.user,
price_id=price_id,
success_url=f'{settings.BASE_URL}/a/{team.slug}/subscription/success/',
cancel_url=f'{settings.BASE_URL}/pricing/',
)
return redirect(checkout_session.url)
Migrate Existing Subscriptions#
# scripts/migrate_subscriptions.py
from apps.teams.models import Team
from djstripe.models import Customer, Subscription
import stripe
stripe.api_key = settings.STRIPE_SECRET_KEY
def migrate_subscriptions():
# Get teams with old Stripe customer IDs
teams = Team.objects.exclude(stripe_customer_id='')
for team in teams:
# Sync customer from Stripe
stripe_customer = stripe.Customer.retrieve(team.stripe_customer_id)
# Create dj-stripe Customer
customer, _ = Customer.get_or_create(id=stripe_customer.id)
# Link to team
team.customer = customer
team.save()
# Sync subscriptions
subscriptions = stripe.Subscription.list(
customer=team.stripe_customer_id,
status='active'
)
for sub in subscriptions.data:
Subscription.sync_from_stripe_data(sub)
print(f"Migrated subscriptions for team: {team.name}")
Step 7: Migrate Templates#
Your Custom Templates#
<!-- Your template -->
{% extends "base.html" %}
{% block content %}
<div class="container">
<h1>Welcome, {{ user.first_name }}</h1>
{% for project in projects %}
<div class="project-card">
<h3>{{ project.name }}</h3>
<p>{{ project.description }}</p>
</div>
{% endfor %}
</div>
{% endblock %}
Pegasus Templates#
<!-- Pegasus pattern -->
{% extends "web/app/app_base.html" %}
{% load team_tags %}
{% block app_content %}
<div class="container mx-auto px-4 py-8">
<h1 class="text-2xl font-bold mb-6">
Welcome to {{ team.name }}
</h1>
{% if membership.is_admin %}
<a href="{% url 'teams:manage_team' team.slug %}"
class="btn btn-primary mb-4">
Team Settings
</a>
{% endif %}
<div class="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
{% for project in projects %}
<div class="card">
<div class="card-body">
<h3 class="card-title">{{ project.name }}</h3>
<p>{{ project.description|truncatewords:20 }}</p>
</div>
</div>
{% endfor %}
</div>
</div>
{% endblock %}
Pegasus Template Tags#
# Pegasus provides team-aware template tags
{% load team_tags %}
{% if membership.is_admin %}
<!-- Admin-only content -->
{% endif %}
{% if team.has_active_subscription %}
<!-- Subscriber content -->
{% else %}
<a href="{% url 'subscriptions:checkout' team.slug %}">
Upgrade to access this feature
</a>
{% endif %}
Step 8: Migrate Background Tasks#
Your Custom Celery Setup#
# Your celery.py
from celery import Celery
app = Celery('myproject')
app.config_from_object('django.conf:settings', namespace='CELERY')
app.autodiscover_tasks()
Pegasus Celery Configuration#
Pegasus comes with Celery pre-configured:
# config/celery.py (Pegasus)
import os
from celery import Celery
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'myproject.settings')
app = Celery('myproject')
app.config_from_object('django.conf:settings', namespace='CELERY')
app.autodiscover_tasks()
# Pegasus adds team-aware task helpers
@app.task(bind=True)
def team_task(self, team_id, *args, **kwargs):
from apps.teams.models import Team
team = Team.objects.get(id=team_id)
# Task logic here
Migrate Your Tasks#
# Your task
@shared_task
def send_report_email(user_id):
user = User.objects.get(id=user_id)
# Send email logic
# Pegasus pattern - team aware
@shared_task
def send_report_email(team_id, user_id):
from apps.teams.models import Team
from apps.users.models import CustomUser
team = Team.objects.get(id=team_id)
user = CustomUser.objects.get(id=user_id)
# Use Pegasus email helpers
from apps.utils.email import send_email
send_email(
recipient=user.email,
subject=f"Report for {team.name}",
template='emails/report.html',
context={'team': team, 'user': user},
)
Step 9: Update URLs#
Your Custom URLs#
# urls.py
urlpatterns = [
path('dashboard/', views.dashboard, name='dashboard'),
path('projects/', views.project_list, name='project_list'),
path('projects/<int:pk>/', views.project_detail, name='project_detail'),
path('settings/', views.settings, name='settings'),
]
Pegasus URL Structure#
# urls.py (Pegasus pattern)
urlpatterns = [
# Public pages
path('', views.home, name='home'),
path('pricing/', views.pricing, name='pricing'),
# Team-scoped URLs
path('a/<slug:team_slug>/', include([
path('', views.team_dashboard, name='team_dashboard'),
path('projects/', views.project_list, name='project_list'),
path('projects/<int:pk>/', views.project_detail, name='project_detail'),
path('settings/', include('apps.teams.urls')),
path('billing/', include('apps.subscriptions.urls')),
])),
# User account URLs
path('accounts/', include('allauth.urls')),
]
Step 10: Test Everything#
Testing Checklist#
- User registration works
- Email verification sends
- Social login (Google, GitHub) works
- Teams are created for new users
- Team invitations work
- Role-based access enforced
- Subscriptions process correctly
- Webhooks update team subscription status
- Existing data migrated correctly
- Background tasks execute
Run Pegasus Tests#
# Pegasus includes comprehensive tests
python manage.py test apps.users
python manage.py test apps.teams
python manage.py test apps.subscriptions
# Run all tests
python manage.py test
Common Challenges#
1. User Model Conflicts#
Problem: Pegasus expects CustomUser, you have different fields.
Solution: Add your custom fields to Pegasus's user model:
# apps/users/models.py
class CustomUser(AbstractUser):
# Pegasus fields
avatar = models.FileField(upload_to='avatars/', blank=True)
# Your custom fields
phone = models.CharField(max_length=20, blank=True)
company = models.CharField(max_length=100, blank=True)
timezone = models.CharField(max_length=50, default='UTC')
2. URL Namespace Conflicts#
Problem: Your URL names conflict with Pegasus.
Solution: Use app_name namespaces:
# Your app urls.py
app_name = 'myapp'
urlpatterns = [
path('projects/', views.list, name='project_list'),
]
# Usage
{% url 'myapp:project_list' team.slug %}
3. Template Override Conflicts#
Problem: Pegasus templates have different block names.
Solution: Map your blocks to Pegasus blocks:
<!-- Create a compatibility layer -->
{% extends "web/app/app_base.html" %}
{% block app_content %}
{% block content %}{% endblock %}
{% endblock %}
Timeline Estimate#
| Phase | Estimated Time |
|---|---|
| Pegasus Setup | 2 hours |
| User Migration | 3 hours |
| Model Migration | 4 hours |
| View Updates | 5 hours |
| Template Updates | 4 hours |
| Billing Migration | 3 hours |
| Testing | 3 hours |
| Total | ~24 hours |
Conclusion#
Migrating from custom Django to SaaS Pegasus gives you a production-tested foundation for teams, billing, and authentication. The main work is adapting your user-scoped code to team-scoped patterns and migrating existing data.
Pegasus's documentation is comprehensive, and their support community is helpful. Take advantage of both during your migration journey.
Not sure which boilerplate to choose?
Take our 2-minute quiz and get personalized recommendations.
Take the Quiz