You can easily update the layout and colors of the registration, pricing and payment pages online directly on the live site through the tool palette on the right of the screen.

For more involved design and/or to fit your deployment process, You can also upload a zip file that contains your theme templates. Example uploading through the API:

curl -i -u *api_key*:  -X POST -F file=@*package*.zip https://*mydomain*/api/themes/

Zip-Package structure

You must use the following structure in the zip file to override the default theme. All files are optional. The default template will be used for a missing file.

  • *package*/
    • templates/
      All files under the templates directory are HTML or email layout templates. Some file names are reserved and will interact with the reverse-proxy logic (ex: login.html, register.html, pricing.html) otherwise all other HTML files will be served as static HTML pages.
    • public/ (optional)
      All files in the public directory are static assets (i.e. Plain Old Data) to be served to the browser client. These includes stylesheets (.css), javascripts (.js), fonts, images and other media.
Example (




When you are modifying root templates, like base.html for example, you may inadvertently break the dashboard and become unable to access this page to upload a fixed package.

It is recommended to make a backup copy of the original theme before you start updating it by clicking on the Download link.

In the advent you are unable to access this page to upload a fixed package, you could the restore the backup theme through an API call as such:

curl -i -u *api_key*:  -X POST -F file=@*package*.zip https://*domain*api/themes/

Thus also make sure to obtain your API key before uploading a new theme.

For convienience we separate the templates in two categories:

Subscriber facing pages

All the pages in this group are accessible to a subscriber, either to establish a relationship with the site by subscribing to a plan, or to manage her billing profile, active subscriptions, etc.

You will want to edit these templates first since they directly impact the look and feel a customer will have of your site.

Getting started: Home page

home landing page

You can upload HTML files with arbitrary names. Some file names are reserved and will interact with the reverse-proxy logic (ex: login.html, register.html, pricing.html) otherwise all other HTML files will be served as static pages.

A file named templates/my-content/hello-world.html, once the zip package has been uploaded will be available at URL /my-content/hello-world/.

We will start by creating an index.html for our site homepage (i.e. /).

Dynamically inserting logged in username

When we browse to our homepage, we see a Sign In link. Ideally we want the Sign In link to transform into a username contextual menu once a subscriber is signed in. To insert the menu dynamically we will update our index.html file as follow:

-<li><a href="">Sign In</a></li>
+{% if request|is_authenticated %}
+<li><a href="{{request|url_profile}}">{{user.username}}</a></li>
+<li><a href="{{request|url_logout}}">Sign Out</a></li>
+{% else %}
+<li><a href="{{request|url_login}}">Sign In</a></li>
+{% endif %}
Making text editable online

Editing HTML files and uploading them can be cumbersome. It is a process is also not suitable for other people in your business, with little or no technical knowledge.

We will want to make some text on the index.html page editable online. In order to do that we add an .editable class and a unique id to a paragraph.

-<p>Hello World</p>
+<p id="title" class="editable">Hello World</p>


  • all editable elements must contain an editable class.
  • all editable elements must have an UNIQUE id attribute.
  • To enable Markdown edition on an element, add a edit-markdown class.
  • To transform <IMG> and <VIDEO> nodes into placeholders, add a droppable-image class.

Getting real: Pricing page

displays the plans available for subscription.

Once all the static messaging pages have been designed, the first page interacting with the reverse proxy that we will customize is the pricing page.

First we wrap the HTML displaying a single plan into a for loop, replacing hardcoded plan title, price amount and description with their actual value taken form the database.

+{% for plan in plan_list %}
+  <h2>{{plan.title}}</h2>
+  <h2>{{plan.period_amount}}</h2>
+  <p>{{plan.description}}</p>
+{% endfor %}

With a few plans created for our site, we see they show up on the pricing page. The prices are displayed in cents and the description are not HTML formatted. In order to fix that, we add some filters.

+{% for plan in plan_list %}
+  <h2>{{plan.period_amount|htmlize_money}}</h2>
+  <p>{{plan.description|md}}</p>
 {% endfor %}

It looks good now! It is time to add a subscribe button so users can subscribe to a plan.

+<form method="post" action=".">
+  <input type="hidden" name="csrfmiddlewaretoken" value="{{csrf_token}}">
 {% for plan in plan_list %}
+  <button type="submit" name="submit" value="{{plan.slug}}">Subscribe</button>
 {% endfor %}

Registering the first user

With a landing page and a pricing page, next we want visitors to be able to register to the site. We will create a template page accounts/register.html

Registering a user is a simple form asking for at least an email and password. A few other optional information can also be asked on registration.

First, as with all other form pages, we make sure the form declares method="post" and action=".", as well as passing the authenticity token back to the server by adding <input type="hidden" name="csrfmiddlewaretoken" value="zU4YXV51TlGJHoentFRIQ5Ev7usfPIXlxq57dcYB7MvtVbAUyl3ODcakhZRi4TAG">.

Many times, registration and login pages are inserted into a page flow, moving from a public page to a page that requires an authenticated user. It would be very annoying to a user that provides credentials to be redirected to a standard landing page instead of the page she intended to reach when she clicked on a link on the public page. The page context always contains the intended redirect url as variable next. We just need to append it to the action.

+<form method="post" action=".{% if next %}/?next={{next}}{% endif %}">
+  <input type="hidden" name="csrfmiddlewaretoken" value="{{csrf_token}}">
+<input class="btn btn-primary" name="commit" type="submit" value="Sign up">

Inside the form, we create input fields for email, first name, last name, password and password confirmation.

 <form method="post" action=".">
  <input type="hidden" name="csrfmiddlewaretoken" value="XUZvbnSdgphC2HFSEz5ujnguXzaSO30uVq0ErELNuQ6mgu1pJfhA6uMj74zV3eDP">
+ <input name="email" type="email">
+ <input name="first_name" type="text">
+ <input name="last_name" type="text">
+ <input name="password" type="password">
+ <input name="password2" type="password">

When we try this form, we quickly realize that when something wrong happens on submit, we get back a blank form. No error message is displayed. This is a little inconvienient. We will add some HTML at the top of the <body> to display error messages back to the user.

+<div class="messages">
+{% for message in messages %}
+  <p class="{{message.tags}}">{{message}}</p>
+{% endfor %}

We also add code to display field specific errors. For example, here is how errors related to the e-mail field are displayed back to the user.

 <form method="post" action=".">
 <input type="hidden" name="csrfmiddlewaretoken" value="{{csrf_token}}">
 <input name="email" type="email">
+{% for error in %}
+  <span>{{error}}</span>
+{% endfor %}

At this point, errors when the form is submitted are displayed back to the user. The form fields are blank though. We would like users to avoid re-typing correct information they already entered. In order to do that we add the following code:

 <form method="post" action=".">
 <input type="hidden" name="csrfmiddlewaretoken" value="{{csrf_token}}">
-  <input name="email" type="email">
+  <input name="email" type="email" {{|value_attr}}>
   {% for error in %}
   {% endfor %}

Going forward, all forms can be built on the same model. We will thus only indicate the fields that must be present in order for the backend logic to behave correctly. In the case of registration, the reference documentation looks like the following

Register a user account
Form fields
Unique email address of the user to communicate outside interaction on the web site.
(optional) First name of the user. If you wish to use a single field for the full_name instead of a first_name/last_name pair, you can do so. Either a full_name or a first_name/last_name pair must be specified.
(optional) Last name of the user. Either a full_name or a first_name/last_name pair must be specified.
(optional) Full name of the user. If a full_name is specified instead of a first_name/last_name pair, the system will internally split it into a first_name/last_name pair on a natural boundary. Either a full_name or a first_name/last_name pair must be specified.
Password required to login as the user
Password confirmation. Password and password confirmation entered by a user must match.
Unique username under which the user is registered.

Authentication pages

Now that we have built a custom registration template, we keep the pace and customize the remainder of the authentication pages (login, password reset, etc.) following the same technique.

accounts/activate.html (example)
Activate an account. Verification emails are sent with a unique link to an activate page. By clicking on the link the user will verify her email and activate the account.
User that was activated. To display the activated user email, insert the following code into the template:
accounts/login.html (example)
Authenticate a user and create a session
Form fields
Unique username or email address the user registered with.
Password associated to the user
accounts/recover.html (example)
Send an e-mail to recover a forgotten password.
Form fields
Email address the user registered with.
accounts/reset.html (example)
Recover a forgotten password. Password reset emails are sent with a unique link to a recover page. By clicking on the link and landing on the recover page, the user will have the ability to enter a new password.
Form fields
Password required to login as the user
Password confirmation. Password and password confirmation entered by a user must match.

Email notifications

To communicate with the user through e-mail, the system sends notifications. Email templates are structured in 3 blocks:

  • subject
  • html_content
  • plain_content (optional)

The subject block contains the subject line of the e-mail. The html_content block contains the content of the e-mail, HTML formatted. The plain_content block contains the content of the e-mail in plain text. In case there are no plain_content block, the system will create it as a stripped down version of the html_content block.

The first e-mail notification we will customize is the password reset e-mail.

notification/password_reset.eml (example)
Email sent to a user with a clickable link to reset her password.

Here in its bare bone version with a subject and html_content blocks.

{% block subject %}
Reset your password on

{% block html_content %}
Please reset your password on by visiting the following


Link is valid for {{expiration_days}} days.
{% endblock %}

Here the context contains back_url, the URL generated by the system to reset a user password. The expiration_days contains the number of days the link is valid.

notification/verification.eml (example)
Email sent to a user with a clickable link to verify her email address.
Link the user must click on to verify her e-mail address.
Number of days the e-mail verification link is valid.

Checkout pipeline pages

Pages to enter a credit card, number of pre-paid periods, etc.

Once a user can register and do the basic login/logout operations and our pricing page is up and running, we move on to the checkout pipeline. The checkout pipeline can be as simple as a form to enter a credit card number or as complex as a multi-page experience with multi-periods at a discount and group billing.

Customizing the saas/billing/cart.html template consists of adding a form with purposefully named input fields, and including a saas/_payment_head.html and saas/_payment_body.html boiler plate in the <head> and <body> nodes respectively.

The minimum form should look like:

<form id="payment-form" method="post" action=".{% if next %}/?next={{next}}{% endif %}">
    <input type="hidden" name="csrfmiddlewaretoken" value="{{csrf_token}}">
    <div id="card-use">
        <!-- BE CAREFULL: Do not add name="" to the #card-number,
             #card-cvc, #card-exp-month, #card-exp-year input nodes,
             else values will hit the server and break PCI compliance. -->
        <div class="form-group">
            <input id="card-number" type="text" autocomplete="off" />
        <div class="form-group">
            <input id="card-cvc" type="text" autocomplete="off" />
        <div class="form-group">
            <input id="card-exp-month" type="text" autocomplete="off" />
        <div class="form-group">
            <input id="card-exp-year" type="text" autocomplete="off" />
        <div class="form-group">
            <input name="card_name" type="text" />
        <div class="form-group">
            <input name="card_address_line1" type="text" />
        <div class="form-group">
            <input name="card_city" type="text" />
        <div class="form-group">
            <input name="region" type="text" />
        <div class="form-group">
            <input name="card_address_zip" type="text" />
        <div class="form-group">
            <input name="country" type="text" />

We then add the following boiler plate code in the <head> node:

<script src="//" type="text/javascript"></script>
<meta name="csrf-token" content="{{csrf_token}}">
{% include "saas/_payment_head.html" %}

We add the following boiler plate code in the <body> node:

{% include "saas/_payment_body.html" %}

Of course, to display error messages, we follow a similar pattern as seen earlier in: Registering the first user.

For simplicity and consistency, both saas/billing/cart-periods.html and saas/billing/cart-seats.html can be directly derived from the saas/billing/cart.html. All three templates share the same context.

Charge Receipt

HTML page displayed once the charge is confirmed.
PDF printable version of the charge confirmation.
Email sent to the user to confirm the charge.

Once the checkout pipeline is completed, a charge on Stripe and receipt are generated. Typically we want to add a link on the charge receipt so that a user can directly browse to the next part of the site that makes sense in the on-boarding.

Profile, billing and subscription pages

Update password for a user.
Update profile information for a user (i.e. email, username, etc.)
This page shows a statement of subscription orders, charge created and payment refunded.
Displays the balance due by a subscriber with the option to charge a card for balance due.
Update the Credit Card information associated to a subscriber.
Update contact information of an organization.
List plans the organization is subscribed to.
List managers (or contributors) for an organization.

Extra subscriber visible pages

Access forbidden
Not found
List all agreements and policies for a provider site. This typically include terms of service, security policies, etc.
Show a single agreement (or policy) document. The content of the agreement is read from saas/agreements/

This page helps User create a new Organization. By default, the request user becomes a manager of the newly created entity.

User and Organization are separate concepts links together by manager and contributor relationship.

The complete User, Organization and relationship might be exposed right away to the person registering to the site. This is very usual in Enterprise software.

On the hand, a site might decide to keep the complexity hidden by enforcing a one-to-one manager relationship between a User (login) and an Organization (payment profile).

Provider facing pages

Provider facing pages are only accessible to managers and contributors of a site. They are used to assess the performance of the business, set pricing strategy, and help with customer support.

Since regular subscribers do not interact with these pages, you might be happy with DjaoDjin's default dashboard. None-the-less if your business or esthetic sense requires to update those templates, you can do so in the same way as the subscriber facing pages.

Manage discounts (i.e. registration codes)
Manage recurring pricing plans
Manage information to transfer funds to the provider bank.
List payments made to a provider or funds transfered to the provider bank.
Initiate transfer of funds to a provider bank. This is typically done automatically by Stripe unless you specified you want to do it manually.
High-level dashboard for a quick glance of the business in real-time.
Performance of Plans for a time period (as a count of subscribers per plan per month)
Reports cash flow and revenue in currency units.
List active and churned subscribers.

Public static assets

Public assets (images, js, css) can either be served through your app in the container, or installed through the *package*.zip. In both cases, they will be available to both, the djaodjin theme templates and your app. The only technical difference between putting the assets in the container vs. the zip is how the asset cache is filled ().

Public static assets (images, js, css) are all files which are downloaded by the client as-is. For best performance you can use a Content Delivery Network (CDN) to host those public static assets.

DjaoDjin does some basic caching of public static assets to serve your site faster. You can fill the assets cache directly by adding a public/ directory into the *package*.zip.

You can also server the public assets through your app container.

In both cases, static assets uploaded in *package*.zip or served through an app container, they will be available in the templates at the same URL location.

To reference static assets from within a template, we use use the asset template tag as such:

-<link rel="stylesheet" media="screen" href="/static/css/bootstrap.css" />
+<link rel="stylesheet" media="screen" href="{{'static/css/bootstrap.css'|asset}}" />