How to Create a Dynamic Pricing Table for Your SaaS Application in Django
Discover the step-by-step process to create a dynamic pricing table for your Django-based SaaS application.

Lover of coding, software development/engineering, indie hackers podcast/community, start-ups, music, guitar, technology, fitness, running, biking, learning new things, travel, the beach, and hiking/mountains.
As a kid I had too many interests. I grew up playing soccer from an early age and played through college! Sports and being a part of a team was always part of my DNA. Not only did I value sports and competition but I loved music, art, drawing, animation, film, computers, math, and learning.
Once I hit college, the decision to choose my life path was paralyzing, and ultimately led me down many different paths. I explored economics, finance, psychology, philosophy, statistics, communications, and marketing. I graduated with a finance degree and thought the data science, statistics, and the trends and patterns would be a fun career, however my first entry level job in the industry discouraged me to continue in the industry and to explore other paths.
I always had an itch to build and start something on my own or with family. Growing up I started a lawn mowing business, shoveling business, lemonade stands, and small Wordpress websites. I loved the creativity of coming up with ideas on how to help people and make money at the same time.
I realized I loved technology, and seeing what could be created and started with technology really urged me to start down the path of learning how to code. My brother and I had an idea for a college social network (similar to Facebook), geared solely towards education and only for students at your college. We wanted to give students the ability to meet people on campus, finding work, organize course material, share notes and materials, find extracurricular activities, sell textbooks and furniture. I took it upon myself to learn how to build something like that. Basically taking an idea and making it happen. I learned about software development, coding languages, web frameworks, startups, marketing all on my own.
I took online free courses, watched videos and tutorials about Django, Python, Javascript, HTML, and databases. I absolutely loved everything about the process. Seeing my work come to life and seeing people use what I created. It satisfied everything that I enjoyed growing up. The creativity, the design, artwork, coming up with a business, learning new things at my own pace, however I learned best, and working with my brother. I did all this while working full-time at a financial institution during my nights and weekends.
We finally launched StudentGrounds, however after a year and 200 user signups later it slowly died down. This experience of taking an idea and learning everything needed to make it a reality basically propelled my interest in learning how to code and do that full time. I learned all about computer science, taking a certificate course at night at a local university. I started another project idea on the side for an event management application for my father's youth soccer tournament, and started applying to every technology company I could think of. I ultimately got my first software engineer job at a small start up in Boston as an apprentice/intern and learned on the job before getting my first full-time software engineer position at a large Boston e-commerce company. My goal there was to learn as much as I could from season professionals, and learning how the corporate world works in terms of software development.
My ultimate goal is to create something on my own doing something I love, as well as enjoy life, and give back to others through education.
Right now I am a full-time Software Engineer with 6 years in the marketing tech space, trying to finish a SaaS boilerplate so that I can spin up any web application for any idea at the click of a button, which will then set me up for my next idea, IdeaVerify, an automated way to verify/validate you're SaaS application idea before actually starting to code and wasting many hours and years developing something that no one would use.
This blog is about my journey navigating the software engineering world, without a CS degree, building in public, keeping record of what I learned, sharing my learnings and at the same time giving back to others, teaching them how to code and giving helpful hints and insights. I am also using this blog to showcase other sides of me such as art, music, writing, creative endeavors, opinions, tutorials, travel, things I recently learned and anything else that interests me. Hope you enjoy!
I’m building Ideaverify! Ideaverify will help automate idea validation for #indiehackers! Each user will be able to create a landing page tied to their own subdomain, with all the popular sections of your typical SaaS landing page. One section I thought I’d share in public so others can learn along the way, is how to create a dynamic pricing table for their SaaS, and how to store and retrieve the data in Django.

You can use an existing django project and add the code below to it. Or start from scratch.
If starting from scratch:
cd DesktopMake sure Django is installed. If not
pip install DjangoNext
django-admin startproject landingpagecd landingpageand the belowpip install virtualenvpython3.8 -m venv envsource env/bin/activatepip install DjangoFirst let’s add the models in our
models.pyfile.
from django.db import models
class PricingDescription(models.Model):
description = models.CharField(max_length=256)
order = models.PositiveIntegerField()
def __str__(self):
return self.description
class Meta:
ordering = ["order", "pk"]
class Pricing(models.Model):
# payment interval enum
class Interval(models.IntegerChoices):
MONTHLY = 1, "MONTHLY"
YEARLY = 2, "YEARLY"
# usage type enum
class Type(models.IntegerChoices):
RECURRING_PAYMENT = 1, "RecurringPayment"
ONE_TIME_PAYMENT = 2, "OneTimePayment"
name = models.CharField(max_length=256)
descriptions = models.ManyToManyField(
to=PricingDescription, related_name="pricing_description"
)
price = models.PositiveIntegerField()
interval = models.PositiveSmallIntegerField(
choices=Interval.choices, null=True, blank=True
)
type = models.PositiveSmallIntegerField(
choices=Type.choices, default=Type.RECURRING_PAYMENT
)
order = models.PositiveIntegerField()
def __str__(self):
return self.name
class Meta:
ordering = ["order", "pk"]
class LandingPage(models.Model):
primary_hero_title = models.CharField(max_length=256)
pricing = models.ManyToManyField(to=Pricing, related_name="landingpage_pricing")
def __str__(self):
return self.primary_hero_title
These 3 models will allow us to define the pricing data and associate it to a landing page.
Make sure to add folder static_files inside root directory.
Inside static_files add a css folder.
Inside your root directory add a templates directory.
Project structure should look like the below.

Now run python manange.py makemigrations (should see No changes detected if this is a brand new project).
Now run python manage.py migrate .

Now run python manage.py makemigrations landingpage and then python manage.py migrate landingpage
2. Next let’s add some admin tables in our admin.pyso we can add data through the UI.
from django.contrib import admin
from .models import (
LandingPage,
Pricing,
PricingDescription,
)
class LandingPageAdmin(admin.ModelAdmin):
model = LandingPage
list_display = (
"primary_hero_title",
)
admin.site.register(LandingPage, LandingPageAdmin)
class PricingAdmin(admin.ModelAdmin):
model = Pricing
list_display = ("id", "name")
admin.site.register(Pricing, PricingAdmin)
class PricingDescriptionAdmin(admin.ModelAdmin):
model = PricingDescription
list_display = ("id", "description")
admin.site.register(PricingDescription, PricingDescriptionAdmin)
3. Now let’s add to our views.py.
from django.shortcuts import render
from .models import LandingPage
def index(request):
landing_page = LandingPage.objects.all()[0] # assuming you have 1 landing page.
context = {"landing_page": landing_page}
return render(request, "index.html", context)
4. Now add an index.html inside your templates folder if not already created with some html code like so.
{% load static %}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>landingpage</title>
<meta content="width=device-width, initial-scale=1" name="viewport" />
<link href="{% static 'css/landingpage.css' %}" rel="stylesheet"
type="text/css" />
</head>
<body>
<section class="rl_section_pricing18">
<div class="rl-padding-global-2">
<div class="rl-container-large-2">
<div class="rl-padding-section-large-2">
<div class="rl_pricing18_component">
<div class="rl_pricing18_heading-wrapper">
<h2 class="rl-heading-style-h2">Pricing plan</h2>
</div>
<div class="rl_pricing18_spacing-block-3"></div>
<div class="rl_pricing18_plans_{{landing_page.pricing.all|length}}">
{% for price in landing_page.pricing.all %}
<div class="rl_pricing18_plan">
<div class="rl_pricing18_plan-content">
<div class="rl_pricing18_plan-content-top">
<div class="rl_pricing18_price-wrapper">
<div class="rl-heading-style-h6">{{price.name}}</div>
<div class="rl_pricing18_spacing-block-4"></div>
<div class="rl-heading-style-h1-2">${{price.price}}<span
class="rl-heading-style-h4">/{% if price.interval == 1 %}mo
{% else%}yr{% endif %}</span></div>
<div class="rl_pricing18_spacing-block-4"></div>
</div>
<div class="rl_pricing18_spacing-block-5"></div>
<div class="rl_pricing18_feature-list">
{% for description in price.descriptions.all %}
<div class="rl_pricing18_feature">
<div class="rl_pricing18_icon-wrapper">
<div class="rl_pricing18_icon w-embed"><svg fill="none"
height=" 100%" viewbox="0 0 24 24" width=" 100%"
xmlns="http://www.w3.org/2000/svg">
<path
d="M19.8501 7.25012L9.2501 17.8501C9.15621 17.9448 9.02842 17.998 8.8951 17.998C8.76178 17.998 8.63398 17.9448 8.5401 17.8501L3.1501 12.4601C3.05544 12.3662 3.0022 12.2384 3.0022 12.1051C3.0022 11.9718 3.05544 11.844 3.1501 11.7501L3.8501 11.0501C3.94398 10.9555 4.07178 10.9022 4.2051 10.9022C4.33842 10.9022 4.46621 10.9555 4.5601 11.0501L8.8901 15.3801L18.4401 5.83012C18.6379 5.63833 18.9523 5.63833 19.1501 5.83012L19.8501 6.54012C19.9448 6.634 19.998 6.7618 19.998 6.89512C19.998 7.02844 19.9448 7.15623 19.8501 7.25012Z"
fill="currentColor"></path>
</svg></div>
</div>
<div class="rl-text-style-regular-2">{{ description.description }}</div>
</div>
{% endfor %}
</div>
<div class="rl_pricing18_spacing-block-6"></div>
</div>
<a class="rl-button-2 w-button" href="#">Get started</a>
</div>
</div>
{% endfor %}
</div>
</div>
</div>
</div>
</div>
</section>
</body>
</html>
5. Now let’s add some css to landingpage.css inside your static_files/css
.rl-button-2 {
border: 1px solid black;
background-color: black;
color: white;
text-align: center;
padding: 0.75rem 1.5rem;
font-family: system-ui, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto,
Noto Sans, Ubuntu, Cantarell, Helvetica Neue, Oxygen, Fira Sans, Droid Sans,
sans-serif;
font-size: 1rem;
}
.rl-text-style-regular-2 {
color: black;
margin-top: 0;
margin-bottom: 0;
font-family: system-ui, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto,
Noto Sans, Ubuntu, Cantarell, Helvetica Neue, Oxygen, Fira Sans, Droid Sans,
sans-serif;
font-size: 1rem;
font-weight: 400;
line-height: 1.5;
}
.rl-heading-style-h4 {
color: black;
margin-top: 0;
margin-bottom: 0;
font-family: system-ui, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto,
Noto Sans, Ubuntu, Cantarell, Helvetica Neue, Oxygen, Fira Sans, Droid Sans,
sans-serif;
font-size: 2rem;
font-weight: 700;
line-height: 1.3;
}
.rl-heading-style-h1-2 {
color: black;
margin-top: 0;
margin-bottom: 0;
font-family: system-ui, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto,
Noto Sans, Ubuntu, Cantarell, Helvetica Neue, Oxygen, Fira Sans, Droid Sans,
sans-serif;
font-size: 3.5rem;
font-weight: 700;
line-height: 1.2;
}
.rl-heading-style-h6 {
color: black;
margin-top: 0;
margin-bottom: 0;
font-family: system-ui, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto,
Noto Sans, Ubuntu, Cantarell, Helvetica Neue, Oxygen, Fira Sans, Droid Sans,
sans-serif;
font-size: 1.25rem;
font-weight: 700;
line-height: 1.4;
}
.rl-heading-style-h2 {
color: black;
margin-top: 0;
margin-bottom: 0;
font-family: system-ui, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto,
Noto Sans, Ubuntu, Cantarell, Helvetica Neue, Oxygen, Fira Sans, Droid Sans,
sans-serif;
font-size: 3rem;
font-weight: 700;
line-height: 1.2;
}
.rl-padding-section-large-2 {
padding-top: 7rem;
padding-bottom: 7rem;
}
.rl-container-large-2 {
width: 100%;
max-width: 80rem;
margin-left: auto;
margin-right: auto;
}
.rl-padding-global-2 {
padding-left: 5%;
padding-right: 5%;
}
.rl_pricing18_spacing-block-6 {
width: 100%;
padding-bottom: 2rem;
}
.rl_pricing18_icon {
flex-direction: column;
justify-content: center;
align-items: center;
width: 1.5rem;
height: 1.5rem;
display: flex;
}
.rl_pricing18_icon-wrapper {
color: black;
flex: none;
align-self: flex-start;
}
.rl_pricing18_feature {
grid-column-gap: 1rem;
grid-row-gap: 1rem;
display: flex;
}
.rl_pricing18_feature-list {
grid-column-gap: 1rem;
grid-row-gap: 1rem;
grid-template-rows: auto;
grid-template-columns: 1fr;
grid-auto-columns: 1fr;
padding-top: 0.5rem;
padding-bottom: 0.5rem;
display: grid;
}
.rl_pricing18_spacing-block-5 {
width: 100%;
padding-bottom: 2rem;
}
.rl_pricing18_spacing-block-4 {
width: 100%;
padding-bottom: 0.5rem;
}
.rl_pricing18_price-wrapper {
text-align: center;
}
.rl_pricing18_plan-content {
flex-direction: column;
justify-content: space-between;
height: 100%;
display: flex;
}
.rl_pricing18_plan {
border: 1px solid black;
flex-direction: column;
padding: 2rem;
display: flex;
}
.rl_pricing18_plans_3 {
grid-column-gap: 2rem;
grid-row-gap: 2rem;
grid-template-rows: auto;
grid-template-columns: 1fr 1fr 1fr;
grid-auto-columns: 1fr;
width: 100%;
display: grid;
}
.rl_pricing18_plans_2 {
grid-column-gap: 2rem;
grid-row-gap: 2rem;
grid-template-rows: auto;
grid-template-columns: 1fr 1fr;
grid-auto-columns: 1fr;
width: 100%;
display: grid;
}
.rl_pricing18_spacing-block-3 {
width: 100%;
padding-bottom: 5rem;
}
.rl_pricing18_spacing-block-2 {
width: 100%;
padding-bottom: 1.5rem;
}
.rl_pricing18_spacing-block-1 {
width: 100%;
padding-bottom: 1rem;
}
.rl_pricing18_heading-wrapper {
text-align: center;
width: 100%;
max-width: 48rem;
}
.rl_pricing18_component {
flex-direction: column;
align-items: center;
display: flex;
}
@media screen and (max-width: 991px) {
.rl-heading-style-h4 {
font-size: 1.75rem;
}
.rl-heading-style-h1-2 {
font-size: 3.25rem;
}
.rl_pricing18_plans_1 {
grid-template-columns: 1fr;
}
.rl_pricing18_spacing-block-3 {
padding-bottom: 4.5rem;
}
.rl-heading-style-h2 {
font-size: 2.75rem;
}
.rl-padding-section-large-2 {
padding-top: 6rem;
padding-bottom: 6rem;
}
}
@media screen and (max-width: 767px) {
.rl-heading-style-h4 {
font-size: 1.5rem;
line-height: 1.4;
}
.rl-heading-style-h1-2 {
font-size: 2.5rem;
}
.rl-padding-section-large-2 {
padding-top: 4rem;
padding-bottom: 4rem;
}
.rl_pricing18_spacing-block-6,
.rl_pricing18_spacing-block-5 {
padding-bottom: 1.5rem;
}
.rl_pricing18_plan {
padding-left: 1.5rem;
padding-right: 1.5rem;
}
.rl_pricing18_plans_1 {
grid-template-columns: 1fr;
}
.rl_pricing18_spacing-block-3 {
padding-bottom: 3rem;
}
.rl_pricing18_spacing-block-2 {
padding-bottom: 1.25rem;
}
.rl_pricing18_spacing-block-1 {
padding-bottom: 0.75rem;
}
.rl-heading-style-h2 {
font-size: 2.25rem;
}
}
6. Now let’s add a urls.py with the below.
from django.conf import settings
from django.conf.urls.static import static
from django.contrib import admin
from django.urls import path
from .views import *
urlpatterns = [
path("admin/", admin.site.urls),
path("index/", index),
] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
7. Make sure your settings.py has been set up and added.
import os
from pathlib import Path
#########################################
## SITE_NAME - change to your own ##
#########################################
SITE_NAME = "Landing page"
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
BASE_DIR = Path(__file__).resolve().parent.parent
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = "your_secret_key"
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
#########################################
## Application Definition ##
#########################################
INSTALLED_APPS = [
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
"landingpage",
]
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
ROOT_URLCONF = "landingpage.urls"
TEMPLATES = [
{
"BACKEND": "django.template.backends.django.DjangoTemplates",
"DIRS": [
BASE_DIR,
"templates/",
],
"APP_DIRS": True,
"OPTIONS": {
"context_processors": [
"django.template.context_processors.debug",
"django.template.context_processors.request",
"django.contrib.auth.context_processors.auth",
"django.contrib.messages.context_processors.messages",
],
},
},
]
WSGI_APPLICATION = "landingpage.wsgi.application"
#########################################
## Database ##
#########################################
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
}
}
#########################################
## Password Validation ##
#########################################
AUTH_PASSWORD_VALIDATORS = [
{
"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
},
{
"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
},
{
"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",
},
{
"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",
},
]
#########################################
## Internationalization ##
#########################################
LANGUAGE_CODE = "en-us"
TIME_ZONE = "America/New_York"
USE_I18N = True
USE_L10N = True
USE_TZ = True
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
STATIC_ROOT = os.path.join(BASE_DIR, "staticfiles")
STATIC_URL = BACKEND_URL + "/static/"
STATICFILES_DIRS = [
os.path.join(BASE_DIR, "static_files"),
]
8. Now run python manage.py createsuperuser and fill in your username, email, and password to sign in to your Django Admin UI
9. Run python manage.py runserver
10. Go to http://127.0.0.1:8000/admin/
11. Log in with your username and password.
12. Go to http://127.0.0.1:8000/admin/landingpage/pricingdescription/
13. Add a description for Basic, Pro and Premium.

14. Go to http://127.0.0.1:8000/admin/landingpage/pricing/
15. Add 3 Prices with your descriptions. One for Basic, Pro, and Premium.



Your pricing table should have 3 prices now.

16. Now go to http://127.0.0.1:8000/admin/landingpage/landingpage/ and add a landing page.

17. You can now view the dynamic Pricing table by going to http://localhost:8000/index/

That’s it! Hope you enjoyed and found this helpful.
If you liked this tutorial and you enjoy learning about coding, building in public, indiehackers, automation, start ups or entrepreneurship, follow me here:
https://indiehackers.com/rcmisk
Sign up for my newsletter to keep track of my progress and to learn along the way!






