How to Build a Django Stripe SaaS Boilerplate

June 14, 2021, 5:20 p.m.
Django Stripe SaaS · 29 min read
How to Build a Django Stripe SaaS Boilerplate
Last Modified: July 12, 2021, 9:15 a.m.

Let's build a SaaS template using Django and Stripe.  With the boilerplate project, you'll be able to quickly build your own minimum viable product in days instead of weeks.

We'll use Django for the backend and Stripe to collect monthly payments.

 

Getting Started

Identify problems and pose solutions

To begin, you need an idea.

Random brainstorming is great, but not beneficial when finding a good SaaS idea.

Start by identifying problems within any given community of people or business industry.

Why identify the problems?

Because solving problems means you can get paid for offering the solution.

Companies will gladly pay for the solution on a recurring basis if your software solves a pain point in their industry.

 

3 Quick SaaS Case Studies: Problems and Solutions

Canva

  • Audience: Individuals, small teams, and enterprises
  • Problem: Graphic design programs aren't the easiest to use and require someone experienced in graphic design. 
  • Solution: A drag-and-drop platform for graphic design that anyone can use.

Webflow

  • Audience: Web developers and non-programmers
  • Problem: Coding a website takes months. 
  • Solution: A no-code platform for web development.

MailChimp

  • Audience: Small businesses
  • Problem: Running an email marketing campaign takes time.
  • Solution: Schedule automated emails and track results. 

 

Create the Boilerplate for your SaaS

Example problem and SaaS solution

So let's say we identified a problem -- small businesses don't have an easy-to-use platform to monitor their monthly expenses. There's Excel but it's a pain to use.  

Our solution is to offer software where users pay to have a simple dashboard that lets them track their monthly expenses.

Now keep in mind this is just an example. There are already companies that offer accounting software for small businesses. If we can offer a better value proposition, make it easier to use, and target the right audience, we could find some product-market fit.

We'll build the skeleton of an MVP to get product validation and collect feedback without spending too much time or money on the product.

This way we can move quickly by only adding the core functionality.

Now let's start building a minimal viable product (MVP).

 

Install Django

Windows Command Prompt

C:\Users\Owner\desktop> py -m venv env

C:\Users\Owner\desktop> cd env

C:\Users\Owner\desktop\env> Scripts\activate

(env)C:\Users\Owner\desktop\env>pip install django

(env)C:\Users\Owner\desktop\env>django-admin startproject mysite

(env)C:\Users\Owner\desktop\env> cd mysite

(env)C:\Users\Owner\desktop\env\mysite> py manage.py startapp main

(env) C:\Users\Owner\Desktop\Code\env\mysite>py manage.py runserver

Create a virtual environment and install Django. The virtual environment creates an isolated environment for project-specific packages. Django is a framework that handles the database, user authentication, and templating engines. 

If you're new to Django follow the beginner's guide to Django.

 

Install Django Crispy Forms

Windows Command Prompt

(env)C:\Users\Owner\desktop\env>pip install django-crispy-forms

Install Django Crispy Forms. We'll use this package to style the Django forms used for user registration and login.

 

Configure Django

env > mysite > mysite > settings.py

...

# Application definition

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'main',
    'crispy_forms',
]

CRISPY_TEMPLATE_PACK = 'bootstrap4'
...


# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/3.2/howto/static-files/

STATIC_URL = '/static/'

STATICFILES_DIRS = [
    BASE_DIR / "static",
    '/var/www/static/',
]

...

MESSAGE_TAGS = {
        messages.DEBUG: 'alert-secondary',
        messages.INFO: 'alert-info',
        messages.SUCCESS: 'alert-success',
        messages.WARNING: 'alert-warning',
        messages.ERROR: 'alert-danger',
 }

Now you'll need to add the basic Django configurations to your settings file. Add main and crispy_forms to the installed apps list, add the Crispy forms template pack, and configure the project for static files

Also, configure the Django messages framework with Bootstrap alerts.

Please refer to the Django beginner's guide if you need a step-by-step walkthrough. 

 

Add the custom stylesheet

env > mysite > (New Folder) static > (New Folder) main > (New Folder) css

   /*client images */
    .client {
      height:60px; 
      width:60px; 
      border-radius: 50%; 
      object-fit:cover;
    }

    /*quote svgs*/
    .bi-chat-square-quote{
      color:rgb(108,99,255, 0.3);
    }

    /*badge color*/
    .badge{
      background-color:rgb(108,99,255, 0.2) !important;
      color:rgb(108,99,255) !important;
    }

    /*stars color */
    .bi-star, .bi-star-fill, .bi-star-half{
      color:#ffc107;
    }

    /* cta text*/
    .cta{
      font-size: 70px;
      line-height: 75px;
      font-family: 'Roboto Slab', serif;
    }


    .navbar-brand{
      font-family: 'Roboto Slab', serif;
    }

    .cta-span{
      color:rgb(108,99,255, 0.8) !important;
    }

    .bg-primary{
      background:rgb(108,99,255) !important;
    }

    .border-primary {
      border: 1px solid rgb(108,99,255) !important;
    }


    .btn-primary, .btn-primary:visited {
        font-weight:600;
        background: rgb(255,86,120);
        border: 1px solid rgb(255,86,120);
        color: white !important;
        border-radius:0px;
    }
    
    .btn-primary:hover, .btn-primary:active, .btn-primary:focus, 
    .btn-outline-primary:hover, .btn-outline-primary:active, .btn-outline-primary:focus {
       background: rgb(255,61,100) !important;
       border: 1px solid rgb(255,61,100) !important;
       border-radius:0px;
       color:white !important;
    }

    .btn-outline-primary, .btn-outline-primary:visited {
        font-weight:600;
        background: transparent;
        border: 1px solid rgb(255,86,120);
        color: rgb(255,61,100);
        border-radius:0px;
    }
    
    .form-control{
      background:#F2F2F2;
      border-radius:0px;
      border:none;
      height:48px !important;
    }

    /*background circle for icons*/
    .numberCircle {
      border-radius: 50%;
      width: 60px;
      height: 60px;
      padding: 8px;
      background:rgb(255,86,120, 0.2);
      border: none;
      color: rgb(255,86,120);
      text-align: center;
      font-size: 25px;
    }

    p{
      line-height: 25px;
    }

    .nav-pills .nav-link.active, .nav-pills .show>.nav-link {
    color: #fff !important;
    background-color:rgb(138,131,255);
    }

    .nav-link:hover{
      color: rgb(138,131,255) !important;
    }

    .footer-social-link:hover {
      color:rgb(137,130,255) !important;
    }

    @media only screen and (max-width: 600px) {  
        .cta{
          font-size: 50px;
          line-height: 55px;
          font-family: 'Roboto Slab', serif;
        }

     }

    @media only screen and (max-width:768px){
      /*background circle for icons*/
        .numberCircle {
          border-radius: 50%;
          width: 60px;
          height: 60px;
          padding: 8px;
          background:rgb(255,86,120, 0.2);
          border: none;
          color: rgb(255,86,120);
          text-align: center;
          font-size: 25px;
          margin-left: auto;
          margin-right: auto;
        }
    }

Add a custom stylesheet to the project. 

 

Add the images

env > mysite > (New Folder) static > (New Folder) main > (New Folder) img

Open Doodles

Add the images for the project. Feel free to use your own images or check out the open-source illustration I used at opendoodles.com

 

Create a navbar

env > mysite > main > templates > main > (New Folder) includes > (New File) navbar.html

<nav class="navbar navbar-expand-lg navbar-light bg-transparent">
  <div class="container-fluid">
    <a class="navbar-brand" href="/">
      <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" style="color: rgb(137,130,255);" fill="currentColor" class="bi bi-bar-chart-fill" viewBox="0 0 16 16">
        <path d="M1 11a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1v3a1 1 0 0 1-1 1H2a1 1 0 0 1-1-1v-3zm5-4a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1v7a1 1 0 0 1-1 1H7a1 1 0 0 1-1-1V7zm5-5a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1v12a1 1 0 0 1-1 1h-2a1 1 0 0 1-1-1V2z"/>
      </svg>
      Track Smart
    </a>
    <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarText" aria-controls="navbarText" aria-expanded="false" aria-label="Toggle navigation">
      <span class="navbar-toggler-icon"></span>
    </button>
    <div class="collapse navbar-collapse" id="navbarText">
      <ul class="navbar-nav me-auto mb-2 mb-lg-0">
      {% if user.is_authenticated %}
        <li class="nav-item">
          <a class="nav-link" href="/dashboard">Dashboard</a>
        </li>
         <li class="nav-item">
          <a class="nav-link" href="/logout">Logout</a>
        </li>
         {% else %}
        <li class="nav-item">
          <a class="nav-link" href="#">Features</a>
        </li>
        <li class="nav-item">
          <a class="nav-link" href="/pricing">Pricing</a>
        </li>
        <li class="nav-item">
          <a class="nav-link" href="/login">Login</a>
        </li>
      {% endif %}
      </ul>
      {% if user.is_authenticated %}
        <p>Welcome, {{user.username}}</p>
      {% else %}
        <a href="/register" class="btn btn-outline-primary" type="button">Get Started</a>
      {% endif %}
    </div>
  </div>
</nav>

Here is a navbar with Django if conditions in the template. I have two if conditions, both of which show an authenticated user different text and links than a non-registered user.

 

Create a footer

env > mysite > main > templates > main > (New Folder) includes > (New File) footer.html

<div class="container-fluid text-center p-5">
	<div class='row'>
		<div class='col-lg-6 col-md-12 text-center text-md-left'>
			<p>© Company</p>
		</div>
		<div class='col-lg-6 col-md-12 text-center text-md-left'>
			<a href="#" class="text-dark footer-social-link"><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" class="mx-2" fill="currentColor" class="bi bi-facebook" viewBox="0 0 16 16">
				<path d="M16 8.049c0-4.446-3.582-8.05-8-8.05C3.58 0-.002 3.603-.002 8.05c0 4.017 2.926 7.347 6.75 7.951v-5.625h-2.03V8.05H6.75V6.275c0-2.017 1.195-3.131 3.022-3.131.876 0 1.791.157 1.791.157v1.98h-1.009c-.993 0-1.303.621-1.303 1.258v1.51h2.218l-.354 2.326H9.25V16c3.824-.604 6.75-3.934 6.75-7.951z"/>
			</svg></a>
			<a href="#" class="text-dark footer-social-link"><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" class="mx-2" fill="currentColor" class="bi bi-instagram" viewBox="0 0 16 16">
				<path d="M8 0C5.829 0 5.556.01 4.703.048 3.85.088 3.269.222 2.76.42a3.917 3.917 0 0 0-1.417.923A3.927 3.927 0 0 0 .42 2.76C.222 3.268.087 3.85.048 4.7.01 5.555 0 5.827 0 8.001c0 2.172.01 2.444.048 3.297.04.852.174 1.433.372 1.942.205.526.478.972.923 1.417.444.445.89.719 1.416.923.51.198 1.09.333 1.942.372C5.555 15.99 5.827 16 8 16s2.444-.01 3.298-.048c.851-.04 1.434-.174 1.943-.372a3.916 3.916 0 0 0 1.416-.923c.445-.445.718-.891.923-1.417.197-.509.332-1.09.372-1.942C15.99 10.445 16 10.173 16 8s-.01-2.445-.048-3.299c-.04-.851-.175-1.433-.372-1.941a3.926 3.926 0 0 0-.923-1.417A3.911 3.911 0 0 0 13.24.42c-.51-.198-1.092-.333-1.943-.372C10.443.01 10.172 0 7.998 0h.003zm-.717 1.442h.718c2.136 0 2.389.007 3.232.046.78.035 1.204.166 1.486.275.373.145.64.319.92.599.28.28.453.546.598.92.11.281.24.705.275 1.485.039.843.047 1.096.047 3.231s-.008 2.389-.047 3.232c-.035.78-.166 1.203-.275 1.485a2.47 2.47 0 0 1-.599.919c-.28.28-.546.453-.92.598-.28.11-.704.24-1.485.276-.843.038-1.096.047-3.232.047s-2.39-.009-3.233-.047c-.78-.036-1.203-.166-1.485-.276a2.478 2.478 0 0 1-.92-.598 2.48 2.48 0 0 1-.6-.92c-.109-.281-.24-.705-.275-1.485-.038-.843-.046-1.096-.046-3.233 0-2.136.008-2.388.046-3.231.036-.78.166-1.204.276-1.486.145-.373.319-.64.599-.92.28-.28.546-.453.92-.598.282-.11.705-.24 1.485-.276.738-.034 1.024-.044 2.515-.045v.002zm4.988 1.328a.96.96 0 1 0 0 1.92.96.96 0 0 0 0-1.92zm-4.27 1.122a4.109 4.109 0 1 0 0 8.217 4.109 4.109 0 0 0 0-8.217zm0 1.441a2.667 2.667 0 1 1 0 5.334 2.667 2.667 0 0 1 0-5.334z"/>
			</svg></a>
			<a href="#" class="text-dark footer-social-link"><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" class="mx-2" fill="currentColor" class="bi bi-twitter" viewBox="0 0 16 16">
				<path d="M5.026 15c6.038 0 9.341-5.003 9.341-9.334 0-.14 0-.282-.006-.422A6.685 6.685 0 0 0 16 3.542a6.658 6.658 0 0 1-1.889.518 3.301 3.301 0 0 0 1.447-1.817 6.533 6.533 0 0 1-2.087.793A3.286 3.286 0 0 0 7.875 6.03a9.325 9.325 0 0 1-6.767-3.429 3.289 3.289 0 0 0 1.018 4.382A3.323 3.323 0 0 1 .64 6.575v.045a3.288 3.288 0 0 0 2.632 3.218 3.203 3.203 0 0 1-.865.115 3.23 3.23 0 0 1-.614-.057 3.283 3.283 0 0 0 3.067 2.277A6.588 6.588 0 0 1 .78 13.58a6.32 6.32 0 0 1-.78-.045A9.344 9.344 0 0 0 5.026 15z"/>
			</svg></a>

		</div>
	</div>
	
</div>

Let's also add a footer with some social media links. 

 

Create a Django header.html

env > mysite > main > templates > main > (New File) header.html

<!DOCTYPE html>
<html lang="en">
<head>
	{% load static %}
	<!-- Required meta tags -->
	<meta charset="utf-8">
	<meta name="viewport" content="width=device-width, initial-scale=1">

	<!-- Bootstrap CSS -->
	<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.1/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-+0n0xVW2eSR5OomGNYDnhzAbDsOXxcvSN1TPprVMTNDbiYZCxYbOOl7+AMvyTG2x" crossorigin="anonymous">

	<!--Custom CSS-->
	<link rel="stylesheet" href="{% static 'main/css/stylesheet.css'%}" type="text/css">

	<!--Google Fonts-->
	<link rel="preconnect" href="https://fonts.gstatic.com">
	<link href="https://fonts.googleapis.com/css2?family=Roboto+Slab:wght@600&display=swap" rel="stylesheet">

	<title>Django + Stripe SaaS</title>
</head>
<body>
	{% include "main/includes/navbar.html" %}

	{% include 'main/includes/messages.html' %}

	<div class="my-5">
		{% block content %}

		{% endblock %} 
	</div>

	{% include 'main/includes/footer.html' %}

	<!-- Optional JavaScript; choose one of the two! -->

	<!-- Option 1: Bootstrap Bundle with Popper -->
	<script class="lazy" data-class="lazy" src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.1/dist/js/bootstrap.bundle.min.js" integrity="sha384-gtEjrD/SeCtmISkJkNUaaKMoLD0//ElJ19smozuHV6z3Iehds+3Ulb9Bn9Plx0x4" crossorigin="anonymous"></script>
</body>
</html>

This is the header that extends to all other HTML templates. It uses the Django include and extends template tags. It also contains the Bootstrap CDNs.

 

Create a homepage

env > mysite > main > templates > main > (New File) home.html

{% extends 'main/header.html' %}
{% block content %}
{% load static %}
<!--CTA-->
<div class="container text-center p-lg-5 p-3">
	<div class="row p-lg-5 p-3">
		<div class="col-lg-6 col-md-12 mb-4">
			<h1 class="cta">Track your monthly spending <span class="cta-span">easily.</span></h1>
			<h5 class="text-muted lead my-3">Don't run away from your finances. Jump right in.</h5>
			<form class="my-5" method="GET" action="/register">
				<div class="row">
					<div class="col-xl-6 col-lg-12 mb-4">
						<div class="form-floating mb-3">
							<input type="text" name="email" class="form-control" id="floatingTextInput1" placeholder="email@example.com">
							<label for="floatingTextInput1">Enter email address...</label>
						</div>
					</div>
					<div class="col-xl-6 col-lg-12">
						<div class="d-grid">
							<button class="btn btn-primary btn-lg" type="submit">Join ➜</button>
						</div>
					</div>
				</div>
			</form>
		</div>
		<div class="col-lg-6 col-md-12 my-auto">
			<img class="img-fluid" class="lazy" src="{% static 'main/img/jumping.svg' %}" alt="jumping">
		</div>
	</div>
</div>
<!--End CTA-->


<!--Value Prop-->

<div class="container p-lg-5 p-3">
	<div class="row p-lg-5 p-3 my-5">
		<div class="col-lg-6 col-md-12 order-lg-2 order-md-1 my-auto">
			<span class="badge rounded-pill">Track monthly spending</span>
			<h1 class="my-3">Reach your monthly budget</h1>
			<p class="text-muted">Set weekly and monthly spending habits for your entire team. Get alerts when your close to your limit.</p>
		</div>
		<div class="col-lg-6 col-md-12 order-lg-1 order-md-2 my-auto">
			<img class="img-fluid" class="lazy" src="{% static 'main/img/sprinting.gif' %}" alt="sprinting">
		</div>
	</div>
</div>

<div class="container p-lg-5 p-3">
	<div class="row p-lg-5 p-3 my-5">
		<div class="col-lg-6 col-md-12 my-auto">
			<span class="badge rounded-pill">Upload images and PDFs</span>
			<h1 class="my-3">Spending made easy</h1>
			<p class="text-muted">No need to keep physical copies of receipts or bills. Upload your images and PDFs with a click of a button to your account.</p>
		</div>
		<div class="col-lg-6 col-md-12 my-auto">
			<img class="img-fluid" class="lazy" src="{% static 'main/img/clumsy.svg' %}" alt="clumsy">
		</div>
	</div>
</div>
<!--End Value Prop-->



<!--Features-->
<div class="container p-lg-5 p-3 text-center text-md-start">
	<h1 class="cta mt-5 text-center">Healthy spending made easy</h1>
	<h5 class="text-muted lead mt-3 mb-5 text-center">We build a platform designed for small businesses. Track your spending, marketing budget and more all one a simple dashboard.</h5>
	<div class="row p-lg-5 px-3 py-5">
		<div class="col-sm-12 col-md-6 col-lg-3 pb-4">
			<div class="numberCircle my-3">
				<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" class="bi bi-phone" viewBox="0 0 16 16">
					<path d="M11 1a1 1 0 0 1 1 1v12a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1h6zM5 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2H5z"/>
					<path d="M8 14a1 1 0 1 0 0-2 1 1 0 0 0 0 2z"/>
				</svg>
			</div>
			<h5 class="card-title">Mobile Friendly</h5>
			<p class="card-text">Monitor your expenses on the go</p>
		</div>
		<div class="col-sm-12 col-md-6 col-lg-3 pb-4"> 
			<div class="numberCircle my-3">
				<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" class="bi bi-archive" viewBox="0 0 16 16">
				  <path d="M0 2a1 1 0 0 1 1-1h14a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1v7.5a2.5 2.5 0 0 1-2.5 2.5h-9A2.5 2.5 0 0 1 1 12.5V5a1 1 0 0 1-1-1V2zm2 3v7.5A1.5 1.5 0 0 0 3.5 14h9a1.5 1.5 0 0 0 1.5-1.5V5H2zm13-3H1v2h14V2zM5 7.5a.5.5 0 0 1 .5-.5h5a.5.5 0 0 1 0 1h-5a.5.5 0 0 1-.5-.5z"/>
				</svg>
			</div>
			<h5 class="card-title">Archived Files</h5>
			<p class="card-text">Refer to archived files forever</p>

		</div>
		<div class="col-sm-12 col-md-6 col-lg-3 pb-4">
			<div class="numberCircle my-3">
				<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" class="bi bi-bag-plus" viewBox="0 0 16 16">
				  <path fill-rule="evenodd" d="M8 7.5a.5.5 0 0 1 .5.5v1.5H10a.5.5 0 0 1 0 1H8.5V12a.5.5 0 0 1-1 0v-1.5H6a.5.5 0 0 1 0-1h1.5V8a.5.5 0 0 1 .5-.5z"/>
				  <path d="M8 1a2.5 2.5 0 0 1 2.5 2.5V4h-5v-.5A2.5 2.5 0 0 1 8 1zm3.5 3v-.5a3.5 3.5 0 1 0-7 0V4H1v10a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V4h-3.5zM2 5h12v9a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V5z"/>
				</svg>
			</div>
			<h5 class="card-title">Upload Receipts</h5>
			<p class="card-text">Upload an image of receipt of your purchases</p>

		</div>
		<div class="col-sm-12 col-md-6 col-lg-3 pb-4">
			<div class="numberCircle my-3">
				<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" class="bi bi-alarm" viewBox="0 0 16 16">
				  <path d="M8.5 5.5a.5.5 0 0 0-1 0v3.362l-1.429 2.38a.5.5 0 1 0 .858.515l1.5-2.5A.5.5 0 0 0 8.5 9V5.5z"/>
				  <path d="M6.5 0a.5.5 0 0 0 0 1H7v1.07a7.001 7.001 0 0 0-3.273 12.474l-.602.602a.5.5 0 0 0 .707.708l.746-.746A6.97 6.97 0 0 0 8 16a6.97 6.97 0 0 0 3.422-.892l.746.746a.5.5 0 0 0 .707-.708l-.601-.602A7.001 7.001 0 0 0 9 2.07V1h.5a.5.5 0 0 0 0-1h-3zm1.038 3.018a6.093 6.093 0 0 1 .924 0 6 6 0 1 1-.924 0zM0 3.5c0 .753.333 1.429.86 1.887A8.035 8.035 0 0 1 4.387 1.86 2.5 2.5 0 0 0 0 3.5zM13.5 1c-.753 0-1.429.333-1.887.86a8.035 8.035 0 0 1 3.527 3.527A2.5 2.5 0 0 0 13.5 1z"/>
				</svg>
			</div>
			<h5 class="card-title">Set Alerts</h5>
			<p class="card-text">Monitor your spending habits with automatic alerts</p>
		</div>
	</div>
</div>
<!--End Features-->



<!--Value Prop #2-->
<div class="container p-lg-5 p-3">
	<div class="row p-lg-5 p-3 my-5">
		<div class="col-lg-6 col-md-12 order-lg-2 order-md-1 my-auto">
			<span class="badge rounded-pill">Save your time</span>
			<h1 class="my-3">Efficiently allocate time</h1>
			<p class="text-muted">Don't send hours looking over your finances every month. Manage expenses as they happen so you can free up your time.</p>
		</div>
		<div class="col-lg-6 col-md-12 order-lg-1 order-md-2 my-auto">
			<img class="img-fluid" class="lazy" src="{% static 'main/img/sitting-reading.svg' %}" alt="sitting-reading">
		</div>
	</div>
</div>

<div class="container p-lg-5 p-3">
	<div class="row p-lg-5 p-3 my-5">
		<div class="col-lg-6 col-md-12 my-auto">
			<span class="badge rounded-pill">Setup made easy</span>
			<h1>Out of the box configuration</h1>
			<p class="text-muted">Create an account and get going. Everything is already configured for easy file upload and tracking on the dashboard.</p>
		</div>
		<div class="col-lg-6 col-md-12 my-auto">
			<img class="img-fluid" class="lazy" src="{% static 'main/img/unboxing.svg' %}" alt="unboxing">
		</div>
	</div>
</div>
<!--End Value Prop #2-->


<!--Testimonials-->
<div class="container p-lg-5 p-3 text-center text-md-start">
	<div class="row p-lg-5 p-3 my-5">
		<div class="col-lg-6 col-md-12 my-5">
			<svg xmlns="http://www.w3.org/2000/svg" width="3em" height="3em" fill="currentColor" class="my-3 bi bi-chat-square-quote" viewBox="0 0 16 16">
				<path d="M14 1a1 1 0 0 1 1 1v8a1 1 0 0 1-1 1h-2.5a2 2 0 0 0-1.6.8L8 14.333 6.1 11.8a2 2 0 0 0-1.6-.8H2a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1h12zM2 0a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h2.5a1 1 0 0 1 .8.4l1.9 2.533a1 1 0 0 0 1.6 0l1.9-2.533a1 1 0 0 1 .8-.4H14a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2H2z"/>
				<path d="M7.066 4.76A1.665 1.665 0 0 0 4 5.668a1.667 1.667 0 0 0 2.561 1.406c-.131.389-.375.804-.777 1.22a.417.417 0 1 0 .6.58c1.486-1.54 1.293-3.214.682-4.112zm4 0A1.665 1.665 0 0 0 8 5.668a1.667 1.667 0 0 0 2.561 1.406c-.131.389-.375.804-.777 1.22a.417.417 0 1 0 .6.58c1.486-1.54 1.293-3.214.682-4.112z"/>
			</svg>
			<figure>
				<blockquote class="blockquote">
					<p>Great product. Love the easy-to-use interface. Saves my employees at least 2 hours by not having to constantly check up with one another and reference a spreadsheet.</p>
				</blockquote>
						<img class="lazy" src="{% static 'main/img/ben.png' %}" class="img-fluid client"><br>
						<b>Ben Smith</b>
						<br><small>Founder, Clean Paws</small><br>
						<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-star-fill" viewBox="0 0 16 16">
							<path d="M3.612 15.443c-.386.198-.824-.149-.746-.592l.83-4.73L.173 6.765c-.329-.314-.158-.888.283-.95l4.898-.696L7.538.792c.197-.39.73-.39.927 0l2.184 4.327 4.898.696c.441.062.612.636.282.95l-3.522 3.356.83 4.73c.078.443-.36.79-.746.592L8 13.187l-4.389 2.256z"/>
						</svg>
						<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-star-fill" viewBox="0 0 16 16">
							<path d="M3.612 15.443c-.386.198-.824-.149-.746-.592l.83-4.73L.173 6.765c-.329-.314-.158-.888.283-.95l4.898-.696L7.538.792c.197-.39.73-.39.927 0l2.184 4.327 4.898.696c.441.062.612.636.282.95l-3.522 3.356.83 4.73c.078.443-.36.79-.746.592L8 13.187l-4.389 2.256z"/>
						</svg>
						<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-star-fill" viewBox="0 0 16 16">
							<path d="M3.612 15.443c-.386.198-.824-.149-.746-.592l.83-4.73L.173 6.765c-.329-.314-.158-.888.283-.95l4.898-.696L7.538.792c.197-.39.73-.39.927 0l2.184 4.327 4.898.696c.441.062.612.636.282.95l-3.522 3.356.83 4.73c.078.443-.36.79-.746.592L8 13.187l-4.389 2.256z"/>
						</svg>
						<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-star-fill" viewBox="0 0 16 16">
							<path d="M3.612 15.443c-.386.198-.824-.149-.746-.592l.83-4.73L.173 6.765c-.329-.314-.158-.888.283-.95l4.898-.696L7.538.792c.197-.39.73-.39.927 0l2.184 4.327 4.898.696c.441.062.612.636.282.95l-3.522 3.356.83 4.73c.078.443-.36.79-.746.592L8 13.187l-4.389 2.256z"/>
						</svg>
						<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-star" viewBox="0 0 16 16">
							<path d="M2.866 14.85c-.078.444.36.791.746.593l4.39-2.256 4.389 2.256c.386.198.824-.149.746-.592l-.83-4.73 3.522-3.356c.33-.314.16-.888-.282-.95l-4.898-.696L8.465.792a.513.513 0 0 0-.927 0L5.354 5.12l-4.898.696c-.441.062-.612.636-.283.95l3.523 3.356-.83 4.73zm4.905-2.767-3.686 1.894.694-3.957a.565.565 0 0 0-.163-.505L1.71 6.745l4.052-.576a.525.525 0 0 0 .393-.288L8 2.223l1.847 3.658a.525.525 0 0 0 .393.288l4.052.575-2.906 2.77a.565.565 0 0 0-.163.506l.694 3.957-3.686-1.894a.503.503 0 0 0-.461 0z"/>
						</svg>
			</figure>
		</div>
		<div class="col-lg-6 col-md-12 my-5">
			<svg xmlns="http://www.w3.org/2000/svg" width="3em" height="3em" fill="currentColor" class="my-3 bi bi-chat-square-quote" viewBox="0 0 16 16">
				<path d="M14 1a1 1 0 0 1 1 1v8a1 1 0 0 1-1 1h-2.5a2 2 0 0 0-1.6.8L8 14.333 6.1 11.8a2 2 0 0 0-1.6-.8H2a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1h12zM2 0a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h2.5a1 1 0 0 1 .8.4l1.9 2.533a1 1 0 0 0 1.6 0l1.9-2.533a1 1 0 0 1 .8-.4H14a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2H2z"/>
				<path d="M7.066 4.76A1.665 1.665 0 0 0 4 5.668a1.667 1.667 0 0 0 2.561 1.406c-.131.389-.375.804-.777 1.22a.417.417 0 1 0 .6.58c1.486-1.54 1.293-3.214.682-4.112zm4 0A1.665 1.665 0 0 0 8 5.668a1.667 1.667 0 0 0 2.561 1.406c-.131.389-.375.804-.777 1.22a.417.417 0 1 0 .6.58c1.486-1.54 1.293-3.214.682-4.112z"/>
			</svg>
			<figure>
				<blockquote class="blockquote">
					<p>I never need to worry about keeping track of receipts or spending. Everything is gets uploaded directly to the site.</p>
				</blockquote>
						<img class="lazy" src="{% static 'main/img/karen.png' %}" class="img-fluid client"><br>
						<b>Karen Adams</b>
						<br><small>Owner, Restaurant Cafe</small><br>
						<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-star-fill" viewBox="0 0 16 16">
							<path d="M3.612 15.443c-.386.198-.824-.149-.746-.592l.83-4.73L.173 6.765c-.329-.314-.158-.888.283-.95l4.898-.696L7.538.792c.197-.39.73-.39.927 0l2.184 4.327 4.898.696c.441.062.612.636.282.95l-3.522 3.356.83 4.73c.078.443-.36.79-.746.592L8 13.187l-4.389 2.256z"/>
						</svg>
						<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-star-fill" viewBox="0 0 16 16">
							<path d="M3.612 15.443c-.386.198-.824-.149-.746-.592l.83-4.73L.173 6.765c-.329-.314-.158-.888.283-.95l4.898-.696L7.538.792c.197-.39.73-.39.927 0l2.184 4.327 4.898.696c.441.062.612.636.282.95l-3.522 3.356.83 4.73c.078.443-.36.79-.746.592L8 13.187l-4.389 2.256z"/>
						</svg>
						<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-star-fill" viewBox="0 0 16 16">
							<path d="M3.612 15.443c-.386.198-.824-.149-.746-.592l.83-4.73L.173 6.765c-.329-.314-.158-.888.283-.95l4.898-.696L7.538.792c.197-.39.73-.39.927 0l2.184 4.327 4.898.696c.441.062.612.636.282.95l-3.522 3.356.83 4.73c.078.443-.36.79-.746.592L8 13.187l-4.389 2.256z"/>
						</svg>
						<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-star-fill" viewBox="0 0 16 16">
							<path d="M3.612 15.443c-.386.198-.824-.149-.746-.592l.83-4.73L.173 6.765c-.329-.314-.158-.888.283-.95l4.898-.696L7.538.792c.197-.39.73-.39.927 0l2.184 4.327 4.898.696c.441.062.612.636.282.95l-3.522 3.356.83 4.73c.078.443-.36.79-.746.592L8 13.187l-4.389 2.256z"/>
						</svg>
						<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-star" viewBox="0 0 16 16">
							<path d="M2.866 14.85c-.078.444.36.791.746.593l4.39-2.256 4.389 2.256c.386.198.824-.149.746-.592l-.83-4.73 3.522-3.356c.33-.314.16-.888-.282-.95l-4.898-.696L8.465.792a.513.513 0 0 0-.927 0L5.354 5.12l-4.898.696c-.441.062-.612.636-.283.95l3.523 3.356-.83 4.73zm4.905-2.767-3.686 1.894.694-3.957a.565.565 0 0 0-.163-.505L1.71 6.745l4.052-.576a.525.525 0 0 0 .393-.288L8 2.223l1.847 3.658a.525.525 0 0 0 .393.288l4.052.575-2.906 2.77a.565.565 0 0 0-.163.506l.694 3.957-3.686-1.894a.503.503 0 0 0-.461 0z"/>
						</svg>   
			</figure>
		</div>
	</div>
</div>
<!--End Testimonials-->


<!--Second CTA-->
<div class="container text-center p-lg-5 p-3">
	<h1 class="cta my-5">Get started today</h1>
	<h5 class="text-muted lead my-4">Stay on top of your small business's finances. Jump right in.</h5>
	<form  method="GET" action="/register">
		<div class="form-floating mb-3 mx-auto" style="max-width: 40rem">
			<input type="text" name="email" class="form-control" id="floatingTextInput1" placeholder="email@example.com">
			<label for="floatingTextInput1">Enter email address...</label>
			<div id="emailHelp" class="form-text">We'll never share your email with anyone else.</div>
		</div>
		<button class="btn btn-primary btn-lg my-4" type="submit">Join ➜</button>
	</form>
</div>
<!-- End Second CTA-->

{% endblock %}

Add the homepage. The homepage needs to include a call-to-action (CTA) above the fold. In other words, the CTA must appear on the page load without the user scrolling.

After the CTA is the value proposition. There can be several that highlight the benefits of the software.  Next are the features. These are quick highlights that showcase the helpful aspects of the platform.

Another section of the homepage should be the testimonials.  The goal is to highlight the existing trust between a current client and the software. 

Finally, add another CTA at the bottom of the page. That way users don't have to scroll back up to the top of the page just to sign up.

Read How to make a SaaS Homepage for a more in-depth walkthrough on SaaS homepages. 

 

SaaS Django+Stripe Homepage

 

Create a register page

env > mysite > main > templates > main > (New File) login.html

{% extends 'main/header.html' %}
	{% block content %}
	{% load crispy_forms_tags %}
	{% load static %}
<div class="container p-md-3 p-sm-2">
	<div class="row g-0">
		<div class="col d-none d-lg-block my-auto">
			<img class="img-fluid" class="lazy" src="{% static 'main/img/sitting-reading.svg' %}" alt="sitting-reading">
		</div>
		<div class="col my-auto">
	<div class="mx-auto" style="max-width:30rem">
	<h1 class="cta my-5 text-center">Register</h1>
	<form method="POST">
		{% csrf_token %}
		{{ register_form|crispy }}                    
		<button class="btn btn-primary my-5" type="submit">Register</button>
	</form>
	</div>
	</div>
</div>
</div>
	{% endblock %}

Add a register template with a register form added to the views.py below.

Head over to User Registration, Login, and Logout Django Guide to set up the register and login forms used in this template and the one below. 

 

Create a login page

env > mysite > main > templates > main > (New File) login.html

{% extends 'main/header.html' %}
	{% block content %}
	{% load crispy_forms_tags %}
	{% load static %}
<div class="container p-md-3 p-sm-2">
	<div class="row g-0">
		<div class="col d-none d-lg-block my-auto">
			<img class="img-fluid" class="lazy" src="{% static 'main/img/reading-side.svg' %}" alt="reading-side">
		</div>
		<div class="col my-auto">
	<div class="mx-auto" style="max-width:30rem">
	<h1 class="cta my-5 text-center">Login</h1>
	<form method="POST">
		{% csrf_token %}
		{{ login_form|crispy }}                    
		<button class="btn btn-primary my-5" type="submit">Login</button>
		<p class="text-center">Don't have an account? <a href="/register">Create an account</a>.</p>
	</form>
	</div>
	</div>
</div>
</div>
	{% endblock %}

Also, add a login template with a login form. These two form templates are where Django Crispy Forms is used. 

 

Install Stripe

Windows Command Prompt

(env)C:\Users\Owner\desktop\env>pip install --upgrade stripe

Install Stripe. 

 

Create a subscription modal

env > mysite > main > models.py

from django.db import models

# Create your models here.
class Subscription(models.Model):
	name = models.CharField(max_length=100)
	amount = models.DecimalField(decimal_places=2, max_digits=5)
	stripe_product_id = models.CharField(max_length=100)

	def __str__(self):
		return self.name

Create a Subscription model. Add this modal to the admin.py and create a superuser to access the admin. 

If you are new to Django models, refer to this guide. on creating a Django superuser and adding model objects via the Django admin.

Notice the stripe_product_id field. We'll get this ID from Stripe in a second. 

 

Connect to Stripe

Login to your Stipe dashboard and turn on "View test data".

Click the "+ Add product" button on the top right side of the page and fill out the Product information form. We'll add three recurring products: Basic, Pro, and Enterprise Plans.

Add product to Stripe dashboard

 

Create the Django subscription model objects

Add products to Django model

Login to the admin and add your subscription plan in addition to the unique Stipe API ID found in the Stripe dashboard when you click each product.

Check out the dj-stripe instructions in Using Django and Stripe for Monthly Subscriptions if you're interested in configuring Django and Stripe to automatically sync.

 

Create a pricing page

env > mysite > main > templates > main > (New File) pricing.html

{% extends 'main/header.html' %}
{% block content %}
{% load static %}

<script class="lazy" src="https://polyfill.io/v3/polyfill.min.js?version=3.52.1&features=fetch"></script>
<script class="lazy" src="https://js.stripe.com/v3/"></script>

<!--Pricing-->
    <div class="container p-lg-5 p-3">
    	<h1 class="cta text-center">Pricing</span></h1>
      <h5 class="text-muted lead my-3 text-center">Don't run away from your finances. Jump right in.</h5>
      <div class="row p-lg-5 p-3">


        {% for s in subscription %}
        <div class="col-lg-4 col-md-12 mb-4">
          <div class="card h-100 shadow-lg text-center mx-auto sub-cards" style="max-width:20rem">
          	 <div class="card-header sub-header">
			    <h4>{{s.name}}</h4>
			  </div>
            <div class="card-body">    
               <h1>${{s.amount}}<span class="text-muted" style="font-weight: 300">/mo</span></h1>
            
        	<p class="my-3">
             {{s.description|safe|linebreaks}}
          </p>
            
            <div class="d-grid">

              {% if user.is_authenticated %}
            	<button class="btn btn-outline-primary btn-block btn-lg sub-button" onclick="selectPlan('{{s.stripe_api_id}}')">Select</button>
              {% else %}
              <a class="btn btn-outline-primary btn-block btn-lg sub-button" href="/register">Select</a>
              {% endif %}
            </div>
          </div>
          </div>
        </div>
        {% endfor %}


      </div>
    </div>

    <script type="text/javascript">

    //grabs csrftoken
    let cookie = document.cookie
    let cookies = cookie.substring(cookie.indexOf('csrftoken=') + 10)
    let csrfToken = cookies.split(";")[0]

    // Create an instance of the Stripe object with your publishable API key
    var stripe = Stripe("pk_test"); //replace with your Stripe API key

    function selectPlan(pid) {
      fetch("/create-checkout-session", {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
          "X-CSRFToken": csrfToken, 
        },
        body: JSON.stringify({
          product_id: pid
        })
      })
        .then(function (response) {
          return response.json();
        })
        .then(function (session) {
          return stripe.redirectToCheckout({ sessionId: session.id });
        })
        .then(function (result) {
          // If redirectToCheckout fails due to a browser or network
          // error, you should display the localized error message to your
          // customer using error.message.
          if (result.error) {
            alert(result.error.message);
          }
        })
        .catch(function (error) {
          console.error("Error:", error);
        });
    } 
    </script>
<!--End Pricing-->

{% endblock %}

Add a pricing page. Use the Django template language to add the model objects to the template dynamically.

Grab the CSRF token for posting, then load in your Stripe API key. Then add selectPlan(). This function takes in the unique Stripe API ID for each product. 

Fetch the /create-checkout-session URL added in the next step and post the product_id as a JSON. 

Then return the JSON response and a Stripe redirect to a Stripe checkout page. 

 

Update the urls.py

env > mysite > main > urls.py

from django.urls import path
from . import views

app_name = "main"   


urlpatterns = [
    path("", views.homepage, name="homepage"),
    path("dashboard", views.dashboard, name="dashboard"),
    path("register", views.register_request, name="register"),
    path("login", views.login_request, name="login"),
    path("logout", views.logout_request, name= "logout"),
    path("create-checkout-session", views.create_checkout_session, name="create checkout session"),
    path('success', views.success_request, name="success"),
    path('cancel', views.cancel_request, name="cancel"),
    path('pricing', views.pricing, name="pricing"),

]

Update the urls.py with all of the necessary URL patterns. The create-checkout-session, success_request, and cancel_request all URL patterns for Stripe monthly subscriptions.

 

Update the views.py

env > mysite > main > views.py

from django.shortcuts import  render, redirect
from .forms import NewUserForm
from django.contrib.auth import login, authenticate, logout
from django.contrib import messages
from django.contrib.auth.forms import AuthenticationForm
from .models import Subscription
from django.http import HttpResponse, JsonResponse
import stripe
import json


# Create your views here.
def homepage(request):
	return render(request = request, template_name="main/home.html")

def pricing(request):
	subscription = Subscription.objects.all()
	return render(request = request, template_name="main/pricing.html", context={"subscription":subscription})

def dashboard(request):
	return render(request = request, template_name="main/dashboard.html")


def register_request(request):
	if request.method == "POST":
		form = NewUserForm(request.POST)
		if form.is_valid():
			user = form.save()
			login(request, user)
			messages.success(request, "Registration successful." )
			return redirect("main:pricing")
		messages.error(request, "Unsuccessful registration. Invalid information.")
	email = request.GET.get("email")
	data = {"email":email}
	form = NewUserForm(initial=data)
	return render (request=request, template_name="main/register.html", context={"register_form":form})

def login_request(request):
	if request.method == "POST":
		form = AuthenticationForm(request, data=request.POST)
		if form.is_valid():
			username = form.cleaned_data.get('username')
			password = form.cleaned_data.get('password')
			user = authenticate(username=username, password=password)
			if user is not None:
				login(request, user)
				return redirect("main:dashboard")
			else:
				messages.error(request,"Invalid username or password.")
		else:
			messages.error(request,"Invalid username or password.")
	form = AuthenticationForm()
	return render(request=request, template_name="main/login.html", context={"login_form":form})

def logout_request(request):
	logout(request)
	messages.info(request, "You have successfully logged out.") 
	return redirect("main:homepage")

def create_checkout_session(request):
	if request.method == "POST":
		YOUR_DOMAIN = 'http://127.0.0.1:8000'
		stripe.api_key = 'sk_test' #replace with your Stripe API key
		data = json.loads(request.body)

		customer = stripe.Customer.create(
			email=request.user.email)
		try:
			checkout_session = stripe.checkout.Session.create(
				payment_method_types=['card'],
				line_items=[
					{
						'price': data["product_id"],
						'quantity': 1,
					}
				],
				mode='subscription',
				success_url=YOUR_DOMAIN + '/success',
				cancel_url=YOUR_DOMAIN + '/cancel',
				customer_email = customer.email,
				)
			return JsonResponse({'id': checkout_session.id})
		except Exception as e:
			return JsonResponse({'error': (e.args[0])}, status =400)
	return JsonResponse({'error':'No GET request allowed.'})

def success_request(request):
	messages.info(request, "You have successfully subscribed.") 
	return redirect("main:dashboard")

def cancel_request(request):
	messages.info(request, "Payment Failed. Please try again.") 
	return redirect("main:pricing")

Let's focus on the last three functions. create_checkout_session() loads in the JSON data from the pricing page, creates a stripe customer, then trys to create the Stripe checkout session. 

Declare the payment_method_types, the line_items from the JSON data, the mode, the success_url, cancel_url, and the customer_email. Lastly, return the checkout session ID.

If the subscription is successful, request success_request() and if the checkout session is canceled, request cancel_request().

 

Next Steps

That's it for this tutorial! Now you have a SaaS boilerplate template that you can integrate on and create your MVP.

The full project is available on the projects tab. It includes a dashboard.

Django Stripe Dashboard






Post a Comment
Join the community

2 Comments
Mark July 10, 2021, 2:22 a.m.

hello thanks for this nice contribue, but i don't see anything about forms.py and his NewUserForm object.

Jaysha replying to Mark July 12, 2021, 9:20 a.m.

Hi Mark, good catch! I've updated the article to include a link to the Django User Registration, Login, and Logout Guide on the site. It goes more in depth on how to add these login and register forms to your project.

2
Jaysha
Written By
Jaysha
Hello! I enjoy learning about new CSS frameworks, animation libraries, and SEO.