These docs are a work-in-progress and are currently undergoing a major rewrite!

Starter template

The Forge starter template is a GitHub repo with what we consider to be the best Django project layout.

It includes all of the public Forge packages, and focuses on a specific set of tools.

You can browse the repo itself on GitHub →

Tools

Poetry

Poetry is our preferred way to manage Python dependencies. You can check the Poetry documentation for more information, but here is what you need to know for daily use.

The files managed by Poetry are pyproject.toml and poetry.lock.

Poetry has a command to add a new dependency:

poetry add <package>

If the package you want is purely for development (ex. "pytest-cov") then you can use the dev flag:

poetry add pytest-cov --dev

Poetry stores the exact versions installed in poetry.lock. You should commit this file in your repo, because it is how you tell Heroku and other people on your team exactly what versions should be installed.

You can update your poetry.lock with:

poetry update

Tailwind CSS

Tailwind CSS is integrated through the forge-tailwind package.

This allows you to use Tailwind without actually installing Node or JavaScript dependencies!

In Forge we consider Tailwind to be the "official" way to style your site. By doing this, we can include styled templates in the Forge packages without requiring additional, custom CSS. Everything uses Tailwind which makes it easy to override templates.

Docker

We use Docker to run a local Postgres database during forge work.

We do not use Docker to run your Django/Python code. In our experience, it's easier to run those processes locally and introducing Docker can be a learning curve and layer of abstraction that does more harm than good. This does mean that you need to have Python 3.x installed on your machine and might occasionally need to pay attention to the exact version being used.

Heroku

Several parts of Forge are designed intentionally to work well with Heroku, and we anticipate building more in the future.

GitHub Actions

In our opinion, GitHub is the best place to host git repos and it integrates well with Heroku. GitHub Actions is also a great and low-cost/free CI solution for running your tests.

The only thing you need to use GitHub Actions is a file called .github/workflows/test.yml in your repo.

Models

The starter template comes with two sets of models that are useful for most projects.

The first is a custom users app with a User model. If you need to store custom data related to a user, you can put fields directly on this model. A custom user model is the recommended way to start a project according to the Django docs.

The second is a teams app, that includes the Team and TeamMembership models. Most projects inevitably need some type of "Team" concept, so it's one of the few places we like to provide a starting point. You can call it something else in your frontend HTML (ex. "Organization" or "Group"), but in general the Team is where you'll group users together and attach billing information.

You don't have to use the teams app if you know you aren't going to need it or prefer a different naming convention. We would strongly recommend you keep a custom users app/model though.

Users

We include two modifications to what the Django AbstractUser provides:

  • the email for a user must be unique (helps with login and duplication)
  • the uuid field is added via UUIDModel (helps with identifying users without exposing the standard auto-incrementing ID)

When you need to store something directly on a user, go ahead and write your code in the User model:

class User(AbstractUser, UUIDModel):
    email = models.EmailField(unique=True)

    # Add your own fields and methods here!

SignupView

The quickstart comes with all of the django.contrib.auth.urls enabled at the root (ex. "example.com/login/"). This includes:

  • login
  • logout
  • password changes
  • password resets

We also include a standard signup form to let users create an account using username, email address, and password.

urlpatterns = [
    # ...
    path("signup/", SignupView.as_view(), name="signup"),
    path("", include("django.contrib.auth.urls")),
]

Django signup form

When someone completes the signup form, they'll be redirected to log in using their new account. You can customize the SignupView to log them in immediately, show welcome messages, or start an email confirmation flow first.

Teams

When you're setting up the relationships in your project, most of the time you'll want to associate things with a "team" rather than a "user".

Whether you need teams right off the bat or plan to support them later, it's a good idea to tie "projects", billing, etc. to a Team even if that team is just a single User. Down the road, if you need to transfer a "project" from a user to their company team, you can effectively do a Team->Team transfer which is much simpler than changing the type of relationship from a User to a Team.

Likewise, if you plan to support subscriptions for individuals and businesses, you should consider both of those to be a Team on the backend which will simplify a lot of the billing logic.

Teams in Forge come with a few fields we think are useful, but you can customize is from there:

class Team(TimestampModel, UUIDModel):
    name = models.CharField(max_length=255)
    members = models.ManyToManyField(
        "users.User",
        related_name="teams",
        through="TeamMembership"
    )

    # Add your own fields and methods here!


class TeamRoles(models.TextChoices):
    ...


class TeamMembership(TimestampModel, UUIDModel):
    ...

Roles

When you add people to teams, we also provide a role field with basic ADMIN or MEMBER options. You can add to these as needed, but if you aren't using roles right away then just make everyone a MEMBER so you can add admin-specific features later!

team = Team.objects.create("A new team")
team_member1 = TeamMembership.objects.create(
    team=team,
    user=user,
    role=TeamRoles.MEMBER,
)

Templates

A new Forge project will come with a base.html to get you started. The look and feel of your app is totally up to you, and you can immediately start customizing it by changing the Tailwind classes.

The blocks in the default base.html are just suggestions. But you should keep a {% block content %} and {% block html_title %} as those are used in the Forge authentication templates.

The quickstart base.html looks something like this:

{% load static %}
<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">

    <title>{% block html_title %}{{ html_title }}{% endblock %}</title>

    <link href="{% static 'dist/tailwind.css' %}" rel="stylesheet">

    {% block head_scripts %}{% endblock %}
  </head>
  <body class="{% block body_class %}flex flex-col bg-gray-50 min-h-screen{% endblock %}">

    {% block header %}
    {% if request.user.is_authenticated %}
    <div class="flex bg-white border-b border-gray-200">
        <div class="container flex items-center justify-between px-4 py-4 mx-auto">
            <a class="px-3 py-2 text-gray-800 rounded hover:bg-gray-200" href="/">Home</a>
            <div class="flex space-x-2">
                <a class="flex items-center px-3 py-2 text-gray-800 rounded hover:bg-gray-200" href="{% url 'my_account' %}">
                    My Account
                </a>
                <a class="flex items-center px-3 py-2 text-gray-800 rounded hover:bg-gray-200" href="{% url 'logout' %}">
                    Log out
                </a>
            </div>
        </div>
    </div>
    {% endif %}
    {% endblock %}

    {% block content_container %}
    <div class="container flex flex-col flex-grow px-4 pt-4 mx-auto">
        {% block content %}{% endblock %}
    </div>
    {% endblock %}

    {% block footer %}
    <div class="container px-4 mx-auto mt-10">
        ...
    </div>
    {% endblock %}

    {% block footer_scripts %}{% endblock %}

  </body>
</html>

Repo layout

The app directory

Your Django code is all inside the app directory. You might have used repos before where all of your Django code it at the root of the repo, but our preference is to isolate the Django/Python code from other "meta" files like .gitignore, .github/, pyproject.toml, Procfile, etc. because when you're working in your app, you don't need access to those files to be right next to your Python code.

Don't rename the app directory — Forge expects it to be named app, it has no effect on your Django code, and it is one less thing to worry about "getting right" when starting a project.

Top-level urls.py, views.py, and settings.py

In Django you have a handful of top-level files that act as "entrypoints" to your app. But they're often buried an additional layer deep in your project (myproject/myproject/settings.py). We don't really see the point in that, and think that mentally it is easier to reason about a project hierarchy when your settings.py and urls.py (set as ROOT_URLCONF) are located right at the top (ex. app/settings.py). Additionally, there are some views like the homepage that don't really belong in a particular app, so a top-level app/views.py can be handy for those as well as view mixins that are shared across the project.

Top-level static and templates

Like the settings.py and other top-level files, we include a top-level static and templates directory.

The static will include your Tailwind CSS files and other project-wide assets like a logo.

The templates directory will include your base.html and other project-wide templates or pages like home.html.

A minimal settings.py

In Forge, your settings.py will be much shorter than what you'd get from a standard Django project. Because we know so much more about your project and how it runs, we set a number of default settings for you. You only need to add or customize beyond the Forge defaults:

from forge.default_settings import *

# Add to the default INSTALLED_APPS
INSTALLED_APPS = INSTALLED_APPS + [
    "projects",
    "teams",
    "users",
]

# Overwrite other Django settings
TIME_ZONE = "America/Chicago"

# And add your own settings
STRIPE_PRICE_ID = environ["STRIPE_PRICE_ID"]

In most Python code, import * is not a great idea. But in this particular case we think it's a great solution for extending settings.

Where's manage.py?!

We've completely removed the manage.py, wsgi.py, and asgi.py files from your repo. They are automatically loaded from Forge itself, because you rarely need to make any changes to them. If you do need to customize them, you can simply place a copy in your app directory.

The forge django <subcommand> is essentially an alias to python manage.py <subcommand>.