This is the abridged developer documentation for SaaS Pegasus
# SaaS Pegasus Documentation
> Everything you need to know about setting up and configuring Pegasus for your project.
### Quicklinks [Section titled “Quicklinks”](#quicklinks) [Getting Started ](/getting-started/) [Configuration ](/configuration/) [Teams ](/teams/) [Subscriptions ](/subscriptions/) [Deployment ](/deployment/overview/) [Get Help From AI ](/ai/development/)
# Getting Started
> Complete setup guide for Pegasus projects with Docker or native Python, including database configuration and post-installation steps.
Here’s everything you need to start your first Pegasus project. ## Watch the video [Section titled “Watch the video”](#watch-the-video) Visual learner? The above video should get you going. Else read on below for the play-by-play. ## Create and download your project codebase [Section titled “Create and download your project codebase”](#create-and-download-your-project-codebase) If you haven’t already, you’ll need to [purchase a Pegasus License on saaspegasus.com](http://www.saaspegasus.com/licenses/). Then, [create a new project on saaspegasus.com](https://www.saaspegasus.com/projects/), following the prompts and filling in whatever configuration options you want to use for your new project. Make sure that the “license” field at the bottom is set. Once you’re done, [connect your project to Github](/github) or download your project’s source code as a zip file. **Note: it’s recommended to use the Github integration which will make future upgrades and changes to your project easier to manage.** ## Set up source control [Section titled “Set up source control”](#set-up-source-control) It is highly recommended to use git for source control. [Install git](https://git-scm.com/downloads) and then follow the instructions below: ### If using the Github integration [Section titled “If using the Github integration”](#if-using-the-github-integration) If you created your project on Github, you can use `git clone` to get the code. Get your git URL from the Github page and then run the following command, swapping in your user account and project id:
```bash
git clone https://github.com/user/project-id.git
```
### If using the Zip file download [Section titled “If using the Zip file download”](#if-using-the-zip-file-download) If you chose to use a zip file instead, unzip it to a folder where you want to do your development and then manually initialize your repository:
```bash
git init
git add .
git commit -am "initial project creation"
```
It is also recommended to create a `pegasus` branch at this time for future upgrades.
```bash
git branch pegasus
```
You can read [more about upgrading here](/upgrading). ## Install prerequisites [Section titled “Install prerequisites”](#install-prerequisites) The following prerequisites are needed to run the app in the recommended configuration: * [Docker](/docker#install-prerequisites) * [uv](/python/setup/#using-uv) * [Node and npm](/front-end/overview/#prerequisites-to-building-the-front-end) On Windows, you will also need to install `make`, which you can do by [following these instructions](https://stackoverflow.com/a/57042516/8207). ## Quick start [Section titled “Quick start”](#quick-start) Once you’ve installed the prerequisites, you can get up and running with the following commands:
```bash
make init
make dev # This is not required if running Docker in "full mode"
```
Open a browser and visit and you should see your application! Then skip ahead to the [post-install steps](/getting-started/#post-installation-steps). ## Manual setup [Section titled “Manual setup”](#manual-setup) The `make` quick start commands cover a lot for you. If you’d rather do everything manually, continue to the sections below. ### Enter the project directory [Section titled “Enter the project directory”](#enter-the-project-directory)
```bash
cd {{ project_name }}
```
You should see your project files, including a `manage.py` file. ### Set up your Python environment [Section titled “Set up your Python environment”](#set-up-your-python-environment) There are several ways of setting up your Python environment. See [this page](/python/setup) for information on choosing an option and setting up your environment. ### Install package requirements [Section titled “Install package requirements”](#install-package-requirements) With `uv`:
```bash
# with uv
uv sync
# or if using pip tools
pip install -r dev-requirements.txt
```
Note: if you have issues installing `psycopg2`, try installing the dependencies outlined in [this thread](https://stackoverflow.com/questions/22938679/error-trying-to-install-postgres-for-python-psycopg2) (specifically `python3-dev` and `libpq-dev`). On Macs you may also need to follow the instructions from [this thread](https://stackoverflow.com/a/58722268/8207). And specifically, run:
```bash
brew reinstall openssl
export LIBRARY_PATH=$LIBRARY_PATH:/usr/local/opt/openssl/lib/
```
### Create your .env file [Section titled “Create your .env file”](#create-your-env-file) If you installed with Github, you’ll have to create your `.env` file for your environment variables and secrets. You can do this from the example, by running:
```bash
cp .env.example .env
```
### Set up database [Section titled “Set up database”](#set-up-database) If you installed with Postgres, edit the `DATABASE_URL` value in `.env` with the appropriate username and password for connecting to your DB. You will also need to create a database for your project if you haven’t already. Assuming that your postgres admin user is named `postgres`:
```bash
createdb -U postgres -h localhost -p 5432 {{ project_name }}
```
Followed by the password for the postgres user. Or, using identity authentication:
```bash
sudo -u postgres createdb {{ project_name }}
```
### Create database migrations [Section titled “Create database migrations”](#create-database-migrations)
```bash
# with uv
uv run manage.py makemigrations
# or with normal venv
python ./manage.py makemigrations
```
### Run database migrations [Section titled “Run database migrations”](#run-database-migrations)
```bash
# with uv
uv run manage.py migrate
# or with normal venv
python ./manage.py migrate
```
### Run server [Section titled “Run server”](#run-server)
```bash
# with uv
uv run manage.py runserver
# or with normal venv
python ./manage.py runserver
```
### Build/run front end [Section titled “Build/run front end”](#buildrun-front-end)
```bash
npm install
npm run dev
```
For more details, see the [front end docs](/front-end/overview). ### Load your app [Section titled “Load your app”](#load-your-app) Open a browser and visit and you should see your application! Continue to the post-installation steps below. ## Post-installation steps [Section titled “Post-installation steps”](#post-installation-steps) Once up and running, you’ll want to review these common next-steps. ### Create a User [Section titled “Create a User”](#create-a-user) To create your first user account, just go through the sign up flow in your web browser. From there you should be able to access all built-in functionality and examples. ### Enable admin access [Section titled “Enable admin access”](#enable-admin-access) Use [the `promote_user_to_superuser` management command](/cookbooks/#use-the-django-admin-ui) to enable access to the Django Admin site. ### Confirm your site URL [Section titled “Confirm your site URL”](#confirm-your-site-url) For Stripe callbacks, email links, and JavaScript API clients to work, you must make sure that you have [configured absolute URLs correctly](/configuration/#absolute-urls). ### Set up your Stripe subscriptions [Section titled “Set up your Stripe subscriptions”](#set-up-your-stripe-subscriptions) If you’ve installed with subscriptions, you’ll want to set things up next. Head to the [subscriptions documentation](/subscriptions) and follow the steps there! ### Set up background tasks [Section titled “Set up background tasks”](#set-up-background-tasks) For the progress bar example to work---and to run background tasks of your own---you’ll need a Celery environment running. Head to [celery](/celery) and follow the steps there! ## Using the Makefile [Section titled “Using the Makefile”](#using-the-makefile) Pegasus ships with a self-documenting `Makefile` that will run common commands for you, including starting your containers, performing database operations, and building your front end. You can run `make` to list helper functions, and you can view the source of the `Makefile` file in case you need to add to it or run any once-off commands. Commands are also documented in your project’s AI rules files. You can add custom commands to the `Makefile` by editing `custom.mk`. ## Customize your application [Section titled “Customize your application”](#customize-your-application) At this point, Pegasus has installed scaffolding for all of the user management, authentication, and (optionally) team views and Stripe subscriptions, and given you a beautiful base UI template and clear code structure to work from. Now that you’re up and running it’s time for the fun part: building your new application! This can obviously be done however you like. Some examples of things you might want to do next include: * Customize your landing page and set up a pricing page * Start modifying the list of navigation tabs and logged-in user experience * Create a new django app and begin building out your data models in `models.py`. It’s recommended to use the [Pegasus CLI](https://github.com/saaspegasus/pegasus-cli/) for this. For some initial pointers on where to to make Pegasus your own, head on over to the [Customizations Page](/customizations). For the nitty-gritty details on setting up things like email, error logging, sign up flow, analytics, and more go to [Settings and Configuration](/configuration).
# Working with Python Packages (uv)
> Fast Python package management with uv using pyproject.toml and uv.lock files for adding, removing, and upgrading dependencies efficiently.
Recent versions of Pegasus use [uv](https://docs.astral.sh/uv/) to manage Python packages. It provides all the functionality of `pip-tools` while being much faster and offering more flexibility and features. ### Requirements Files [Section titled “Requirements Files”](#requirements-files) `uv` uses two files to manage requirements. The first is a `pyproject.toml` file, which contains the base list of packages. The `pyproject.toml` file also supports dependency groups, which are used for development and production requirements. `pyproject.toml` replaces the previous `requirements.in`, `dev-requirements.in`, and `prod-requirements.in` files. The second file is the `uv.lock` file. This file contains the pinned versions of dependencies that are used by the project’s environment. This file is automatically generated from the `pyproject.toml` file and *should not be edited by hand*. `uv.lock` replaces the previous `requirements.txt`, `dev-requirements.txt`, and `prod-requirements.txt` files. #### Adding or removing a package [Section titled “Adding or removing a package”](#adding-or-removing-a-package) To add or remove packages you can run the following commandss:
```bash
# native version
uv add
uv remove
# docker version
make uv add
make uv remove
```
If you’re using natively this is all you have to do! The command will update your `pyproject.toml` file, your `uv.lock` file, and sync your virtual environment. On Docker, you will have to also rebuild the container. You can do that with:
```bash
make build
make restart
```
The `make requirements` command can also be used to sync your `uv.lock` file and rebuild / restart your containers. #### Upgrading a package [Section titled “Upgrading a package”](#upgrading-a-package) You can upgrade a package with:
```bash
# native version - update the lockfile
uv lock --upgrade-package
# native version - update the lockfile and sync the virtual environment
uv sync --upgrade-package
# docker version
make uv "lock --upgrade-package wagtail"
```
You can upgrade *all* packages with:
```bash
# native version - update the lockfile
uv lock --upgrade
# native version - update the lockfile and sync the virtual environment
uv sync --upgrade
# docker version
make uv "lock --upgrade"
```
Like with adding packages, if you’re using Docker, you’ll have to rebuild and restart Docker containers for the updated environment to work:
```bash
make build
make restart
```
# Pegasus's Code Structure
> Understand Pegasus project organization with apps, static files, templates, and code formatting using pre-commit hooks and ruff.
## Overall structure [Section titled “Overall structure”](#overall-structure) This is the overall structure of a new Pegasus project: The first three directories are Python modules while the remaining ones are not. ## Your `{{project_name}}` module [Section titled “Your {{project\_name}} module”](#your-project_name-module) This is your Django project root directory. It’s where your settings, root urlconf and `wsgi.py` file will live. ## Your `apps` module [Section titled “Your apps module”](#your-apps-module) This is where your project’s apps will live. It is pre-populated with Pegasus’s default apps for you to further customize to your needs. The module starts with several apps, depending on your configuration. Here are some of the main ones: * `content` is where the [Wagtail CMS models](/wagtail) are configured. * `subscriptions` is for functionality related to [Stripe subscriptions](/subscriptions). * `users` is where your user models and views are defined. * `teams` is where [team models and views](/teams) are defined. * `utils` is a set of functionality shared across the project. * `web` contains utilities and components related to the generic views, layouts and templates. ## The `pegasus` module [Section titled “The pegasus module”](#the-pegasus-module) This is where the Pegasus examples live. In general, it is not expected that you’ll need to modify much in this module, though feel free to do so! ## The `requirements` folder [Section titled “The requirements folder”](#the-requirements-folder) This is where you define your project’s Python requirements. Requirements are managed using `pip-tools`. For more information on using it see [their documentation](https://github.com/jazzband/pip-tools). ## The `assets` folder [Section titled “The assets folder”](#the-assets-folder) This is where the source files for your site’s JavaScript and CSS live. These files are what you should edit to change your JS and CSS. See [front-end](/front-end/overview) for more information on how to compile these files. ## The `static` folder [Section titled “The static folder”](#the-static-folder) This folder contains your project’s static files, including the compiled output files from the `assets` folder as well as images. ## The `templates` folder [Section titled “The templates folder”](#the-templates-folder) This folder contains your project’s Django templates. There is one sub-folder for each application that has templates. The majority of the project’s base template layouts are in the `templates/web` folder. ## Code formatting [Section titled “Code formatting”](#code-formatting) For projects that have enabled the `Autoformat code` option, the code will have been formatted using [ruff](https://github.com/astral-sh/ruff)—a drop-in replacement for [black](https://black.readthedocs.io/en/stable/) and [isort](https://pycqa.github.io/isort/) that runs much faster than those tools. The project will also include [pre-commit](https://pre-commit.com/) as a dependency in the requirements file as well as the `.pre-commit-config.yaml` file in the root directory. pre-commit is a tool for managing pre-commit hooks - which can be used to ensure your code matches the correct format when it’s committed. After installing the project dependencies you can install the pre-commit hooks:
```bash
$ pre-commit install --install-hooks
pre-commit installed at .git/hooks/pre-commit
```
The default configuration that ships with Pegasus will run `ruff` and `ruff-format` prior to every Git commit. If there are fixes that are needed you will be notified in the shell output. ### pre-commit Usage [Section titled “pre-commit Usage”](#pre-commit-usage) **Manually running hooks**
```bash
# run all hooks against currently staged files
pre-commit run
# run all the hooks against all the files. This is a useful invocation if you are using pre-commit in CI.
pre-commit run --all-files
```
**Temporarily disable hooks** See For more information on using and configuring pre-commit check out the [pre-commit docs](https://pre-commit.com/#quick-start) ### Tool configurations [Section titled “Tool configurations”](#tool-configurations) The configuration for the tools can be found in the [`pyproject.toml`](https://black.readthedocs.io/en/stable/usage_and_configuration/the_basics.html#what-on-earth-is-a-pyproject-toml-file) file, using the same syntax as `black`. For the most part the default black/ruff formats have been preserved, with a few updates, for example, increasing the line length to 120. You can find more information about these values in the [ruff README](https://github.com/astral-sh/ruff?tab=readme-ov-file#configuration). ### Upgrading [Section titled “Upgrading”](#upgrading) See [this cookbook](/cookbooks/#migrating-to-auto-formatted-code) for guidance on how to enable code formatting on an existing Pegasus project.
# Settings and Configuration
> Configure Django settings, environment variables, email providers, social authentication, Stripe payments, and production deployments.
This section describes some of the settings and configuration details you can change inside Pegasus. ## Settings and environment files [Section titled “Settings and environment files”](#settings-and-environment-files) Pegasus uses environment variables and `django-environ` to manage settings. You *can* modify values directly in `settings.py`, but the recommended way to modify any setting that varies across environments is to use a `.env` file. Out-of-the-box, Pegasus will include multiple `.env` files for your settings: **`.env` is for development in either a native or a Docker-based environnment.** It will be picked up by default if you run `./manage.py runserver` or `docker compose start`. If you need to swap between these environments you might need to modify a few variables in this file---in particular the database and redis URLs. The `.env` is typically not checked into source control (since it may include secrets like API keys), so is included in the `.gitignore`. **`.env.example` is an example file.** It is not used for anything, but can be checked into source control so that developers can use it as a starting point for their `.env` file. Projects downloaded as zip files will include a `.env` file, but projects created or pulled from Github will typically only include a `.env.example` file, so you will need to copy this file locally to run your development server. *Note: Pegasus versions prior to 2024.3 also included a `.env.docker` file. This has been merged with the `.env` file.* ### Settings environment precedence [Section titled “Settings environment precedence”](#settings-environment-precedence) Most settings are configured in the form:
```python
SOME_VALUE = env('SOME_VALUE', default='')
```
As mentioned above, *it is recommended to set these values in your environment / `.env` file*, which will always work as expected. The environment takes precedence over the default if it’s set---even if it is set to an empty value. This can lead to confusing behavior. For example, if in your `.env` file you have this line:
```dotenv
SOME_VALUE=''
```
And in your `settings.py` you provide a default:
```python
SOME_VALUE = env('SOME_VALUE', default='my value')
```
The default will be ignored, and `SOME_VALUE` will be an empty string. To fix this, either *remove the value entirely from your `.env` file*, or *explicitly set the value in your `settings.py`* (instead of using the `default` argument). E.g.
```python
SOME_VALUE = 'my value'
```
## Project Metadata [Section titled “Project Metadata”](#project-metadata) When you first setup Pegasus it populated the `PROJECT_METADATA` setting in `settings.py` with various things like page titles and social sharing information. These settings can later be changed as you like by editing the setting directly:
```python
PROJECT_METADATA = {
'NAME': 'Your Project Name',
'URL': 'http://www.example.com',
'DESCRIPTION': 'My Amazing SaaS Application',
'IMAGE': 'https://upload.wikimedia.org/wikipedia/commons/2/20/PEO-pegasus_black.svg',
'KEYWORDS': 'SaaS, django',
'CONTACT_EMAIL': 'you@example.com',
}
```
See the [project metadata documentation](/page-metadata) for more information about how this is used. ## Absolute URLs [Section titled “Absolute URLs”](#absolute-urls) In most of Django/Pegasus, URLs are *relative*, represented as paths like `/account/login/` and so forth. But in some cases you need a complete URL, including the *protocol* (http vs https) and *server* (e.g. [www.example.com](http://www.example.com)). These are necessary whenever you use a link in an email, with an external site (e.g. Stripe API callbacks and social authentication), and in some places when APIs are accessed from your front end. ### Setting your site’s protocol [Section titled “Setting your site’s protocol”](#setting-your-sites-protocol) The *protocol* is configured by the `USE_HTTPS_IN_ABSOLUTE_URLS` variable in `settings.py`. You should set this to `True` when using https and `False` when not (typically only in development). ### Setting your server URL [Section titled “Setting your server URL”](#setting-your-server-url) When you first install Pegasus it will use the `URL` value from `PROJECT_METADATA` above to create a Django `Site` object in your database. The domain name of this `Site` will be used for your server address. If you need to change the URL after installation, you can go to the site admin at `admin/sites/site/` and modify the values accordingly, leaving off any http/https prefix. In development, you’ll typically want a domain name of `localhost:8000`, and in production this should be the domain where your users access your app. Note that this URL must match *exactly* what is in the browser address bar. So, for example, if you load your development site from `127.0.0.1:8000` instead of `localhost:8000` then that is what you should put in. **Example Development Configuration**  **Example Production Configuration**  ## Sending Email [Section titled “Sending Email”](#sending-email) Pegasus is setup to use [django-anymail](https://github.com/anymail/django-anymail) to send email via Amazon SES, Mailgun, Postmark, and a variety of other email providers. To use one of these email backends, change the email backend in `settings.py` to:
```python
EMAIL_BACKEND = 'anymail.backends.mailgun.EmailBackend'
```
And populate the `ANYMAIL` setting with the required information. For example, to use [Mailgun](https://www.mailgun.com/) you’d populate the following values:
```python
ANYMAIL = {
"MAILGUN_API_KEY": "key-****",
"MAILGUN_SENDER_DOMAIN": 'mg.{{project_name}}.com', # should match what's in mailgun
}
```
If you are in the EU you may also need to add the following entry:
```python
'MAILGUN_API_URL': 'https://api.eu.mailgun.net/v3',
```
The [anymail documentation](https://anymail.readthedocs.io/en/stable/) has much more information on these options. The following django settings should also be set:
```python
SERVER_EMAIL = 'noreply@{{project_name}}.com'
DEFAULT_FROM_EMAIL = 'you@{{project_name}.com'
ADMINS = [('Your Name', 'you@{{project_name}}.com'),]
```
See [Sending email](https://docs.djangoproject.com/en/stable/topics/email/) in the django docs for more information. ## User Sign Up [Section titled “User Sign Up”](#user-sign-up) The sign up workflow is managed by [django-allauth](https://allauth.org/) with a sensible set of defaults and templates. ### Social logins [Section titled “Social logins”](#social-logins) Pegasus optionally ships with “Login with Google/Twitter/Github” options. You’ll separately need to follow the steps listed on the [provider-specific pages here](https://docs.allauth.org/en/latest/socialaccount/providers/index.html) to configure things on the other side. These steps can sometimes be a bit involved and vary by platform. But will generally entail two steps: 1. Creating a new application / client on the service you want to use. 2. Adding the credentials to your environment (`.env`) file. See the Google guide below for an example you can follow. If you want to add a social login that’s not supported out of the box (e.g. Facebook/Meta or Apple), you can follow the existing patterns and configure things based on the allauth docs. If you need help setting this up feel free to get in touch! Additionally, see the resources below. #### Google OAuth Specific instructions [Section titled “Google OAuth Specific instructions”](#google-oauth-specific-instructions) 1. Register the application with google by following just the “App registration” section [here](https://docs.allauth.org/en/latest/socialaccount/providers/google.html). Note that the trailing slash for the “Authorized redirect URLs” is required. For example, assuming you are developing locally, it should be set to exactly `http://localhost:8000/accounts/google/login/callback/`. 2. Set the resulting client id and secret key in the `.env` file in the root of your project.
```dotenv
GOOGLE_CLIENT_ID="actual client id from the google console"
GOOGLE_SECRET_ID="actual secret id from the google console"
```
#### Other Social Setup Guides [Section titled “Other Social Setup Guides”](#other-social-setup-guides) The Pegasus community has recommended the following guides to set things up with specific providers: * [Github](https://python.plainenglish.io/django-allauth-a-guide-to-enabling-social-logins-with-github-f820239fb73f) ### Requiring email confirmation [Section titled “Requiring email confirmation”](#requiring-email-confirmation) Pegasus does not require users to confirm their email addresses prior to logging in. However, this can be easily changed by changing the following value in `settings.py`
```python
ACCOUNT_EMAIL_VERIFICATION = 'optional' # change to "mandatory" to require users to confirm email before signing in.
```
*Note: The email verification step will be skipped if using a social login.* ### Enabling sign in by email code [Section titled “Enabling sign in by email code”](#enabling-sign-in-by-email-code) Sign in by email code is controlled by the `ACCOUNT_LOGIN_BY_CODE_ENABLED` setting. You can enable / disable it in `settings.py`.
```python
ACCOUNT_LOGIN_BY_CODE_ENABLED=True
```
### Two-factor authentication [Section titled “Two-factor authentication”](#two-factor-authentication) Two-Factor authentication (2FA) is configured using the [allauth’s mfa](https://docs.allauth.org/en/latest/mfa/index.html) support. When using Two-Factor Auth with Pegasus, a new section is added to the user profile for enabling & configuring the OTP (one-time password) devices for the user. If a user has a Two-Factor device configured then they will be prompted for a token after logging in. ### Customizing emails [Section titled “Customizing emails”](#customizing-emails) Pegasus ships with simple, responsive email templates for password reset and email address confirmation. These templates can be further customized by editing the files in the `templates/account/email` directory. See [the allauth email documentation](https://docs.allauth.org/en/latest/common/email.html) for more information about customizing account emails. ### Disabling public sign ups [Section titled “Disabling public sign ups”](#disabling-public-sign-ups) If you’d like to prevent everyone from signing up for your app, set the following in your `settings.py`, replacing the existing value:
```python
ACCOUNT_ADAPTER = 'apps.users.adapter.NoNewUsersAccountAdapter'
```
This will prevent all users from creating new accounts, though existing users can continue to login and use the app. ### Further configuration [Section titled “Further configuration”](#further-configuration) Allauth is highly configurable. It’s recommended that you look into the various [configuration settings available within allauth](https://docs.allauth.org/en/latest/account/configuration.html) for any advanced customization. ## Stripe [Section titled “Stripe”](#stripe) If you’re using [Stripe](https://www.stripe.com/) to collect payments you’ll need to fill in the following in `settings.py` (or populate them in the appropriate environment variables):
```python
STRIPE_LIVE_PUBLIC_KEY = os.environ.get("STRIPE_LIVE_PUBLIC_KEY", "")
STRIPE_LIVE_SECRET_KEY = os.environ.get("STRIPE_LIVE_SECRET_KEY", "")
STRIPE_TEST_PUBLIC_KEY = os.environ.get("STRIPE_TEST_PUBLIC_KEY", "")
STRIPE_TEST_SECRET_KEY = os.environ.get("STRIPE_TEST_SECRET_KEY", "")
STRIPE_LIVE_MODE = False # Change to True in production
```
## Google Analytics [Section titled “Google Analytics”](#google-analytics) To enable Google Analytics, add your analytics tracking ID to your `.env` file or `settings.py` file:
```python
GOOGLE_ANALYTICS_ID = 'UA-XXXXXXX-1'
```
Pegasus uses a “global site tag” with gtag.js by default, which is a simpler version of Google Analytics that can be rolled out with zero additional configuration. If you use Google Tag Manager, you can make changes in `templates/web/components/google_analytics.html` to match the snippet provided by Google. See [this article](https://support.google.com/tagmanager/answer/7582054) for more on the differences between gtag.js and Google Tag Manager. ## Sentry [Section titled “Sentry”](#sentry) [Sentry](https://sentry.io/) is the gold standard for tracking errors in Django applications and Pegasus can connect to it with a few lines of configuration. If you build with Sentry enabled, all you need to do is populate the `SENTRY_DSN` setting - either directly in your `settings.py` or via an environment variable. After setting it up on production, you can test your Sentry integration by visiting `https:///simulate_error`. This should trigger an exception which will be logged by Sentry. ## OpenAI and LLMs [Section titled “OpenAI and LLMs”](#openai-and-llms) For help configuring LLMs and AIs, see the [AI docs](/ai/development/). ## Celery [Section titled “Celery”](#celery) See the [celery docs](/celery) for set up and configuration of Celery. ## Turnstile [Section titled “Turnstile”](#turnstile) To enable support for [Cloudflare Turnstile](https://www.cloudflare.com/products/turnstile/), set `TURNSTILE_KEY` and `TURNSTILE_SECRET` in your settings or environment variables. This should automatically enable turnstile on your sign up pages. It is recommended to create two different Turnstile accounts on Cloudflare for development and production. In development you can specify “localhost” as your domain like this:  In production, you should replace that with your site’s production domain. ## Mailing List [Section titled “Mailing List”](#mailing-list) Pegasus includes support for subscribing users to a marketing email list upon signup. Currently, three platforms are supported: 1. [Mailchimp](https://mailchimp.com/) 2. [Kit (formerly ConvertKit)](https://kit.com/) 3. [Email Octopus](https://emailoctopus.com/?urli=Cd7hX) Make sure you choose the platform you would like to use when building your Pegasus project. Then follow the instructions below for the platform you’ve chosen. After completing these steps, new sign-ups will automatically be added to your configured marketing list. Note that it is your responsibility to notify your users / get their consent as per your local privacy regulations. ### Mailchimp [Section titled “Mailchimp”](#mailchimp) To enable the Mailchimp integration, first create a mailing list, then fill in the following to values in your environment/settings.
```python
MAILCHIMP_API_KEY = ''
MAILCHIMP_LIST_ID = ''
```
### Kit (formerly ConvertKit) [Section titled “Kit (formerly ConvertKit)”](#kit-formerly-convertkit) To enable the Kit integration, create your Kit account and go to Settings —> Developer, and create a new V4 API key. Then add the API key value to your `.env` file or your environment/settings.
```python
KIT_API_KEY = ""
```
That’s it! New user sign-ups will automatically be added as Kit subscribers. ### Email Octopus [Section titled “Email Octopus”](#email-octopus) To enable the Email Octopus integration, first create a mailing list, then fill in the following values in your environment/settings.
```python
EMAIL_OCTOPUS_API_KEY = ""
EMAIL_OCTOPUS_LIST_ID = ""
```
Note: If you use [this link](https://emailoctopus.com/?urli=Cd7hX) to sign up for email octopus, you’ll get $15 off your first payment, and help support Pegasus. ## Logging [Section titled “Logging”](#logging) Pegasus ships with a default Django log configuration which outputs logs to the console as follows: * Django log messages at level INFO and above * Pegasus log messages at level INFO and above The Pegasus loggers are all namespaced with the project name e.g. `{{project_name}}.subscriptions`. ### Changing log levels [Section titled “Changing log levels”](#changing-log-levels) There are two environment variables which can be used to control the log levels of either Django messages or Pegasus message: * `DJANGO_LOG_LEVEL` * `{{project_name.upper()}}_LOG_LEVEL` Alternatively the entire log configuration can be overridden using the `LOGGING` setting as described in the [Django docs](https://docs.djangoproject.com/en/stable/topics/logging/). ## Storing media files [Section titled “Storing media files”](#storing-media-files) SaaS Pegasus ships with optional configuration for storing dynamic media files in S3 e.g. user profile pictures. If you do not have this enabled the [default Django configuration](https://docs.djangoproject.com/en/stable/topics/files/) will be used which requires you to have persistent storage available for your site such as a Docker volume. ### Setting up S3 media storage [Section titled “Setting up S3 media storage”](#setting-up-s3-media-storage) *For a video walkthrough of this content (using kamal deployment), see below:* This section assumes you have set up your SaaS Pegasus project with the **S3 media file storage** enabled. In order to use S3 for media storage you will need to create an S3 bucket and provide authentication credentials for writing data to the bucket. Once you have done the S3 setup (see below), you can update your `.env` file as follows:
```dotenv
USE_S3_MEDIA=True
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
```
With this configuration your media files will be accessible at `https://{{ project_name }}-media.s3.amazonaws.com/media/`. [This guide](https://testdriven.io/blog/storing-django-static-and-media-files-on-amazon-s3/) is an excellent resource with step-by-step instructions for the S3 setup. #### Additional settings [Section titled “Additional settings”](#additional-settings) AWS\_STORAGE\_BUCKET\_NAME : Name of the S3 bucket to use. Defaults to `{{project_name}}-media`. ### Alternative storage backends [Section titled “Alternative storage backends”](#alternative-storage-backends) Should you wish to use a different storage backed e.g. [Digital Ocean Spaces](https://www.digitalocean.com/products/spaces) you can follow the setup described in the [django-storages](https://django-storages.readthedocs.io/en/latest/index.html) documentation. There is also a [Pegasus community guide](/community/digital-ocean-spaces) that walks through this in more detail. ## Django Debug Toolbar [Section titled “Django Debug Toolbar”](#django-debug-toolbar) Pegasus ships with [Django Debug Toolbar](https://github.com/jazzband/django-debug-toolbar#readme) as an optional package. This section describes how the feature is configured in Pegasus. The `django-debug-toolbar` package is placed in the `dev-requirements.txt` file which means it will only be available in dev environments. Should you wish to use it in a production environment you will need to add it to your `prod-requirements.in` file and [re-build](/python/setup) your `prod-requirements.txt` file. By default, the toolbar is enabled in development environments via the `ENABLE_DEBUG_TOOLBAR` setting in your `.env` file(s). You can change this setting in any environment to turn it on/off.
```dotenv
ENABLE_DEBUG_TOOLBAR=True
```
# APIs
> Django REST Framework APIs with auto-generated OpenAPI schemas, TypeScript clients, and authentication support for building modern web applications.
Pegasus comes with a rich ecosystem of APIs that can used by your app’s front end as well as exposed to third-party developers. ## APIs in Pegasus [Section titled “APIs in Pegasus”](#apis-in-pegasus) APIs in Pegasus consist of three pieces: 1. **API endpoints**, created with [Django Rest Framework (DRF)](https://www.django-rest-framework.org/). These are the Django views that serve your APIs. 2. **API schemas**, created with [drf-spectacular](https://drf-spectacular.readthedocs.io/en/latest/). These are automatically created by your APIs, and can be used for API documentation and client generation. They follow the [OpenAPI 3](https://spec.openapis.org/oas/v3.1.0) specification. 3. **API clients**, created by [OpenAPI Generator](https://openapi-generator.tech/). These can be used by developers to interact with your APIs. Pegasus ships with a TypeScript (JavaScript) client that is used in your app’s front end by the parts of the app that interact with the backend APIs (e.g. JavaScript charts, and the React/Vue demos). This might sound like a lot of moving parts, but, critically, *all the logic lives in the API endpoints themselves*. The schemas are auto-generated by the endpoints, and the clients are auto-generated by the schemas. So you only have to maintain your APIs in a single place, and everything else is kept in sync with tooling. Using the schemas and clients is optional. You can always interact with a Pegasus API by making the appropriate HTTP requests directly. However, using a client can greatly simplify the code you write and improve the development experience. Front end code in Pegasus that interacts with APIs uses it by default. Additionally, getting API docs “for free” from the schemas can be a big win if you plan to make your project’s API third-party-developer-facing. ## API Documentation [Section titled “API Documentation”](#api-documentation) By default, your Pegasus app ships with two built-in sets of API documentation available at the `/api/schema/swagger-ui/` endpoint ( in development) and `/api/schema/redoc/` endpoint ( in development). The API docs will look something like this: **Swagger API docs:**  **Redoc API docs:**  ## API Clients [Section titled “API Clients”](#api-clients) As part of the [front end](/front-end/overview), Pegasus ships with an API client that can be used to interact with your project’s APIs. **This client is automatically generated from your APIs and should not be modified by hand.** You can find the source code of the API client(s) in the `api-client` folder in your project’s root directory. *Note: In releases prior to 2024.3 the API client was in the `assets/javascript/api-client` directory.* ### Using the API client [Section titled “Using the API client”](#using-the-api-client) There are several example usages of the API client in the Pegasus codebase. The steps, as seen in the employee app demo, are as follows: **Initialize the API client**
```javascript
import {Cookies} from "./app";
import {Configuration, PegasusApi} from "./api-client";
const apiConfig = new Configuration({
basePath: 'https://yourserver.com/', // or pass this in via {{server_url}} template variable
headers: {
'X-CSRFToken': Cookies.get('csrftoken'),
}
})
const client = new PegasusApi(apiConfig);
```
**Call an API**
```javascript
client.employeesList().then((result) => {
// do something with the API result here
console.log('your employees are ', result.results);
});
```
### Client method names [Section titled “Client method names”](#client-method-names) The easiest way to find out the methods available in the API client is by looking at the source code in `api-client/apis/Api.ts`. Method names are determined by the `operationId` value for the API in the auto-generated `schema.yaml` file. These identifiers are auto-generated, but can be overridden using DRF Spectacular’s `extend_schema_view` and `extend_schema` helper functions. This can be done for an entire `ViewSet` as follows:
```python
from drf_spectacular.utils import extend_schema_view, extend_schema
from rest_framework import viewsets
@extend_schema_view(
create=extend_schema(operation_id='employees_create'),
list=extend_schema(operation_id='employees_list'),
retrieve=extend_schema(operation_id='employees_retrieve'),
update=extend_schema(operation_id='employees_update'),
partial_update=extend_schema(operation_id='employees_partial_update'),
destroy=extend_schema(operation_id='employees_destroy'),
)
class EmployeeViewSet(viewsets.ModelViewSet):
# rest of viewset code here
```
The IDs in the Python code will be converted to camelCase in the JavaScript client. ### Generating the OpenAPI3 schema.yml file [Section titled “Generating the OpenAPI3 schema.yml file”](#generating-the-openapi3-schemayml-file) In a new Pegasus installation, the OpenAPI3 `schema.yml` will be available at the `/api/schema/` endpoint ( in dev). If you plan to use the `schema.yml` file in production, it is more efficient to create it once and serve it as a static file. This can be done by running:
```bash
./manage.py spectacular --file static/api-schema.yml
```
Then you can reference the file by using `{% static /api-schema.yml %}` in a Django template. ### Generating the API client [Section titled “Generating the API client”](#generating-the-api-client) Anytime you change your APIs you should create a new API client to keep things in sync. This can be done using the [OpenAPI Generator](https://openapi-generator.tech/) project. The [typescript-fetch](https://openapi-generator.tech/docs/generators/typescript-fetch) client is the one used by Pegasus. #### Running natively (requires Java) [Section titled “Running natively (requires Java)”](#running-natively-requires-java) To generate your API client natively, first install the `openapi-generator-cli` (this library also requires `java`):
```bash
npm install @openapitools/openapi-generator-cli -g
```
Then run it as follows:
```bash
openapi-generator-cli generate -i http://localhost:8000/api/schema/ -g typescript-fetch -o ./api-client/
```
The above assumes your Django server is running at , but you can replace that value with any URL or file system reference to your `schema.yml` file. #### Running in docker [Section titled “Running in docker”](#running-in-docker) You can also generate your API client with docker to avoid having to install Java by running:
```bash
make build-api-client
```
while your server is running. You should see the files in `api-client` get updated. #### Rebuilding your front end [Section titled “Rebuilding your front end”](#rebuilding-your-front-end) After re-creating the API client, you’ll have to rebuild your front end:
```bash
npm run dev
```
Note that introducing breaking changes to your APIs can also break your API client! If you’re unsure if you introduced breaking changes it is worth testing any functionality that depends on the API client. ## Authentication APIs [Section titled “Authentication APIs”](#authentication-apis) *Added in version 2024.3. Changed in 2025.4.1* If you enable the “Use Authentication APIs” checkbox in your project, Pegasus will generate a set of API endpoints for registering and logging in users. These endpoints can be used to integrate your backend with single page applications (SPAs) and mobile apps. Under the hood, Pegasus uses [allauth headless](https://docs.allauth.org/en/dev/headless/openapi-specification/) for these endpoints. This feature uses Django’s session-based authentication by default---which works great for single page apps---though it is possible to add in JWT or another token-based authentication scheme to better support mobile applications. A complete end-to-end example that uses the API authentication feature in a React SPA can be found in the experimental [standalone front end](/experimental/react-front-end). This example includes React/API-based sign up, login, password reset, two-factor authentication, email confirmation and more. ## API Keys [Section titled “API Keys”](#api-keys) Pegasus supports the use of API Keys to access APIs, built on top of the [Django REST Framework API Key](https://florimondmanca.github.io/djangorestframework-api-key/) project. Pegasus includes the ability to create API keys, associate them with your User objects, and access APIs using the key. ### Creating and managing API keys [Section titled “Creating and managing API keys”](#creating-and-managing-api-keys) A simple UI for creating, viewing, and revoking API keys is available to end users from the Profile page. More advanced/customized management of API keys---including the ability to associate names and expiry dates with keys---is available through the Django admin interface. Note that when an API key is created it will be displayed *once* and will not be available after that. For more details on working with API keys see [the library documentation](https://florimondmanca.github.io/djangorestframework-api-key/guide/#creating-and-managing-api-keys). ### API keys and Users [Section titled “API keys and Users”](#api-keys-and-users) Pegasus associates API keys with your Django `User` objects. This is a good, practical way to get started with API key scoping. All access granted by the key will the same as the associated `CustomUser` object, which allow you to easily create APIs that work with logged-in users *or* API keys. The `apps.api.models.UserAPIKey` class is used to associate an API key with a `CustomUser`. You can then enable API keys for any user-specific views, by following the instructions for `APIView`s and `ViewSet`s below. More complex API key permissions---for example, associating a key with a single API or a single team---can be created by following [these instructions](https://florimondmanca.github.io/djangorestframework-api-key/guide/#api-key-models). To enable API-key support for an `APIView`, or `ViewSet`, use the `IsAuthenticatedOrHasUserAPIKey` permission class in place of `IsAuthenticated`. This will allow either authenticated users or UserAPIKey users to access the APIs. In either case, the associated user object will be available as `request.user`. You can see an example `APIView` in the `EmployeeDataAPIView` class that ships with the Pegasus examples, and an example `ViewSet` in the `EmployeeViewSet` code. ### Testing API keys [Section titled “Testing API keys”](#testing-api-keys) The easiest way to test API key functionality is to use a tool like [curl](https://curl.se/). The following command can be used to test a user-based API key with a default Pegasus installation:
```bash
curl http://localhost:8000/pegasus/employees/api/employees/ -H "Authorization: Api-Key "
```
You should replace `` with the API key displayed when it is created. ## Troubleshooting [Section titled “Troubleshooting”](#troubleshooting) ### API client requests are failing [Section titled “API client requests are failing”](#api-client-requests-are-failing) When API client requests fail you will get error messages in parts of the application that use the API clients, including the Teams UI (if you are using React), and the React/Vue employee examples. The most common reason that API client requests fail is a mismatch between the absolute URL configured in the server and the servers *actual* URL. This mismatch be fixed by modifying the Django Site object and settings to match the URL you’re loading the site from, as described in the documentation on [absolute URLs](/configuration/#absolute-urls). In *development* the most common issues are: 1. Your Django Site is not set up for development. Ensure the site’s domain name is `localhost:8000` in your Django admin, [as described here](/configuration/#absolute-urls). 2. You are loading from a mismatched domain. Be sure you are loading your browser at and not . Or alternatively, if you want to use the 127.0.0.1 address, update the Django site accordingly to use that.
# Async and Websocket Support
> Enable asynchronous Django views and real-time websockets using Daphne, Uvicorn, and Django Channels for modern web applications.
As of version 2023.10, Pegasus provides support [asynchronous support](https://docs.djangoproject.com/en/stable/topics/async/), as well as support for websockets via the [channels library](https://channels.readthedocs.io/). ## Enabling Async Support [Section titled “Enabling Async Support”](#enabling-async-support) You can enable Async support by checking the “Use Async / Websockets” option in your project settings. Enabling Async will: 1. Change your default development server to [Daphne](https://docs.djangoproject.com/en/stable/howto/deployment/asgi/daphne/). 2. Change your default production server to [Uvicorn](https://www.uvicorn.org/) (via gunciorn). 3. Add and configure `channels` in your project for websocket support. In addition to the above configuration changes, enabling async will also use it for LLM chats if available. Finally, there is an optional group chat application you can separately add (details below). ## The Async / Websocket Demo Application [Section titled “The Async / Websocket Demo Application”](#the-async--websocket-demo-application) Pegasus includes an optional demo application to demonstrate the asynchronous and socket capabilities. The demo application is an extension of the demo application that you build while completing the [channels tutorial](https://channels.readthedocs.io/en/latest/tutorial/index.html). You can see a demo below. The demo application uses the [HTMX websockets extension](https://htmx.org/extensions/ws/) to simplify the implementation. If you prefer not to use HTMX at all, you can change your websocket connection logic to use vanilla JavaScript instead, as shown in the [channels tutorial here](https://channels.readthedocs.io/en/latest/tutorial/part_2.html#add-the-room-view). A React-based websocket demo is on the roadmap. ## Websocket urls [Section titled “Websocket urls”](#websocket-urls) Websocket URLs are defined separately from your app’s main `urls.py` file. In Pegasus, the convention is to put your websocket urls in `channels_urls.py` in your project folder (the same one containing `urls.py`). Because websocket urls are separate from your main app, and because they follow a different protocol, they must be referenced as absolute URLs in your front end (including prepending “ws\://” or “wss\://” depending on whether you’re using HTTPS). Pegasus ships with two helper functions you can use to assist with working with URLs, so long as you follow Pegasus conventions. The `websocket_reverse` function will reverse a relative websocket URL, and the `websocket_absolute_url` function will turn a relative URL into an absolute websocket URL based on your Site address and the `USE_HTTPS_IN_ABSOLUTE_URLS` setting. You can combine these functions like so to pass the URL of a websocket endpoint to a template:
```python
room_ws_url = websocket_absolute_url(websocket_reverse("ws_group_chat", args=[room_id]))
```
You can then use the websocket URL in a template/JavaScript like this:
```js
const chatSocket = new WebSocket({{ room_ws_url}});
chatSocket.onmessage = function(e) {
// handle message
};
```
## Asynchronous web servers [Section titled “Asynchronous web servers”](#asynchronous-web-servers) There are several ASGI servers supported by Django. By default, Pegasus uses the Daphne web server in development and the Uvicorn web server in production, for reasons described below. That said, you can customize your app to use whichever server you prefer. ### Daphne [Section titled “Daphne”](#daphne) In development, Pegasus uses the [Daphne](https://pypi.org/project/daphne/) web server for its tight integration with Django’s `runserver` command, as [outlined in the Django docs](https://docs.djangoproject.com/en/stable/howto/deployment/asgi/daphne/). Daphne is installed via `dev-requirements` and will be added to your `INSTALLED_APPS` whenever `settings.DEBUG` is `True`. ### Uvicorn [Section titled “Uvicorn”](#uvicorn) In production, Pegasus uses the [Uvicorn](https://www.uvicorn.org/) web server. Uvicorn has a seamless integration with `gunicorn`, making transitioning to it very easy. Uvicorn is installed via `prod-requirements`, and if you build with async features enabled, your `gunicorn` command will be updated to use it. ## Troubleshooting [Section titled “Troubleshooting”](#troubleshooting) **The chat app loads but nothing happens when I send a message.** The most likely reason this would happen is if your site URLs are not set up properly, which would cause the websocket endpoints to not hit the right address. See the documentation on [absolute URLs](/configuration/#absolute-urls) to fix this, and in particular make sure your Django site object has the right domain. In development this should be set to `localhost:8000`. **I’m getting an error: No module named ‘daphne’** If you are getting this error *in production* it is likely because your `DEBUG` environment variable is not set. Due to the order in which settings are imported, you *must* define `DEBUG=False` in your *environment*, `.env` file, or main `settings.py` file. This is in addition to (or instead of) setting `DEBUG=False` in your `settings_production.py` file. If you are getting this error *in development*, be sure that Daphne is installed. You should have the a `channels[daphne]` entry in your `dev-requirements.in` file, and you should [build and install your requirements](/python/setup) as needed. To do this in a non-Docker environment, run:
```plaintext
pip-compile requirements/dev-requirements.in
pip install -r requirements/dev-requirements.txt
```
**I’m having another issue deploying to production.** Since this is a new feature there may be some speed-bumps getting it into production on all platforms. While every deployment platform is expected to work, it is not possible to test every app/configuration. So, if you have any issues please reach out over email () or on Slack and I will do my best to help!
# Celery
> Set up Celery distributed task queues with Redis for background tasks, scheduled jobs, and async processing in Pegasus applications.
[Celery](https://docs.celeryq.dev/) is a distributed task queue used to run background tasks. It is required by several Pegasus features, including: 1. The “background task” example. 2. Per-unit subscriptions (celery runs the background task to sync unit amounts with Stripe). 3. AI Chat (it is used in all builds to set chat names, and, if async is not enabled, for the chats themselves). If you aren’t using any of the above features, you can disable celery by unchecking the “use celery” option---added in version 2025.1---in your project settings. **If you *are* using any of the above features, this option will not do anything.** ## Quick Start [Section titled “Quick Start”](#quick-start) **If you’re using [Docker in development](/docker) then Celery should automatically be configured and running. The instructions in this section are for running Celery outside of Docker.** The easiest way to get going in development is to [download and install Redis](https://redis.io/download) (if you don’t already have it) and then run: *With uv:*
```bash
uv run celery -A {{ project_name }} worker -l info --pool=solo
```
*With standard Python:*
```bash
celery -A {{ project_name }} worker -l info --pool=solo
```
Note that the ‘solo’ pool is recommended for development but not for production. When running in production, you should use a more robust pool implementation such as `prefork` (for CPU bound tasks) or `gevent` (for I/O bound tasks). ### Celery and Gevent [Section titled “Celery and Gevent”](#celery-and-gevent) In production Celery is configured to run with the `gevent` pool, which drastically improves performance of Celery when running tasks that are I/O bound (which tends to be most tasks that make API or database calls). However, `gevent` does have some limitations, including that it does not work well `asyncio`. This means that if you are calling lots of async code in your Celery tasks, you should consider a different pool. To change the pool used by Celery you can modify (or remove) the `--pool` command when you call it. Note that `--pool=solo` or `--pool=gevent` is recommended for running Celery on Windows, since Celery 4.x [no longer officially supports Windows](https://docs.celeryq.dev/en/4.0/whatsnew-4.0.html#removed-features). For more information see the [Celery documentation](https://docs.celeryq.dev/en/stable/userguide/concurrency/gevent.html). ## Setup and Configuration [Section titled “Setup and Configuration”](#setup-and-configuration) The above setup uses [Redis](https://redis.io/) as a message broker and result backend. If you want to use a different message broker, for example [RabbitMQ](https://www.rabbitmq.com/), you will need to modify the `CELERY_BROKER_URL` and `CELERY_RESULT_BACKEND` values in `settings.py`. More details can be found in the [Celery documentation](https://docs.celeryq.dev/en/stable/getting-started/backends-and-brokers/index.html). ## Monitoring with Flower [Section titled “Monitoring with Flower”](#monitoring-with-flower) [Flower](https://flower.readthedocs.io/en/latest/) is an open-source web application for monitoring and managing Celery clusters. It provides real-time information about the status of Celery workers and tasks. If you’d like to use Flower in development, add the following to the `services` section of your `docker-compose.yml`:
```yaml
flower:
image: mher/flower
environment:
- CELERY_BROKER_URL=redis://redis:6379
command: celery flower
ports:
- 5555:5555
depends_on:
- redis
```
In production, you will likely want to run Flower behind a private VPN, or [set up authentication](https://flower.readthedocs.io/en/latest/auth.html) on your Flower instance, and use a [reverse proxy](https://flower.readthedocs.io/en/latest/reverse-proxy.html) to expose it. ## Scheduled Tasks with Celery Beat [Section titled “Scheduled Tasks with Celery Beat”](#scheduled-tasks-with-celery-beat) [Celery Beat](https://docs.celeryq.dev/en/stable/userguide/periodic-tasks.html) is a scheduler that triggers tasks at regular intervals, which can be used to run periodic tasks like daily reports, or sending scheduled notifications. ### Configuration [Section titled “Configuration”](#configuration) By default, Celery Beat will store the schedule in file on the filesystem. When running in a production environment and especially in a containerized environment, you should use persistent storage to store the schedule. Pegasus is pre-configured to store the schedule in the Pegasus database using [`django-celery-beat`.](https://django-celery-beat.readthedocs.io/en/latest/). You can place the schedule task definitions in the `SCHEDULED_TASKS` setting in your `settings.py` file and then run the `bootstrap_celery_tasks` management command to create the tasks in the database.
```python
from celery.schedules import crontab
SCHEDULED_TASKS = {
'example-task-every-morning': {
'task': '{{ project_name }}.tasks.example_task',
'schedule': crontab(hour=7, minute=0), # Run every day at 7:00 AM
},
'another-example-every-hour': {
'task': '{{ project_name }}.tasks.another_example',
'schedule': 3600.0, # Run every hour (in seconds)
'args': (16, 16), # Arguments to pass to the task
},
}
```
```bash
python manage.py bootstrap_celery_tasks --remove-stale
```
This will create or update the tasks in the database and remove any stale tasks that are no longer defined in `SCHEDULED_TASKS`. If you want to bootstrap the tasks automatically during you application deploy process you can do so by running the bootstrap command alongside the Django migration command. ### Running Celery Beat [Section titled “Running Celery Beat”](#running-celery-beat) To run Celery Beat in development: *With Docker:* If you are using the local dockerized setup with docker compose, then Celery Beat will already be running as part of the `celery` service. *With uv:*
```bash
# Alongside the Celery worker, you can run Celery Beat
uv run celery -A {{ project_name }} worker -l info --beat
# AS a dedicated process
uv run celery -A {{ project_name }} beat -l info
```
Note that if you run Celery Beat as a standalone process, you will need to ensure that the Celery worker is running separately. This is because Celery Beat is responsible for scheduling tasks while the worker executes them. #### Production Setup [Section titled “Production Setup”](#production-setup) In production, you can run Celery Beat as a separate process. You must ensure that there is only ever one Celery Beat process running at a time to avoid multiple instances of the same task being scheduled. It’s also important to note that you can not run Celery Beat in the same process as a worker that is using the `gevent` pool. For more information, see the [Celery Beat documentation](https://docs.celeryq.dev/en/stable/userguide/periodic-tasks.html).
# Cookbooks
> Step-by-step guides for Django admin setup, migrating from pip-tools to uv, enabling code formatting, and common development tasks.
Step-by-step guides to some different things you might want to do with Pegasus. ## Use the Django Admin UI [Section titled “Use the Django Admin UI”](#use-the-django-admin-ui) Pegasus ships with a simple script to promote any user to a superuser who can access the Django admin. After going through the sign up flow, to convert your newly-created user into an admin, run the following command, being sure to replace the email address with the one you used to sign up: **Docker:**
```bash
docker compose exec web python ./manage.py promote_user_to_superuser yourname@example.com
```
**Native:**
```bash
python ./manage.py promote_user_to_superuser yourname@example.com
```
Now you should be able to access the django admin at ## Migrating from pip-tools to uv [Section titled “Migrating from pip-tools to uv”](#migrating-from-pip-tools-to-uv) To migrate your project from pip-tools to uv follow these steps. ### Install uv [Section titled “Install uv”](#install-uv) If you haven’t already, [install uv](https://docs.astral.sh/uv/getting-started/installation/):
```bash
curl -LsSf https://astral.sh/uv/install.sh | sh
```
### Update your project code [Section titled “Update your project code”](#update-your-project-code) It’s recommended to do this in two steps: 1. [Upgrade your project](/upgrading) to the latest Pegasus version, keeping your package manager as “pip-tools”. Merge all conflicts and ensure your project is working properly on this version. 2. Then, change the package manager from pip-tools to uv in your project settings and do another upgrade/pull request. At this point you will likely have conflicts in your requirements files, but hopefully nowhere else. See the next sections for resolving these. ### Prepare to resolve conflicts [Section titled “Prepare to resolve conflicts”](#prepare-to-resolve-conflicts) First, follow the github instructions to merge your project on your local machine, by checking out the pegasus upgrade branch and merging the main branch into it. You will have to update the command below with the exact branch name of the pull request created by Pegasus:
```bash
git fetch origin
git checkout pegasus--
git merge main
```
At this point you’ll have a partially merged branch with conflicts. ### Migrate your requirements.in files [Section titled “Migrate your requirements.in files”](#migrate-your-requirementsin-files) The uv build of Pegasus no longer uses requirements files, so any changes you’ve made to these will need to be migrated to `pyproject.toml` and `uv.lock`. You can use the [reqs-sync](https://github.com/saaspegasus/reqs-sync/) package to help with this. Follow the steps below for any file with conflicts. To migrate your main *requirements.in* file:
```bash
uv tool run reqs-sync reqs-to-toml requirements/requirements.in
```
To migrate your development *dev-requirements.in* file:
```bash
uv tool run reqs-sync reqs-to-toml requirements/dev-requirements.in --group=dev
```
To migrate your production *prod-requirements.in* file:
```bash
uv tool run reqs-sync reqs-to-toml requirements/prod-requirements.in --group=prod
```
These commands should copy all project requirements from your `requirements.in` file(s) to your `pyproject.toml` file (into the appropriate group, if necessary). ### Update your uv.lock file [Section titled “Update your uv.lock file”](#update-your-uvlock-file) Next you should rebuild your `uv.lock` file from the updated `pyproject.toml` file:
```bash
uv lock
```
You should then check the versions that were added to the `uv.lock` file and update any as needed based on the versions your requirements.txt files. ### Test the migration [Section titled “Test the migration”](#test-the-migration) Run your project (`uv run python manage.py runserver`) and verify everything works as expected. ### Remove your requirements files [Section titled “Remove your requirements files”](#remove-your-requirements-files) Finally, run:
```bash
git rm requirements/*`
```
To remove all your requirements files. Congratulations, you’ve migrated to uv! Resolve any other conflicts, push and merge your code, and you’re done! ## Migrating to auto-formatted code [Section titled “Migrating to auto-formatted code”](#migrating-to-auto-formatted-code) As of February, 2023 all Pegasus projects have the option to auto-format your Python code. To migrate a project from non-formatted to formatted code, you can go through the following steps: 1. First, do a full Pegasus upgrade to the version you want to update to, as described [here](/upgrading). **Do *not* check the “autoformat” checkbox yet.** 2. Next, run the formatting tools on your project’s `main` branch: 1. Install ruff: `pip install ruff` 2. Run ruff linting `ruff check --extend-exclude migrations --line-length 120 . --fix` 3. Run ruff formatting: `ruff format --line-length 120 .` 3. Commit the result: 1. `git add .` 2. `git commit -m "apply formatting changes"` 4. Finally, check the “autoformat” box on your Pegasus project, and do *another* upgrade according to the same process. ## Migrating an existing project to Pegasus [Section titled “Migrating an existing project to Pegasus”](#migrating-an-existing-project-to-pegasus) There is not a one-size-fits-all answer to how to migrate an existing app to Pegasus, as it can depend on the size, complexity, age, and architecture of the project you’re migrating from. That said, the strategy that has worked best for most people on small-to-medium-sized projects is to basically **start a new project on Pegasus and merge your existing functionality into it**. Here is a rough guideline for how you can do that: 1. Create a new Pegasus project with the exact settings you want your app to have. If your existing app uses certain technologies (e.g. css frameworks, deployment, etc.) it’s probably easiest to pick all the same ones, if possible, unless you know you want to change those at the same time. 2. Bring across the custom logic of your legacy project, while largely preserving the previous project’s structure. So, for example, if your project was split into multiple Django apps, just copy those across. If it was a monolith, just leave it that way, and so on. 3. Reconcile any conflicting data models. E.g. you will only want a single user model. Ideally you would use Pegasus’s built-in `CustomUser` model and update your foreign keys accordingly, although this can make data migrations more complicated. 4. Try and get the urls/views etc. working for your previous app’s functionality. Don’t worry about UI, but just try to get the routes and business logic working and routing to the right tmeplates. 5. Migrate those templates to use the Pegasus base templates, etc. (if possible). You can kind of do this page by page, making each one look good as you go. Alternatively, keep your own base template if it is different enough from Pegasus’s or if you want to keep your existing app scaffolding. The latter option will require updating Pegasus’s built-in functionality to work with your own templates, and will make future upgrades/merges more complicated. 6. Figure out a data migration (assuming you already have production data). This can often be the trickiest part, especially if you’re swapping the user model or othe foregn keys. It can often be easiest to write scripts to copy the data across from one instance to the other, but in some cases you might prefer to keep your previous migration history in place and run migrations on the live database. The larger the project is, the more likely it is you’ll want to keep the database and existing models and just use migrations to do the minimal set of Pegasus changes. The main downsides of the above approach are that you lose your git history on the previous project, data migrations can be tricky, and if you have a complex UI then it might take some effort to port across. The main upside is that once you get through the pain, all future Pegasus updates will likely be much smoother. ## Delete Pegasus Examples [Section titled “Delete Pegasus Examples”](#delete-pegasus-examples) You can remove the Pegasus examples by unchecking the “Include Examples” checkbox on your project page and re-downloading (/or [upgrading](/upgrading)) your codebase. For earlier versions you can use [these instructions](https://github.com/saaspegasus/pegasus-docs/blob/1becc2cb8f86738eeba85c9faddb15f69b8ad7bc/cookbooks.md#delete-pegasus-examples).
# Customizations
> Customize landing pages, navigation, styles, and JavaScript in your Pegasus application with popular CSS frameworks.
This page outlines the basics of customizing Pegasus to meet your application’s needs. ## Personalize your landing page [Section titled “Personalize your landing page”](#personalize-your-landing-page) Pegasus ships with a simple landing page that varies based on your selected CSS framework. Most projects will want to highly customize the landing page from what comes out of the box. Unless you are planning on building a marketing site on a different platform, this is likely one of the first things you’ll do. To modify the default landing page, you can edit the `./templates/web/landing_page.html` file (and any included sub-templates) and make the customizations you want. Another good option is to use a paid or open-source alternative for your marketing content. Some recommended places to get marketing templates include: * **Tailwind**: [Tailwind UI](https://tailwindui.com/), [Flowbite](https://flowbite.com/). * **Bootstrap**: [Official themes](https://themes.getbootstrap.com/), [other free recommendations](https://dev.to/bootstrap/bootstrap-5-templates-91p). * **Bootstrap (Material)**: [Material Kit Pro](https://www.creative-tim.com/product/material-kit-pro) ## Update the logged-in experience [Section titled “Update the logged-in experience”](#update-the-logged-in-experience) After you’ve tweaked your landing page, you’ll likely want to dive into the nuts and bolts that make up your app. To modify the logged-in default page, edit the `./templates/web/app_home.html` file to your liking. ### Changing the navigation [Section titled “Changing the navigation”](#changing-the-navigation) There are two levels of navigation that ship with Pegasus, the top nav and the sidebar nav. You’ll likely want to modify both. To change the top nav edit the `./templates/web/components/top_nav.html` file. To change the sidebar nav edit the `./templates/web/components/app_nav.html` file. ## Styles [Section titled “Styles”](#styles) All of Pegasus’s CSS frameworks are designed to be customized to your needs. You can set specific colors or override the themes entirely. How styles are customized depends on the CSS framework. For more information, see the individual page for your framework in [the CSS docs](/css/overview) ## Javascript [Section titled “Javascript”](#javascript) The project uses a Vite build pipeline to compile the JavaScript files. For more details on how it works see the [front-end documentation](/front-end/overview).
# Using Docker in Development
> Set up Django development environment with Docker Compose including PostgreSQL, Redis, Celery, and debugging configuration.
Pegasus recommends using [Docker](https://www.docker.com/) during development. Although Docker is not strictly required, many parts of the documentation and helper tools do assume you are using it. In production, Docker can also be used to deploy your application to containerized platforms. See [the deployment page](/deployment/overview) for more details on Docker in production. ## Choosing a Docker Setup [Section titled “Choosing a Docker Setup”](#choosing-a-docker-setup) When configuring your Pegasus project to use Docker, you can select from two different options: **services-only**, and **full-Docker development**. In **services-only mode**, Docker is only used to run the external services: PostgreSQL and Redis. The Django server, Celery and any other processes are run directly on the local machine. In this mode, you don’t need to install PostreSQL and Redis on your local machine, which simplifies the setup and maintenance. You also have direct access to the other dev processes which simplifies debugging and inspection. The main downside of services-only mode is that it requires installing `uv` and `npm`. In **full-Docker** mode, Docker is used to run the services, as above, but also runs Django, npm, and Celery. No processes are run directly on your local machine. This mode makes it easier to get up and running---since all you need to install is Docker---but it can make development more complicated, since all the processes are running inside Docker. As a rough guideline: **If you are comfortable installing and running Python and Node.js on your machine, use services-only mode. Otherwise, use full-Docker mode.** ## Install prerequisites [Section titled “Install prerequisites”](#install-prerequisites) You need to install [Docker](https://www.docker.com/get-started) prior to setting up your environment. Mac users have reported better performance on Docker using [OrbStack](https://orbstack.dev/), which is a Docker Desktop alternative optimized for performance. ## Starting the application [Section titled “Starting the application”](#starting-the-application) To start the Docker services, run:
```bash
make start
```
This will start the Database services (PostgreSQL and Redis) and in full-mode, start all the processes needed to run your application, including Django, the front end server / bundler, and Celery. The first time you run the app you should run:
```bash
make init
```
Which will also create and run database migrations and bootstrap your application. ## Stopping the application [Section titled “Stopping the application”](#stopping-the-application) To stop the Docker services, run:
```bash
make stop
```
This will stop the Database services (PostgreSQL and Redis) and in full-mode, stop the other container processes (Django, Vite/Webpack, and Celery). ## Architecture and how it works [Section titled “Architecture and how it works”](#architecture-and-how-it-works) This section provides technical details about the Docker setup and how it works. The Docker configuration is primarily in `docker-compose.yml`, where you can inspect the configured containers. ### Services only mode [Section titled “Services only mode”](#services-only-mode) In this mode, the `docker-compose.yml` file will only include container definitions for PostgreSQL and Redis. The containers listed below will run with their default ports exposed. Use `docker ps` to check. | Container Name | Purpose | Port | | -------------- | ------------------------------------ | ---- | | `db` | Runs Postgres (primary Database) | 5432 | | `redis` | Runs Redis (Cache and Celery Broker) | 6379 | ### Full Docker dev mode [Section titled “Full Docker dev mode”](#full-docker-dev-mode) In this mode, the `docker-compose.yml` file will also include containers for Django, node, and Celery. Depending on your project settings, there are several containers that might be running. These are outlined in the table below: | Container Name | Purpose | Included | | -------------- | ---------------------------------------- | ----------------------------------------------------------------------- | | `db` | Runs Postgres (primary Database) | Always | | `redis` | Runs Redis (Cache and Celery Broker) | Always | | `web` | Runs Django | Always | | `vite` | Runs Vite (for CSS/JavaScript assets) | If [building with Vite](/front-end/vite) | | `webpack` | Runs Webpack (for CSS/JavaScript assets) | If [building with Webpack](/front-end/webpack) | | `celery` | Runs Celery (for background tasks) | If [Celery is enabled](/celery) | | `frontend` | Runs the React Front End | If [the standalone front end is enabled](/experimental/react-front-end) | Like above, the DB containers will expose their default ports. You can inspect the `Dockerfile`s being used in `docker-compose.yml`. Python containers use the `Dockerfile.dev` file. ### Settings [Section titled “Settings”](#settings) The docker environment sets environment variables using the included `.env` file. The `.env` file is automatically ignored by git, so you can put any additional secrets there. It generally should not be checked into source control. You can instead add variables to `.env.example` to show what should be included. ## Working with full docker mode [Section titled “Working with full docker mode”](#working-with-full-docker-mode) The following instructions are specific to “full” docker mode, where Docker is also running your application. ### Python environments [Section titled “Python environments”](#python-environments) The Python environment is run in the containers, which means you do not need to have your own local environment if you are always using Docker for development. Python requirements are automatically installed when the container builds. However, keep in mind that if you go this route, you will need to run all commands inside the containers as per the instructions below. ### Running once-off management commands [Section titled “Running once-off management commands”](#running-once-off-management-commands) Running commands on the server can be done using `docker compose`, by following the pattern used in the `Makefile`. For example, to bootstrap Stripe subscriptions, run:
```bash
docker compose exec web python manage.py bootstrap_subscriptions
```
Or to promote a user to superuser, run:
```bash
docker compose exec web python manage.py promote_user_to_superuser me@example.com
```
You can also use the `make manage` command, passing in `ARGS` like so:
```bash
make manage ARGS='promote_user_to_superuser me@example.com'
```
You can add any commonly used commands you want to `custom.mk` for convenience. ### Updating Python packages [Section titled “Updating Python packages”](#updating-python-packages) If you add or modify anything in your `requirements.in` (and `requirements.txt`) files, you will have to rebuild your containers. The easiest way to add new packages is to add them to `requirements.in` and then run:
```bash
make requirements
```
Which will rebuild your `requirements.txt` file, rebuild your Docker containers, and then restart your app with the latest dependencies. ### Debugging [Section titled “Debugging”](#debugging) You can use debug tools like `pdb` or `ipdb` by enabling service ports. This can be done by running your web container with the following:
```bash
docker compose run --service-ports web
```
If you want to set up debugging with PyCharm, it’s recommended to follow [this guide on the topic](https://testdriven.io/blog/django-debugging-pycharm/). ## Troubleshooting [Section titled “Troubleshooting”](#troubleshooting) ### ”No such file or directory” errors [Section titled “”No such file or directory” errors”](#no-such-file-or-directory-errors) Some environments---especially on Windows---can have trouble finding the files on your local machine. This will often show up as an error like this when starting your app:
```plaintext
python: can't open file '/code/manage.py': [Errno 2] No such file or directory
```
These issues are usually related to your *disk setup*. For example, mounting your code on a remote filesystem or external drive to your machine. To fix, try running the code on the same drive where Docker Desktop is installed, or on your machine’s default “C:” drive. You can also get around this issue by running your application natively, instead of with Docker. ## Other Resources [Section titled “Other Resources”](#other-resources) * [Dockerizing Django with Postgres, Gunicorn, and Nginx](https://testdriven.io/blog/dockerizing-django-with-postgres-gunicorn-and-nginx/) provides an overview of the setup, and has additional information about using Docker in production * [Environment variables in Compose](https://docs.docker.com/compose/environment-variables/) is a good resource on the different ways to work with environment variables in Docker
# Early Access Program
> How to get early access to new SaaS Pegasus features and development releases.
If you’d like to try out new Pegasus features before they’re officially released, you can request access to the early-access / beta testing program. ## What’s in Early Access [Section titled “What’s in Early Access”](#whats-in-early-access) You can see the [Development Release Notes](/release-notes-dev/) for a preview of what’s currently in development. With early access you have the opportunity to: * **Test new features** before they’re officially released * **Provide feedback** that shapes the final implementation * **Get a head start** on upcoming changes It is worth noting that early-access features are more likely to have bugs as they have not gone through the same rigor of QA and testing as final release features. ## How to Request Access [Section titled “How to Request Access”](#how-to-request-access) To request early access, reach out through one of these channels: * **Slack**: Message in the SaaS Pegasus Slack community * **Email**:
# Feature Flags
> Implement feature flags with Django Waffle to control feature rollouts, A/B testing, and user-specific or team-based feature access.
[Waffle](https://waffle.readthedocs.io/en/stable/) is the top library for managing feature flags in Django. Pegasus includes configuration for using Waffle with or without teams. If you are using [Teams](/teams) then the Waffle flags can be turned on based on the user or the team. If you are not using Teams then flags only apply to users. ## Usage [Section titled “Usage”](#usage) Waffle can be used to turn on and off features. For example:
```python
import waffle
def my_view(request):
if waffle.flag_is_active(request, 'flag_name'):
"""Behavior if flag is active."""
else:
"""Behavior if flag is inactive."""
```
The flags themselves are managed via the Django Admin site where each flag can be activated for specific users or teams, or based on certain conditions such as *superuser* status. Flags can also be managed via the command line. For full details on configuring flags see the [Flag Attributes](https://waffle.readthedocs.io/en/stable/types/flag.html#flag-attributes) of the Waffle docs. Flags may be used in views, templates, JavaScript and more. For full details see the [Waffle docs](https://waffle.readthedocs.io/en/stable/usage/index.html) ## Usage with *Teams* [Section titled “Usage with Teams”](#usage-with-teams) If you are using [Teams](/teams), Pegasus ships with a [custom flag model](https://waffle.readthedocs.io/en/stable/types/flag.html#custom-flag-models) which allows you to activate flags on a per-team basis in addition to the other default options. ## Example usage [Section titled “Example usage”](#example-usage) To see flags in actions look at the “Flags” example in the Pegasus Example Gallery. The flag in the example is configured in [test mode](https://waffle.readthedocs.io/en/stable/testing/user.html) which allows us to activate the flag with a URL parameter.
# Forms
> Render Django forms with CSS framework integration, dynamic Alpine.js functionality, and custom template tags for better UX.
Pegasus ships with some extensions to Django forms to integrate with different CSS frameworks and add some extensions. ## The `form_tags` module [Section titled “The form\_tags module”](#the-form_tags-module) You can use default Django form rendering for forms, but if you want all the built-in style support, you should instead use the utilities in the `form_tags` module. To use it, first include `form_tags` in any Django template file:
```jinja
{% load form_tags %}
```
Then, you can render a form using the `render_form_fields` template tag. Here is a basic example:
```jinja
```
You can also render individual fields using `render_field`:
```jinja
```
## Dynamic forms with Alpine.js [Section titled “Dynamic forms with Alpine.js”](#dynamic-forms-with-alpinejs) *Added in version 2023.6* The form rendering helpers also support adding attributes, which can be useful to add Alpine.js to make a form more dynamic. For example, you can bind a form value to an alpine model by passing it in `attrs` like this:
```python
class ExampleFormAlpine(forms.Form):
YES_NO_OTHER = (
("yes", gettext("Yes")),
("no", gettext("No")),
("other", gettext("Other")),
)
like_django = forms.ChoiceField(
label=gettext("Do you like Django?"),
choices=YES_NO_OTHER,
widget=forms.Select(attrs={"x-model": "likeDjango"}), # this line will bind the value to an alpine model
)
```
Then in the HTML template you have to add an alpine model to the form:
```jinja
```
# GitHub Integration
> Integrate projects with GitHub using the GitHub App, OAuth, or personal access tokens for automated updates, pull requests, and version control.
You can connect your Pegasus projects directly to GitHub instead of downloading them as a zip file. This makes for a more streamlined workflow---especially when changing or upgrading your project. ## Watch the video [Section titled “Watch the video”](#watch-the-video) The following video shows how to create and update a project using the Github integration. ## Connecting your account [Section titled “Connecting your account”](#connecting-your-account) There are three ways to connect your Github account to Pegasus. The **GitHub App** is the recommended approach for most users. ### Using the GitHub App (Recommended) [Section titled “Using the GitHub App (Recommended)”](#using-the-github-app-recommended) The GitHub App is the easiest and most secure way to connect your account. Unlike the other methods, the GitHub App only grants Pegasus access to repositories you explicitly choose, and it works seamlessly with both personal and organization-owned repositories. 1. **Create a repository.** First, [create a new private repository](https://github.com/new) on GitHub for your project. 2. **Install the app.** From your project download page, click “Install GitHub App”. You’ll be redirected to GitHub to install the app and select which repositories it can access. Make sure to grant access to the repository you just created. 3. **Connect your repo.** After installing the app, you’ll be redirected back to Pegasus. Select your repository from the dropdown and click “Connect Repository”. Once connected, Pegasus can push code and create pull requests in your repository. **Managing permissions:** If you need to grant access to additional repositories later, click “Configure permissions” from the project download page. You can also manage your installation from your [GitHub App settings](https://github.com/settings/installations). ### Using “Connect Github” (OAuth) [Section titled “Using “Connect Github” (OAuth)”](#using-connect-github-oauth) You can also connect your account using the “Connect Github” button on the project download page. This is an easy way to get set up, but grants access to all private repositories in your account. Pegasus does not view or modify data in any repositories unless you connect them, but it theoretically could. ### Using Personal Access Tokens [Section titled “Using Personal Access Tokens”](#using-personal-access-tokens) Pegasus can also connect to your repositories using [Personal Access Tokens](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens). These work similarly to the Github app, but are less user-friendly and require more manual setup. There is no real reason to use them now that the Github app exists, but they are still supported for legacy projects. #### With Classic Tokens [Section titled “With Classic Tokens”](#with-classic-tokens) To use Pegasus with a classic token, visit the [Personal access tokens](https://github.com/settings/tokens) page on Github, then select “Generate new token (classic)” from the dropdown, or [visit this page](https://github.com/settings/tokens/new). Choose a note and expiration date for your token and grant the following scopes: * user:email (Access user email addresses (read-only)) * repo (Full control of private repositories) * workflow (Update GitHub Action workflows) Then click “Generate token”. You will be taken to a page where your token is displayed. Copy this value and paste it into the “personal access token” field from your project download page on Pegasus. Note that you won’t be able to view the token again! #### With Fine-Grained Access Tokens [Section titled “With Fine-Grained Access Tokens”](#with-fine-grained-access-tokens) If you want the most control over your permissions, you should use a fine-grained access token, which allow you to control access to specific repositories. Note that if you use fine-grained tokens **you must create the repository for your project before creating the token**. Pegasus cannot create the project for you with these tokens. After creating the repository, [create a new fine-grained-token from this page](https://github.com/settings/personal-access-tokens/new). Set a token name and expiration date, and then use “Only select repositories” to choose the repositories you want to grant access to (the one you just created). Under “Permissions” —> “Account Permissions” you must grant *read* access to: * Email addresses Then under “Permissions” —> “Repository Permissions” you must grant **read and write** access to: * Contents * Pull Requests * Workflows Then click “Generate token”. You will be taken to a page where your token is displayed. Copy this value and paste it into the “personal access token” field from your project download page on Pegasus. Note that you won’t be able to view the token again! ## Connecting an existing project to Github [Section titled “Connecting an existing project to Github”](#connecting-an-existing-project-to-github) Projects that were created before February 2024, or that didn’t use the Github integration can still be connected to Github via a one-time process. After completing this, you will be able to upgrade and change your Pegasus project using automatic pull requests. First, you’ll have to connect your Github account using one of the methods described above. Next, you will need to find the commit id of the last Pegasus update you have made. If you have never updated your codebase, this will be the first commit in the repository, which you can find by running `git log --reverse`. If you have updated your codebase using one of the other methods below, this will be the last commit on the `pegasus` branch of your repository, which you can find by running `git checkout pegasus` followed by `git log`. Once you have the commit id ready, add your existing Github repository to your Pegasus project from the downloads page. After completing this step you will be prompted with a page that looks like this:  Enter the commit ID there, and you should now be able to update your project with pull requests. ## Working with repositories owned by an organization [Section titled “Working with repositories owned by an organization”](#working-with-repositories-owned-by-an-organization) If you’re using the **GitHub App**, organization repositories work automatically---just make sure to install the app on the organization account (not your personal account) and grant access to the relevant repositories. For **OAuth** and **personal access tokens**, Github organizations do not allow API-based repository access by default, so you will also need to grant programmatic access. Github provides detailed guidance on how to do this. For “Connect Github,” follow the [oauth instructions](https://docs.github.com/en/organizations/managing-oauth-access-to-your-organizations-data), and for personal access tokens, follow the [personal access token instructions](https://docs.github.com/en/organizations/managing-programmatic-access-to-your-organization/setting-a-personal-access-token-policy-for-your-organization). ## Pushing Pegasus code to a subdirectory in your repository [Section titled “Pushing Pegasus code to a subdirectory in your repository”](#pushing-pegasus-code-to-a-subdirectory-in-your-repository) By default, your entire git repository is dedicated to Pegasus, with all of Pegasus’s files included at the root of the repository. Some projects---especially those with a separate front end---may want to instead include Pegasus code in a subdirectory of the repository (e.g. “backend”), so that other projects (e.g. “frontend”) can be included in the same repository. It is possible to configure your Github integration this way. Connect your repository first, then expand “Repo settings” on the project download page and set the subdirectory there. **Make sure to set this before pushing your project for the first time.** If you would like to update an existing project to use a subdirectory, you’ll have unlink and re-add your repository, then [reconnect it](#connecting-an-existing-project-to-github). ## Troubleshooting [Section titled “Troubleshooting”](#troubleshooting) **I keep getting “Error pushing to GitHub. Please check your token scopes.” when pushing my project.** While Pegasus does its best to catch errors that come from Github and show them to you, sometimes it will return this generic error. One common reason a valid token is unable to push code is related to email privacy settings. Specifically the “Blocking command line pushes that expose your personal email address” setting---which currently must be *disabled* in order to use the Github integration. To check and disable this setting: 1. Go to your [Github email settings](https://github.com/settings/emails) 2. Scroll down to where it says “Keep my email addresses private”. 3. If that option is checked, ensure that the “Block command line pushes that expose my email” option below it is *not* checked. 4. If that option is *not* checked, then it is a different problem. You are welcome to reach out directly for support **My GitHub App can no longer access my repository.** If you remove a repository from your GitHub App’s permissions (or uninstall and reinstall the app), Pegasus will lose access to the repository. To fix this, click “Update your app permissions” from the error message on your project download page. This will redirect you to GitHub where you can re-grant access to the repository.
# Using Github Actions
> Automate Django testing and front-end builds with GitHub Actions CI/CD workflows for continuous integration.
[GitHub Actions](https://github.com/features/actions) allows you to automate your software workflows. Pegasus apps optionally ship with Github actions support for a few things to build off. If you’ve built with Github actions support, they should successfully run the first time you push your code to Github. Actions are configured in the `.github` directory in your project. The following actions ship with Pegasus: ## Running Django Tests [Section titled “Running Django Tests”](#running-django-tests) The Django tests are configured in `.github/tests.yml`. By default, it will: * Run on every push to the `main` branch and every pull request. * Run on Python version 3.14 (other Python versions can be added by modifying the `python-version` list) * Use the latest version of Postgres * Run `./manage.py test` All of these can be changed by modifying the relevant sections of the `.github/tests.yml` file. ## Building the Front End [Section titled “Building the Front End”](#building-the-front-end) The front end build is configured in `.github/build_frontend.yml`. By default, it will: * Run on every push to the `main` branch and every pull request. * Run on Node version 22 (other Node versions can be added by modifying the `node-version` list). * Run `npm run build`, ensuring your front end builds properly. * Run `npm run type-check`, ensuring all type checks pass. Any compilation errors in your JavaScript should show up as build failures.
# Internationalization
> Add multi-language support and timezone handling to Django applications with translation files, locale management, and user preferences.
Pegasus supports internationalization via built-in support for timezones and language translations. To enable timezone and multi-language support, you must select the “use internationalization” option in your project settings. ## Translation Demo [Section titled “Translation Demo”](#translation-demo) This two-minute demo highlights how translations work in Pegasus apps. ## Localization [Section titled “Localization”](#localization) Pegasus ships with full support for localizing user-facing text. Currently, not all the user-facing text is properly tagged for localization but this will be incrementally addressed in future releases. For full documentation on localization see the [Django docs](https://docs.djangoproject.com/en/stable/topics/i18n/). ## Big picture [Section titled “Big picture”](#big-picture) Big picture there are two steps to translation: 1. **Define the text you want to translate (in Python, HTML, or JavaScript)**. This step happens in your project’s code. 2. **Add a translation for that text to other languages**. This step happens in your project’s translation files, which can be found in the `locale//LC_MESSAGES/` folders (there will be one for each language). ## Managing enabled languages [Section titled “Managing enabled languages”](#managing-enabled-languages) There are two steps to updating the list of languages that will be available on your site. The first step is to define it in `settings.LANGUAGES`. Out of the box this will be English and French:
```python
from django.utils.translation import gettext_lazy
LANGUAGES = [
('en', gettext_lazy('English')),
('fr', gettext_lazy('French')),
# add other languages here
]
```
The second step is to create the translations folder for the language. This can be done by running:
```bash
python ./manage.py makemessages -l [new lang code] --ignore node_modules --ignore venv
```
Or in Docker:
```bash
docker compose exec web python manage.py makemessages -l [new lang code] --ignore node_modules --ignore venv
```
## Marking text in your app for translation [Section titled “Marking text in your app for translation”](#marking-text-in-your-app-for-translation) All text you want to be translatable must be tagged in your application. This can be done as follows: **In Python:**
```python
from django.utils.translation import gettext
def my_view(request):
output = gettext("Welcome to my site.")
return HttpResponse(output)
```
See the [Django docs](https://docs.djangoproject.com/en/stable/topics/i18n/translation/#internationalization-in-python-code) for more. **In Django templates:**
```jinja
{% load i18n %}
{% translate "This is the title." %}
```
See the [Django docs](https://docs.djangoproject.com/en/stable/topics/i18n/translation/#internationalization-in-template-code) for more. **In JavaScript:**
```javascript
document.write(gettext('this is to be translated'));
```
See the [Django docs](https://docs.djangoproject.com/en/stable/topics/i18n/translation/#internationalization-in-javascript-code) for more. **In Wagtail:** See the [Wagtail docs](/wagtail/#internationalization). ## Creating / updating translation files [Section titled “Creating / updating translation files”](#creating--updating-translation-files) After you’ve marked text for translation, you’ll need to update your language files. This can be done by running:
```bash
python ./manage.py makemessages --all --ignore node_modules --ignore venv
python ./manage.py makemessages -d djangojs --all --ignore node_modules --ignore venv
```
Or in Docker:
```bash
make translations
```
Note: if you get any errors you may need to [install gettext](https://stackoverflow.com/q/35101850/8207). ## Adding actual translations for other languages [Section titled “Adding actual translations for other languages”](#adding-actual-translations-for-other-languages) To add a translation for another language you need to edit that languages messages (.po) file. For example, to edit a French translation, you would update `locale/fr/LC_MESSAGES/django.po`. Then search for the text you want to translate, and add the French translation:
```plaintext
msgid "My Team"
msgstr "Mon Équipe"
```
The above lines will replace “My Team” with “Mon Équipe” whenever the French language is configured. After editing any message (.po) file, you will have to compile the messages for the updates to show up in your app. This can be done by:
```bash
python ./manage.py compilemessages
```
Or in Docker:
```bash
make translations
```
## Technical notes [Section titled “Technical notes”](#technical-notes) Pegasus is configured to use cookies to track the current locale. This allows localization to work for both authenticated and unauthenticated users. More information on this approach is available the Django docs: [How Django discovers language preference](https://docs.djangoproject.com/en/stable/topics/i18n/translation/#how-django-discovers-language-preference) ## Timezones [Section titled “Timezones”](#timezones) Pegasus includes support for user’s setting their own time zones via their profile (version 2023.7 and later). When a user sets a timezone, it will be automatically activated by the `UserTimezoneMiddleware` so that by default all dates and times will appear in their local time. For more information on working with timezones in Django, see [Django’s timezone documentation](https://docs.djangoproject.com/en/stable/topics/i18n/timezones/).
# Project/Page Metadata and SEO
> Configure SEO metadata, page titles, social sharing tags, and XML sitemaps for better search engine optimization and discoverability.
Pegasus comes with some built in tools and best-practices for setting page-level metadata (e.g. title, image URL, etc.). ## The `PROJECT_METADATA` setting [Section titled “The PROJECT\_METADATA setting”](#the-project_metadata-setting) Your Pegasus project will ship with a `settings.py` variable called `PROJECT_METADATA` with the following values:
```python
PROJECT_METADATA = {
'NAME': '',
'URL': '',
'DESCRIPTION': '',
'IMAGE': 'https://upload.wikimedia.org/wikipedia/commons/2/20/PEO-pegasus_black.svg',
'KEYWORDS': 'SaaS, django',
'CONTACT_EMAIL': '',
}
```
This information will be available in every view under the variable name `project_meta`. Out of the box, the values are used in a number of places, though can be overridden/modified at the view level. ## Page Titles [Section titled “Page Titles”](#page-titles) The default title for your pages will be your project name and description from `PROJECT_METADATA`. If you want to add a custom page title, you can pass a `page_title` context variable to the template. For example:
```python
def my_new_view(request):
return render('a/template.html', {'page_title': 'My New Page'})
```
Pegasus will then set your title to be `My New Page | `. If you’d like to change the way the title is formatted (e.g. remove the project name), you can change that behavior in `web.templatetags.meta_tags.get_title`. In Pegasus versions after 2022.4 you can also override the title directly in a template by overriding the `page_title` block. For example:
```jinja
{% block page_title %}This title will be used instead of the Pegasus versions{% endblock %}
```
## Sitemaps [Section titled “Sitemaps”](#sitemaps) As of version 2022.6, Pegasus will automatically generate a basic [sitemap](https://developers.google.com/search/docs/advanced/sitemaps/overview) for your site at `sitemap.xml`. Out of the box, the sitemap will only contain your application’s homepage, but can be readily extended by adding URLs in `apps/web/sitemaps.py`. If you have [enabled Wagtail](/wagtail), your sitemap will also include any content managed by Wagtail. Make sure you [properly set the hostname in your Wagtail site](https://docs.wagtail.org/en/stable/reference/contrib/sitemaps.html#setting-the-hostname).
# E-Commerce / Payments
> Build digital storefronts with Stripe integration for one-time and recurring payments, product management, and purchase tracking.
Pegasus (version 2023.9.1 and up) includes an out-of-the-box E-Commerce/Payments demo. In a few clicks you can have a fully functional digital storefront in your application, allowing you to collect and track one-time or recurring payments with Stripe. ## Watch a video [Section titled “Watch a video”](#watch-a-video) To see how this feature works, you can watch the following video: ## Getting Started [Section titled “Getting Started”](#getting-started) ### Set up Stripe Products [Section titled “Set up Stripe Products”](#set-up-stripe-products) First add your products in the Stripe dashboard. Be sure to add readable product names, descriptions, and images, as these will be used for the in-app store. Additionally, make sure each product includes at least one Price. ### Set up your development environment [Section titled “Set up your development environment”](#set-up-your-development-environment) Setting up your development is similar to the [process for subscriptions](/subscriptions), but has fewer steps. 1. If you haven’t already, update the `STRIPE_*` variables in `settings.py` or in your os environment variables to match the keys from Stripe. See [this page](https://stripe.com/docs/keys) to find your API keys. 2. Run `python manage.py bootstrap_ecommerce` to sync your Stripe products and prices to your local database. Once you’ve done this, login and click on the e-commerce tab in the navigation, and you should see your store. ## Data models [Section titled “Data models”](#data-models) ### `ProductConfiguration` [Section titled “ProductConfiguration”](#productconfiguration) What shows up in your store is controlled by the `ProductConfiguration` data model. You can manage these objects from the Django admin (available at locally). For example, to remove a product from the store you can uncheck “is active”. The `ProductConfiguration` model is also a good place to add additional information to your products. For example, you can add additional display data there, or add a `FileField` if you want purchases to grant access to a digital download. ### `Purchase` [Section titled “Purchase”](#purchase) The `Purchase` model is used to record user purchases. A `Purchase` is associated with a `User` and a `ProductConfiguration` and also has details of the Stripe checkout session, date of purchase, and product/price used at the time of purchase. ## Feature gating [Section titled “Feature gating”](#feature-gating) The `@product_required` decorator can be used to restrict access to a view based on whether or not the logged-in user has purchased a particular product. This decorator expects a `product_slug` field in the URL / view with the slug of the `ProductConfiguration` object to be checked. If the user owns the product, they will be granted access to the view. Additionally, if the user gets access, two additional field will be populated on the `request` object: * `request.product_config` will have the `ProductConfiguration` object. * `request.product_purchase` will have the `Purchase` object. If the user does *not* have access to the product, the decorator will redirect them back to the store homepage. ## Webhooks [Section titled “Webhooks”](#webhooks) Like subscriptions, it’s recommended to use webhooks to ensure you receive all updates from Stripe. For the e-commerce store, the only required webhook is `checkout.session.completed`. Follow [the subscriptions documentation](/subscriptions/#webhooks) to set up webhooks in development and production.
# Subscriptions
> Implement SaaS subscriptions with Stripe billing, pricing tables, webhooks, customer portals, and per-seat or usage-based pricing models.
## Overview [Section titled “Overview”](#overview) Subscriptions in Pegasus have three components which must all be setup in order for them to work correctly. 1. **Stripe Billing data**. This is configured in Stripe. 2. **Local Stripe models**. These are synced automatically from Stripe to your local database, using [`dj-stripe`](https://github.com/dj-stripe/dj-stripe). 3. **Pegasus metadata**. This is configured in `apps/subscriptions/metadata.py` and used to augment the data from Stripe. The easiest way to set up all three is to follow the guide below. ## Getting Started [Section titled “Getting Started”](#getting-started) Complete the following steps in order to set up your first subscription workflow. ### Choose your billing setup [Section titled “Choose your billing setup”](#choose-your-billing-setup) If you haven’t already, [set up Pegasus and create an account](/getting-started). In your project settings you will see several options related to subscriptions.  The first option is your *billing model*. Most projects should choose *standard*, which lets you create multiple plans with different monthly or annual prices. Choose *per unit* if you want to charge a variable cost based on the number of units used, for example, if you want to charge for every team-member. [Metered billing](https://stripe.com/docs/billing/subscriptions/usage-based), while not officially supported is largely compatible with the standard model. The second option is your *pricing UI*. If you’re on a standard model or metered billing, it’s recommended to use Stripe’s [embedded pricing table](https://stripe.com/docs/payments/checkout/pricing-table). If you’re using per-unit billing, it’s recommended to choose “managed by your application”. If all of this is intimidating, don’t worry! You can always change these things later. If you’re unsure what you want to use, it is recommended to choose “standard” and “embedded pricing table” to start, as that is the simplest setup and works well for most projects. ### Set up your billing model in Stripe [Section titled “Set up your billing model in Stripe”](#set-up-your-billing-model-in-stripe) Before setting up your development environment for subscriptions, you’ll need to create your billing model in Stripe. You should do this in a test account for development. You’ll eventually be able to copy everything to production once you’re happy with the set up. [Stripe’s documentation](https://stripe.com/docs/billing/subscriptions/build-subscriptions?ui=checkout#create-pricing-model) has guidance on doing this. At a minimum you should create at least one product with a “recurring” price. If you want to offer multiple pricing plans, create one product for each plan. If you want to offer both monthly and annual pricing, make sure every product you add includes both a “monthly” and a “yearly” price. If you are using the Stripe embedded pricing table, you may also want to add product descriptions and [features](https://stripe.com/docs/payments/checkout/pricing-table#product-features), as these will be used on your pricing page. If you are using the Stripe embedded pricing table, you should also set it up now, following the [Stripe pricing table documentation](https://stripe.com/docs/payments/checkout/pricing-table). ### Set up your development environment [Section titled “Set up your development environment”](#set-up-your-development-environment) Once you’ve created your billing model on Stripe, follow these instructions to set up your development environment. 1. Update the `STRIPE_*` variables in your project’s [`.env` file](/configuration/#settings-and-environment-files) to match the keys from Stripe. See [this page](https://stripe.com/docs/keys) to find your API keys. 2. Run `./manage.py bootstrap_subscriptions`. If things are set up correctly, you should see output that includes information about each product / price that you created, and an output starting with `ACTIVE_PRODUCTS = `containing the products you just created. This step will also automatically update your API keys in the Django admin, as described in [dj-stripe’s instructions](https://dj-stripe.dev/api_keys/#adding-new-api-keys). 3. Next, if you are *not* using the Stripe embedded pricing table: 1. Paste the `ACTIVE_PRODUCTS` output from the previous step into `apps/subscriptions/metadata.py` overriding what is there. Update any other details you want, for example, the “description” and “features” fields. 2. Optionally edit the `ACTIVE_PLAN_INTERVALS` variable in `apps/subscriptions/metadata.py` if you don’t plan to include both monthly and annual offerings. 4. Alternatively, if you *are* using the Stripe embedded pricing table set the `STRIPE_PRICING_TABLE_ID` variable in your settings/environment to the pricing table ID you created in Stripe. Now login and click the “Subscription” tab in the navigation. If you’ve set things up correctly you should see a page that looks like this (it will look slightly different if you are using the Stripe pricing table, or a different CSS framework):  ## Configuring the pricing table [Section titled “Configuring the pricing table”](#configuring-the-pricing-table) ### Using the embedded Stripe pricing table [Section titled “Using the embedded Stripe pricing table”](#using-the-embedded-stripe-pricing-table) The following 5-minute video walks through setting up an embedded pricing table in your project. Here are the detailed instructions: If you are using the Stripe embedded pricing table, then all customization happens within the Stripe dashboard. You can change the products, names, descriptions, images, and features by editing the products in Stripe with the desired changes. You can also change the color scheme and other options. After setting up your pricing table, you should add a custom confirmation URL for each product. This tells Stripe to return to your application to properly process the subscription after it is purchased. To do this, edit your pricing table, and under “Payment settings” change the confirmation page setting to “Don’t show confirmation page (Redirect customers to your website.)”. It should look like this:  In the URL box, put the following address for (for development), leaving `{CHECKOUT_SESSION_ID}` exactly like it is written:
```plaintext
http://localhost:8000/subscriptions/confirm/?session_id={CHECKOUT_SESSION_ID}
```
In production, enable https, and replace `localhost:8000` with the url of our site. E.g.
```plaintext
https:///subscriptions/confirm/?session_id={CHECKOUT_SESSION_ID}
```
*Make sure you check the option to apply this change to all prices* if you’re using monthly and annual pricing. And then *repeat this process for every product in the pricing page*. **If you don’t make this change, you will not see subscriptions updated unless you are also running webhooks.** ### Using the in-app pricing table [Section titled “Using the in-app pricing table”](#using-the-in-app-pricing-table) If you are using the in-app pricing table, your pricing table configuration is handled in `metadata.py`. You can modify `ACTIVE_PRODUCTS` and `ACTIVE_PLAN_INTERVALS` and see how the page changes. Whenever you make changes in Stripe, you will need to re-run `./manage.py bootstrap_subscriptions`, and incorporate any necessary changes into the `ACTIVE_PRODUCTS` list. More background and details on this set up can be found in this [Django Stripe Integration Guide](https://www.saaspegasus.com/guides/django-stripe-integrate/). ## Customer Portal [Section titled “Customer Portal”](#customer-portal) Pegasus uses the [Stripe Billing Customer Portal](https://stripe.com/docs/billing/subscriptions/customer-portal) for subscription management after subscription creation To set up the portal you must also enable it in the Stripe dashboard, as outlined in [Stripe’s integration guide](https://docs.stripe.com/customer-management/integrate-customer-portal#configure). After that, most of the set up should be handled by Pegasus. **To use the portal you will also need to set up webhooks as per below. Updates made in the portal will not show up if webhooks are not running.** Pegasus ships with webhooks to handle some common actions taken in the billing portal, including: * Subscription upgrades and downgrades * Subscription cancellation (immediately) * Subscription cancellations (end of billing period) In the Stripe dashboard, you will need to subscribe to a minimum of `customer.subscription.updated` and `customer.subscription.deleted` to ensure subscription changes through the portal make it to your app successfully. For more advanced use cases, read through [Stripe’s integration guide](https://docs.stripe.com/customer-management/integrate-customer-portal). ## Webhooks [Section titled “Webhooks”](#webhooks) Webhooks are used to notify your app about events that happen in Stripe, e.g. failed payments. More information can be found in [Stripe’s webhook documentation](https://stripe.com/docs/webhooks). Pegasus ships with webhook functionality ready to go, including default handling of many events taken in Stripe’s checkout and billing portals. That said, you are strongly encouraged to test locally using [Stripe’s excellent guide](https://stripe.com/docs/webhooks/test). ### Webhooks in development [Section titled “Webhooks in development”](#webhooks-in-development) In development, the easiest way to set up webhooks is with the [Stripe CLI](https://stripe.com/docs/stripe-cli). First install the CLI and set it up. Then print your CLI secret with:
```bash
stripe listen --print-secret
```
Or with Docker (no install required):
```bash
docker run --network host --rm -it stripe/stripe-cli listen \
--print-secret \
--api-key sk_test_
```
Then you can set up your webhook endpoint by running:
```bash
./manage.py bootstrap_dev_webhooks --secret
```
This will create a webhook endpoint for `djstripe` in your application. The `bootstrap_dev_webhooks` will also output a stripe command you can then use to listen for webhooks. It will look something like this, with the `` replaced by your own webhook endpoint’s ID:
```bash
stripe listen \
--forward-to http://localhost:8000/stripe/webhook//"
```
Or in Docker:
```bash
docker run --network host --rm -it stripe/stripe-cli listen \
--forward-to localhost:8000/stripe/webhook// \
--api-key sk_test_
```
### Webhooks in production [Section titled “Webhooks in production”](#webhooks-in-production) The webhook setup changed significantly in version 2025.4.1. If you are on version 2025.4.1 or later, follow these steps, which are taken from the dj-stripe docs. For versions earlier than 2025.4.1 see the next section. * As a superuser, visit the Django admin of your site and navigate to djstripe -> Webhook endpoints -> Add webhook endpoint (or /admin/djstripe/webhookendpoint/add/). * Select your Stripe account, check “Live mode” and verify the Base url matches your server’s domain, e.g. . * Under “Advanced” verify the API version is correct. * Under “Advanced”, choose the enabled events you want to listen for. At a minimum you want: * `checkout.session.completed` * `customer.subscription.updated` * `customer.subscription.deleted` * You can add other webhooks as well, (or choose `*` to enable all webhooks) but these are the minimum set required for subscriptions and the billing portal to work properly. * Verify the other settings (the defaults should be fine) and click “Save”. After completing these steps, visit your [Stripe dashboard](https://dashboard.stripe.com/webhooks) and confirm the new webhook endpoint has been synced to Stripe. Secrets are managed by dj-stripe, and the webhook should be working! In production, you should not need to run `stripe listen --forward-to localhost:8000/stripe/webhook/` (or the Docker equivalent). Once webhooks are properly set up, all underlying Stripe data will be automatically synced from Stripe with no additional setup required on your part. #### Legacy setup before Pegasus 2025.4.1: [Section titled “Legacy setup before Pegasus 2025.4.1:”](#legacy-setup-before-pegasus-202541) **These instructions are only for projects running prior to Pegasus version 2025.4.1. For recent projects, use the instructions above.** * Navigate to this page [Webhooks](https://dashboard.stripe.com/webhooks) (assuming you’re logged into Stripe). * Toggle off test mode in the top right corner. * Click on `Add endpoint`. * In the `Endpoint URL` field, enter the following URL, replacing `yourserver.com` with your server’s domain name. Note: **the trailing slash is required.** * `https://yourserver.com/stripe/webhook/` * Click on `Select Events to Listen To`. * Search for `checkout.session.completed`, `customer.subscription.updated`, and `customer.subscription.deleted`, and select them. These events are connected by default (see `apps/subscriptions/webhooks.py` for the source code). You can add other webhooks as well, but these are the minimum set required for subscriptions and the billing portal to work properly. * Write a description if needed and then click `Add endpoint`. * **Ensure to set `DJSTRIPE_WEBHOOK_SECRET` in your `settings.py` or as an environment variable.** This value can be found in the Stripe dashboard where you configure your webhook and may be referred to as the `Signing Secret`. ### Custom Webhook Handling [Section titled “Custom Webhook Handling”](#custom-webhook-handling) You may want to do more than just update the underlying Stripe objects when processing webhooks, for example, notifying a customer or admin of a failed payment. Pegasus ships with an example of executing custom logic from a webhook in `apps/subscriptions/webhooks.py`. This basic example will mail your project admins when a Subscription is canceled. More details on custom webhooks can be found in the [dj-stripe documentation](https://dj-stripe.dev/2.9/usage/webhooks/). ## Supporting multiple currencies [Section titled “Supporting multiple currencies”](#supporting-multiple-currencies) If you use Stripe’s embedded pricing table you get multi-currency support out of the box. Follow [the Stripe guide](https://stripe.com/docs/payments/checkout/present-local-currencies?platform=multi-currency-prices) to set your products and prices up for multiple currencies. If you use an in-app pricing table, Stripe will still present your prices to customers in local currencies, but the pricing table itself will display the prices in your default currency. ## Free trials [Section titled “Free trials”](#free-trials) You can easily enable free trials using the option in Stripe’s embedded pricing table. Your customers will be able to sign up with their credit cards for a trial and will have the same experience in your application as someone who is paying for the plan. They’ll be able to update their status from the customer portal, and once the trial period ends they will be billed. If you’re using trials you must set up webhooks to be notified whether the customer subscribes or cancels at the end of their trial. It is also possible to use free trials without the embedded pricing table. To do so, you need to add a `trial_end` or `trial_period_days` value to the `subscription_data` in `create_stripe_checkout_session`, as described in [the Stripe documentation](https://stripe.com/docs/billing/subscriptions/trials). ## Feature-Gating [Section titled “Feature-Gating”](#feature-gating) Pegasus ships with a demo page with a few feature-gating examples, which are available from a new Pegasus installation under the “Subscription Demo” tab. These include: 1. Changing content on a page based on the user/team’s subscription. 2. Restricting access to an entire page based on the user/team’s subscription. 3. Showing subscription details like plan, payment details, and renewal date. ### Using the `active_subscription_required` decorator [Section titled “Using the active\_subscription\_required decorator”](#using-the-active_subscription_required-decorator) One common use-case is restricting access to a page based on the user’s subscription. Pegasus ships with a decorator that allows you to do this. You can use it as follows:
```python
@login_required
@active_subscription_required
def subscription_gated_page(request, subscription_holder=None):
return TemplateResponse(request, 'subscriptions/subscription_gated_page.html')
```
If the user doesn’t have an active subscription, they’ll be redirected to the subscription page to upgrade. You can also restrict access based on a specific plan (or set of plans), as follows:
```python
@login_required
@active_subscription_required(limit_to_plans=["pro", "enterprise"])
def subscription_gated_page(request, subscription_holder=None):
return TemplateResponse(request, 'subscriptions/subscription_gated_page.html')
```
In this case the user will only be allowed to view the page if they have a pro or enterprise plan. ### Using the `get_feature_gate_check` helper function [Section titled “Using the get\_feature\_gate\_check helper function”](#using-the-get_feature_gate_check-helper-function) For more fine-grained control you can use the `get_feature_gate_check` helper function. This takes in two arguments, the `subscription_holder` (usually a User or Team), and optionally the same `limit_to_plans` list above, and returns a `FeatureGateCheckResult`, which includes whether the check passed (subscription holder has a subscription of the right type), and an optional message explaining the answer. Example usage:
```python
from apps.subscriptions.feature_gating import get_feature_gate_check
def my_view(request):
check_result = get_feature_gate_check(request.team, ["professional"])
if check_result.passed:
do_actions_pro_only()
else:
logging.info(f"Pro actions skipped: {check_result.message}")
```
## Per-Unit / Per-Seat Billing [Section titled “Per-Unit / Per-Seat Billing”](#per-unit--per-seat-billing) Pegasus supports per-unit / per-seat billing. Choose this option when building your project to enable it. **It is not recommended to use the Stripe embedded pricing table if you are using per-unit billing.** For Team-based builds the default unit is Team members. For non-Team builds you will have to implement your own definition of what to use for billing quantities. Here is [a short video walkthrough of this feature](https://youtu.be/v_ayMEj924w). ### Choosing your billing model [Section titled “Choosing your billing model”](#choosing-your-billing-model) Refer to the [Stripe documentation](https://stripe.com/docs/products-prices/pricing-models) for how to set this up in your Price model. You can use any of: * Standard pricing (e.g. $10/user) * Package pricing (e.g. $50 / 5 new users) * Tiered pricing (graduated or volume) (e.g. $50 for up to 5 users, $5/user after that) ### Displaying prices on the subscriptions page [Section titled “Displaying prices on the subscriptions page”](#displaying-prices-on-the-subscriptions-page) For per-unit billing you can no longer display a single upgrade price since it is dependent on the number of units. To avoid displaying an “unknown” price when showing the subscription, you can add a `price_displays` field to your `ProductMetadata` objects that takes the following format:
```python
ProductMetadata(
stripe_id='',
name=_('Graduated Pricing'),
description='A Graduated Pricing plan',
price_displays={
PlanInterval.month: 'From $10 per user',
PlanInterval.year: 'From $100 per user',
}
),
```
This will show “From $10 per user” or “From $100 per user” when the monthly or annual plan is selected, respectively. #### Per-seat pricing and the embedded pricing table. [Section titled “Per-seat pricing and the embedded pricing table.”](#per-seat-pricing-and-the-embedded-pricing-table) **Though it is possible to use the pricing table with per-seat billing, it is not recommended.** This is because Stripe does not allow you to pass per-seat quantities with the Pricing Table, so if you use the pricing table with per-seat pricing, your users will be able to choose the number of “seats” (quantity) when they check out. This is different from the expected behavior, which is that your app sets the quantity explicitly based on the number of team members (or your own business logic, for user-based builds). Since Pegasus is set up to automatically update the subscription quantity based on the “usage” in your application, this could result in your users buying a certain number of seats, and then unexpectedly having the price change to the amount they are actually using. You can disable Pegasus automatically updating the per-seat quantity, and then modify your application’s business logic to be based off the “quantity” property of the user/team’s subscription, though this is not an officially supported workflow. ### Keeping your Stripe data up to date [Section titled “Keeping your Stripe data up to date”](#keeping-your-stripe-data-up-to-date) When changes are made that impact a user’s pricing, you will need to notify Stripe of the change. This should happen automatically every 24 hours as long as you have enabled celery and celerybeat. You can also trigger it manually via a management command `./manage.py sync_subscriptions`. To ensure this command works properly, you must implement two pieces of business logic: 1. You must update the billing model’s `billing_details_last_changed` field any time the number of units has change. 2. You must override the `get_quantity` function on your billing model to tell Stripe how many units it contains. **If you use Teams with per-seat billing this will be automatically handled for you by default.** All you have to do is run the management command or connect it to a periodic task. For User-based, or more complex billing models with Teams you will have to implement these changes yourself. #### A User-based example [Section titled “A User-based example”](#a-user-based-example) Here’s a quick example of how you might do this with User-based billing. Let’s say your app allows users to define workspaces and they are billed based on the number of workspaces they create. You might have a workspace model that looks like this:
```python
class Workspace(models.Model):
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='workspaces')
# other workspace fields here
```
Then you would want to update the `billing_details_last_changed` field of the `CustomUser` object every time a workspace was added or removed (step 1, above). That might look something like this, using [Django signals](https://docs.djangoproject.com/en/stable/topics/signals/):
```python
@receiver(post_save, sender=Workspace)
def update_billing_date_on_workspace_creation(sender, instance, created, **kwargs):
if created:
instance.user.billing_details_last_changed = timezone.now()
instance.user.save()
@receiver(post_delete, sender=Workspace)
def update_billing_date_on_workspace_deletion(sender, instance, **kwargs):
instance.user.billing_details_last_changed = timezone.now()
instance.user.save()
```
The other piece of code you would need to add is associating the `get_quantity` function on the user with the number of workspaces they have. You’d want to add a method like this to `CustomUser`:
```python
class CustomUser(SubscriptionModelBase, AbstractUser):
# other stuff here
def get_quantity(self):
return self.workspaces.count()
```
## Stripe in Production [Section titled “Stripe in Production”](#stripe-in-production) In development, you will use your Stripe test account, but when it comes time to go to production, you will want to switch to the live account. This entails: 1. Setting `STRIPE_LIVE_MODE` to `True` in your settings/environment. 2. Populating `STRIPE_LIVE_PUBLIC_KEY` and `STRIPE_LIVE_SECRET_KEY` in your environment. 3. Updating your `ACTIVE_PRODUCTS` to support both test and live mode (see below) ### Managing Test and Live Stripe Products [Section titled “Managing Test and Live Stripe Products”](#managing-test-and-live-stripe-products) When you run `bootstrap_subscriptions` Pegasus will generate a list of your `ACTIVE_PRODUCTS` that includes hard-coded Stripe Product IDs. This works great in development, but presents a problem when trying to enable live mode. One way to workaround this is to replace the hard-coded product IDs with values from your django settings. E.g. in `apps/subscriptions/metadata.py` change from:
```python
ACTIVE_PRODUCTS = [
ProductMetadata(
stripe_id='prod_abc', # change this line for every product
slug='starter',
...
```
To:
```python
ACTIVE_PRODUCTS = [
ProductMetadata(
stripe_id=settings.STRIPE_PRICE_STARTER, # to something like this
slug='starter',
...
```
Then in your `settings.py` file, you can define these values based on the `STRIPE_LIVE_MODE` setting:
```python
STRIPE_LIVE_MODE = env.bool("STRIPE_LIVE_MODE", False)
STRIPE_PRICE_STARTER = "prod_xyz" if STRIPE_LIVE_MODE else "prod_abc"
```
You will have to do this for each of your products. ## Troubleshooting [Section titled “Troubleshooting”](#troubleshooting) **Stripe is not returning to the right site after accessing checkout or the billing portal.** There are two settings that determine how Stripe will call back to your site. If Stripe is returning to the *wrong site entirely* it is likely a problem with your Django `Site` configuration. See the documentation on [absolute URLs](/configuration/#absolute-urls) to fix this. If Stripe is returning to the correct site, *but over HTTP instead of HTTPS* (or vice versa) then you need to change the `USE_HTTPS_IN_ABSOLUTE_URLS` setting in `settings.py` or a production settings file. **Subscriptions are not being created in your app when using the embedded pricing table.** Make sure that you have updated the confirmation page for every product and every price to `https:///subscriptions/confirm/?session_id={CHECKOUT_SESSION_ID}` as described above. You can also turn on webhooks to fix this, though it’s recommended to use the custom confirmation page to provide a better user experience. **Stripe webhooks are failing with a signature error.** If you get an error like “No signatures found matching the expected signature for payload” or similar there are a few things to check: First, double check all of your API keys and secrets in your environment/settings files. These are: * `STRIPE_LIVE_PUBLIC_KEY` and `STRIPE_LIVE_SECRET_KEY` (for live mode), or `STRIPE_TEST_PUBLIC_KEY` and `STRIPE_TEST_SECRET_KEY` (for test mode) * `STRIPE_LIVE_MODE` should match whether you’re in live / test mode. * `DJSTRIPE_WEBHOOK_SECRET` should match the secret from the Stripe dashboard. **Getting “DataError: value too long for type character varying(9)” from webhooks** This is caused by an issue in `dj-stripe` where a database column is not large enough to support values that were added by Stripe in API version xxx. There are two recommended workarounds to this: 1. Downgrade your Stripe API version. The most recent supported version is `2023-10-16`. Your API version can be found in the “Developers” section of the Stripe dashboard. 2. Manually add a database migration to your project to increase the size of the column. If you build with Pegasus version 2024.10 or later, this migration will be automatically added to your project. Otherwise, see below for instructions on adding it. Run:
```bash
python manage.py makemigrations web --empty`
```
Then copy the following into the generated file:
```python
class Migration(migrations.Migration):
dependencies = [
("web", "0001_initial"),
("djstripe", "0012_2_8"),
]
operations = [
migrations.RunSQL(
"ALTER TABLE djstripe_paymentintent ALTER COLUMN capture_method TYPE varchar(255);"
),
]
```
*You can [read more about this issue and workarounds here](https://github.com/dj-stripe/dj-stripe/issues/2038#issuecomment-2119244742).*
# Using Teams
> Build multi-tenant applications with team-based data models, role-based permissions, and collaborative user management features.
Teams are designed to provide sandboxes for groups of users collaborating on single project. Users can join one or more teams, invite other users to their teams, and give different team members different roles. Pegasus provides the building blocks to setup a team-based application. Some of those building blocks are documented here. **Note: all of the following examples assume you have setup Pegasus with teams enabled.** ## Example App [Section titled “Example App”](#example-app) Pegasus ships with an optional built-in example application demonstrating the basics of working with team-based models and views. The example app includes: 1. A data model that belongs to a team. 2. A set of class based views for working with that data model, limited to the context of a team. #### Third party examples [Section titled “Third party examples”](#third-party-examples) A Pegasus user Peter Cherna has created some more [example applications](https://github.com/pcherna/pegasus-example-apps/) that demonstrate additional team-based examples, including functional views, pagination, APIs and working with “global” objects. They are a great place to start for inspiration and getting something up and running quickly! *Note: the example apps are not officially sanctioned/supported by Pegasus—though features from them will be continually incorporated into future releases.* ## Data Models [Section titled “Data Models”](#data-models) Teams use three primary models - `apps.users.CustomUser`, `apps.teams.Team`, and `apps.teams.Membership`. The `Membership` model uses [Django’s “through” support](https://docs.djangoproject.com/en/stable/ref/models/fields/#django.db.models.ManyToManyField.through) to extend the `User`/`Team` relationship with additional fields. By default, a `role` field is added to represent the `User`’s role in the `Team` (admin or member). ### Team-based models [Section titled “Team-based models”](#team-based-models) Data models that “belong” to a Team can subclass `BaseTeamModel`. In addition to always including a `.team` field, this model also has an additional manager called `for_team` that will automatically filter querysets based on the current team (set by the [context variable](#team-context-variable)). This is useful to use to provide strict access to your data models (if they should never be accessed outside a team). These two statements are approximately equivalent:
```python
MyTeamModel.for_team.filter(...)
MyTeamModel.objects.filter(team=get_current_team(), ...)
```
See the example app for additional usage examples. For more strict logic around team queries, see [the strict team access section](#strict-team-access). ## Team Context variable [Section titled “Team Context variable”](#team-context-variable) From version 2025.11, a [ContextVar](https://docs.python.org/3/library/contextvars.html) is available to keep track of the current team. The team is set automatically in views by the [middleware](#middleware) or by using the `current_team` context manager like this:
```python
a_team = Team.objects.get(slug='a-team')
with current_team(a_team):
# get_current_team() will return the `a_team` object inside this block
call_code_that_uses_the_curent_team()
MyModel.for_team.all() # only returns objects with `team=a_team`
```
## Team-scoped shortcuts [Section titled “Team-scoped shortcuts”](#team-scoped-shortcuts) From version 2026.6, Pegasus includes a `get_team_object_or_404` shortcut for looking up a single object scoped to the current team. It mirrors Django’s `get_object_or_404` but resolves the team automatically from the [team context variable](#team-context-variable):
```python
from apps.teams.shortcuts import get_team_object_or_404
def detail_view(request, team_slug, pk):
obj = get_team_object_or_404(MyTeamModel, pk=pk)
...
```
This is equivalent to either of the following, but with an API that matches Django’s stock shortcut:
```python
get_object_or_404(MyTeamModel.for_team, pk=pk)
get_object_or_404(MyTeamModel, team=get_current_team(), pk=pk)
```
If there is no team in context (for example, in a Celery task that didn’t set one), the call raises a 404. This is especially useful if you are using [strict team access](#strict-team-access), where the model’s default manager is `all_objects` (unfiltered) — so plain `get_object_or_404(MyTeamModel, ...)` would bypass the team filter. ## Team-based Views [Section titled “Team-based Views”](#team-based-views) At its core, all Team-based views need the following: ### Urls [Section titled “Urls”](#urls) See `apps.teams.urls` for an example of how to set these up in your apps, and your main `apps.{project}.urls` file for how to add them to your site’s URLs. Anything that goes into `team_urlpatterns` in `apps.{project}.urls` will automatically be added under the URL `https://example.com/a//`. The `team_slug` is a human-readable, URL-friendly version of the team name that is auto-generated for you. ### Middleware [Section titled “Middleware”](#middleware) The `apps.teams.middleware.TeamsMiddleware` must be included in the list of middleware. It must be placed after `django.contrib.auth.middleware.AuthenticationMiddleware`. The purpose of this middleware is to set `request.team` and `request.team_membership` based on the current request. It will attempt to load the team as follows: * From the `team_slug` in the request path if available * From the current session if available * From the user’s list of teams if available If the `team_slug` is available from the request path but it does not match a team that the user has access to then the request will terminate with a 404. Apart from this the middleware does not do any validation of the team or the team membership. That is left to the decorators described below. ### Views [Section titled “Views”](#views) See `apps.team.views` for example team views. All views that are referenced under `team_urlpatterns` must contain `team_slug` as the first argument. In addition to adding this field, you will likely want to use one of the built-in permission decorators (see below) to ensure the logged-in user can access the selected team. Additionally, you will have to scope any data model access to the relevant Team in any Database/ORM queries you make inside your views. ## Permission Control [Section titled “Permission Control”](#permission-control) Pegasus includes two convenience decorators for use in team views. These can be found in `apps.teams.decorators`. #### The `login_and_team_required` decorator [Section titled “The login\_and\_team\_required decorator”](#the-login_and_team_required-decorator) This decorator can be used to ensure that the logged in user has access to the team in the view. It requires your view takes in a `team_slug`, as in the example views. It can be used in functional views like this:
```python
@login_and_team_required
def a_team_view(request, team_slug):
# other view logic here
return render(request, 'web/my_template.html', context={
'team': request.team,
})
```
Or in class-based views like this:
```python
@method_decorator(login_and_team_required, name='dispatch')
class ATeamView(View):
# other view details go here
```
If the current user does not have access to the team they will see a 404 page. If no user is logged in they’ll be redirected to a login view, just like the `login_required` decorator. ### The `team_admin_required` decorator [Section titled “The team\_admin\_required decorator”](#the-team_admin_required-decorator) The `team_admin_required` decorator works just like the `login_and_team_required` decorator, except in addition to checking team membership the role is also checked and if the user doesn’t have “admin” access they will not be able to access the view. ### The `LoginAndTeamRequiredMixin` and `TeamAdminRequiredMixin` classes [Section titled “The LoginAndTeamRequiredMixin and TeamAdminRequiredMixin classes”](#the-loginandteamrequiredmixin-and-teamadminrequiredmixin-classes) These mixins provide the same functionality as the decorators, but are designed to work with Django’s generic class-based views. They can be used like this:
```python
class ATeamModelListView(LoginAndTeamRequiredMixin, ListView):
model = MyModel
```
See the example app for more details. ### Template tags [Section titled “Template tags”](#template-tags) In addition to the decorators, you can also use template tags to check user / team access from a template. This can be useful for hiding/showing certain content based on a user’s team role. The `is_member_of` filter can be used to check team membership, and the `is_admin_of` filter can be used to check if *a* user is a team admin. For example, the following will show only if the logged in user is an admin of the associated team:
```jinja
{% load team_tags %}
{% if team and request.user|is_admin_of:team %}
You're an admin of {{team.name}}.
{% elif team and request.user|is_member_of:team %}
You're a member of {{team.name}}.
{% else %}
Sorry you don't have access to {{team.name}}.
{% endif %}
```
### Adding Roles [Section titled “Adding Roles”](#adding-roles) The permission system is designed to be simple enough to easily use, but extensible enough that you can customize it to match your project’s needs. Here’s how you can add a new role to your app: #### 1. Define the New Role in `roles.py` [Section titled “1. Define the New Role in roles.py”](#1-define-the-new-role-in-rolespy) First, you need to add your new role constant and update the choices in `apps/teams/roles.py`:
```python
ROLE_ADMIN = 'admin'
ROLE_MEMBER = 'member'
ROLE_MODERATOR = 'moderator' # Add your new role here
ROLE_CHOICES = (
# customize roles here
(ROLE_ADMIN, 'Administrator'),
(ROLE_MEMBER, 'Member'),
(ROLE_MODERATOR, 'Moderator'), # Add your new role choice here
)
```
Technically, this is all that’s needed, as this will cause the role to show up in the invitation UI and allow it to be used in team memberships. However, you’ll probably also want to use the role in your app permissions system. To do that, you should also add a helper function for the new role if you want to use it in permission checks:
```python
def is_moderator(user: CustomUser, team) -> bool:
if not team:
return False
from .models import Membership
return Membership.objects.filter(team=team, user=user, role=ROLE_MODERATOR).exists()
```
#### 2. Update the Membership Model (if needed) [Section titled “2. Update the Membership Model (if needed)”](#2-update-the-membership-model-if-needed) The `Membership` model in `models.py` already uses `roles.ROLE_CHOICES` for its role field, so it will automatically pick up your new role. However, you might want to add a helper method to the `Membership` model:
```python
class Membership(BaseModel):
# ... existing code ...
def is_moderator(self) -> bool:
return self.role == roles.ROLE_MODERATOR
```
#### 3. Add a new decorator (if needed) [Section titled “3. Add a new decorator (if needed)”](#3-add-a-new-decorator-if-needed) If you’d like to use the role in decorators, similar to `@team_admin_required` you can do so by adding a new function to `apps/teams/decorators.py`:
```plaintext
# import the and use function you created in step 1
from .roles import is_admin, is_member, is_moderator
def team_moderator_required(view_func):
return _get_decorated_function(view_func, is_moderator)
```
#### 4. Use the role [Section titled “4. Use the role”](#4-use-the-role) You’ll need to update any views or logic that handle role-based permissions, by calling the helper functions and decorators you’ve defined above. The specifics here will depend on the role you’ve added and the goals you’re trying to achieve with it. ## Background Tasks [Section titled “Background Tasks”](#background-tasks) From version 2026.6, for Celery tasks that operate on team-scoped data, use the `@team_task` decorator from `apps.teams.celery`. It loads the team and enters the [team context](#team-context-variable) for the duration of the task, so the team-scoped `objects` manager works inside the task body the same way it does in views handled by the [middleware](#middleware). The convention is that the task’s first parameter (after `self` when `bind=True`) is a `team_id`. It can be passed either positionally or as a keyword argument:
```python
from apps.teams.celery import team_task
@team_task(bind=True, max_retries=2, queue="default")
def process_team_data(self, team_id, payload_id):
payload = Payload.objects.get(pk=payload_id) # automatically scoped to team_id
# ... do work ...
```
Enqueue it like any other Celery task:
```python
process_team_data.delay(team.id, payload.id) # positional
process_team_data.delay(team_id=team.id, payload_id=...) # kwarg
```
`@team_task` accepts all the same options as Celery’s `@shared_task` (`bind`, `max_retries`, `rate_limit`, `queue`, etc.) and forwards them through. If the team does not exist, the task raises `Team.DoesNotExist` rather than silently no-opping. A task enqueued against an invalid team is treated as a programming bug, not a recoverable condition. ## Cookbooks [Section titled “Cookbooks”](#cookbooks) ### Strict team access [Section titled “Strict team access”](#strict-team-access) If you want to ensure that your data models are only ever accessible in the context of a team, you can change the declaration of the manager classes on `BaseTeamModel` as follows:
```python
class BaseTeamModel(BaseModel):
"""
Abstract model for objects that are part of a team.
"""
team = models.ForeignKey(Team, verbose_name=gettext("Team"),
on_delete=models.CASCADE)
# rename the global manager to `all_objects`.
# This will be used in the Django admin, but any calling code
# using `.objects` will automatically filter by team.
all_objects = models.Manager()
# Override `.objects` with the TeamScopedManager to always filter
# queries by the current team.
objects = TeamScopedManager()
class Meta:
abstract = True
```
You can also set `settings.STRICT_TEAM_CONTEXT = True` to fail hard if `.objects` is ever called without a valid team set. If you make this change, you never need to add `.filter(team=team)` to any of your queries, as the filter will be applied automatically anytime you reference `TeamModel.objects`. ### Partially using teams [Section titled “Partially using teams”](#partially-using-teams) Many projects might want to use teams in the background but hide them from users. This can be useful in certain scenarios: * If you know you want to use teams eventually, but haven’t built out support for them yet. * If your application has different user types, some of which belong to teams and some of which don’t. The recommended way to handle this situation is to **enable teams in Pegasus, but hide/restrict them in the UI**. In this world, all users will still belong to a default team (which is created for them automatically), and all models are associated with teams. However, the concept of teams will be hidden from users until you decide to make them visible. This allows you to easily “turn on” teams when you are ready, or migrate a user from a “non-team” to a “team” account, while being able to use the same underlying business logic and not have to deal with complicated data migrations. To achieve this, you should do something like the following: 1. Build your application with teams enabled. This will provide the data models and URL scaffolding to work with teams, and make using teams later much easier. 2. Hide the `team_name` field from the signup form (`signup.html`), and let teams be auto-created for new users. 3. Hide the team-related items from the application navigation (entry point is `app_nav.html`). 4. (Optional) Hide/remove out the team-based url mappings (entry point is `teams/urls.py`). Do this if you don’t want people to be able to access team-related functionality even if they navigate to the right URL in the browser. 5. Continue building your application using the team-based models and URL patterns, but don’t expose them to the user. If you follow these steps it should be relatively easy to expose teams down the line instead of having to deal with a complicated migration. That process will mostly involve un-hidng the team functionality that was hidden. ### Renaming “teams” in your application [Section titled “Renaming “teams” in your application”](#renaming-teams-in-your-application) Many projects may want to rename “Teams” to something else, e.g. “Organizations”. To rename teams it is recommended to rename it in the UI only and not update the data models or app name. Renaming it in the models and app will make future Pegasus upgrades much more difficult. To rename teams in the UI, you can search for “teams” in the codebase and update the references, which are mostly in the template and javascript files.
# Information about Templates
> Overview of Django template structure and organization in Pegasus projects for building consistent user interfaces.
*This documentation is a work in progress* All Pegasus templates are found in `/pegasus/templates/`. These include Pegasus’s own templates, as well as overridden templates from other apps. Pegsaus’s templates are found in `/pegasus/templates/pegasus`. The `app` directory contains templates within the application, e.g. after sign in. Any `components` subdirectory contains partial templates relevant to that directory. E.g. `app/components` contains partial templates used within the application.
# Upgrading and Changing your Project Settings
> Upgrade Pegasus projects to new versions using GitHub integration, git branches, or patch files with conflict resolution strategies.
There are several ways to update your Pegasus project. These methods can be used to upgrade your project to a new Pegasus version, or when changing anything in your project configuration. ## Using an AI Agent [Section titled “Using an AI Agent”](#using-an-ai-agent) The easiest way to upgrade your project is with the [Pegasus agent skills](https://github.com/saaspegasus/pegasus-skills). ### Prerequisites [Section titled “Prerequisites”](#prerequisites) * Set up your project with the Gihtub integration. * Install the [agent skills from the repository](https://github.com/saaspegasus/pegasus-skills). * Install the [`pegasus-cli`](https://github.com/saaspegasus/pegasus-cli) version 0.12 or later. Note: These will be installed by default in Pegasus projects created/updated after version 2026.2.2. Skills are installed if you choose “Claude Code” as one of your project’s enabled coding assistants. ### Upgrading [Section titled “Upgrading”](#upgrading) After setting up the prerequisites you should be able to run the `/upgrade-pegasus` skill in Claude code, or ask it to “upgrade your pegasus project”. The agent will then run through the upgrade steps for you. After it completes, you should review the code changes carefully before merging. You can watch a demo of this set up here: ## Using the Github integration [Section titled “Using the Github integration”](#using-the-github-integration) The easiest way to upgrade your project is to use the built-in Github integration. If you created your project with Github, you should be able to make any changes you want to your project configuration and then create a pull request with the updated code from the “Download” page. If you did not create your project with the Github you can still use this method. First follow the instructions for [connecting an existing project to Github](/github/#connecting-an-existing-project-to-github). After completing that step, you should be able to submit updates to your project via pull request, just like above. You can watch a demo of this set up here: Note: Whenever you merge Pegasus pull requests you should **use the Github option to “Create a merge commit”**. Do NOT use “Squash and merge” or “Rebase and merge”, as they could prevent future updates from merging cleanly into your project. ## Manually, using branches [Section titled “Manually, using branches”](#manually-using-branches) If you don’t want to, or can’t, use the Github integration, you can manually upgrade your project with git branches. Note that this is a longer and more complicated process than using the Github integration, which handles most of these steps for you. With this option you maintain a “pure” Pegasus branch in your repository with no other modifications. Then, you merge this branch into your main app when you upgrade. This process is outlined below, and also in the below screencast which shows a live example on a real Pegasus project. Here are the steps to take: ### 1. Create a branch for the upgrade [Section titled “1. Create a branch for the upgrade”](#1-create-a-branch-for-the-upgrade) First [checkout the first commit](https://stackoverflow.com/questions/43197105/how-do-you-jump-to-the-first-commit-in-git) in your repository and create a new branch from there. After finding and checking out the initial commit, run:
```bash
git branch pegasus
git checkout pegasus
```
*Note: if you created the `pegasus` branch when you set up your codebase you can skip this step. Alternatively, if you don’t have any commit with pure pegasus code, see the instructions at the bottom of this page to create one.* Next, make sure the branch is up-to-date with your current Pegasus version: 1. Download your Pegasus project on your *current* version and unzip the code. 2. Copy the `.git` folder from your main project into the downloaded codebase. 3. Make sure you are on the `pegasus` branch (`git checkout pegasus`) 4. Commit all changes (`git add .` then `git commit -am "ready to upgrade"`) ### 2. Upgrade the code in the branch [Section titled “2. Upgrade the code in the branch”](#2-upgrade-the-code-in-the-branch) 1. Upgrade your project on saaspegasus.com 2. Download the latest codebase and unzip the code. 3. Copy the `.git` folder from step 1 into this new folder. 4. Commit all changes (`git add .` then `git commit -am "upgrade to latest Pegasus"`) ### 3. Merge into your main branch [Section titled “3. Merge into your main branch”](#3-merge-into-your-main-branch) 1. Checkout the (latest/current) main branch (`git checkout main`) 2. Merge the code (`git merge pegasus`) Alternatively you may wish to do this in a new branch and then submit a pull request to the main branch from there: 1. Create a new branch off of the main branch (`git checkout main; git checkout -b upgrade-pegasus`) 2. Merge the code (`git merge pegasus`) In the merging step you should look at the modifications being made, and you may have to manually resolve conflicts that come up. You may also need to run `./manage.py makemigrations` to create any database migrations that were not included with Pegasus. ## Manually, using patches (if you can’t use Github or branches) [Section titled “Manually, using patches (if you can’t use Github or branches)”](#manually-using-patches-if-you-cant-use-github-or-branches) You can also follow a similar process to the above using Git patches. Patches do not require working in the same repository or having a previously created branch. At a high level you will: 1. Create a patch file containing the changes in the upgrade. 2. Apply the patch to your app. Here we’ll walk through the steps in more detail. ### 1. Creating the patch file [Section titled “1. Creating the patch file”](#1-creating-the-patch-file) Follow these steps to create your patch file: 1. Download a “clean” version of your Pegasus project on your *current* version, and commit it to a git branch or repository. 2. Upgrade your Pegasus version (or change your configuration), and download the new codebase. 3. Copy your `.git` directory from your “clean” project in step 1 into your new project in step 2. E.g. `cp -r path/to/yourapp/.git path/to/newapp/`. 4. In your new project directory, *commit all of the changes* in a single commit. 5. Create a patchfile for the commit using [git-format-patch](https://git-scm.com/docs/git-format-patch). The recommended command to run is `git format-patch -1 HEAD`. You should now see a file in your repository root with a name like `0001-branch-details.patch`. This is your patch file. ### 2. Applying the patch file [Section titled “2. Applying the patch file”](#2-applying-the-patch-file) Now return to your main branch in your application’s repository. First, use [git-apply](https://git-scm.com/docs/git-apply) to apply the patch. The recommended command to run is:
```bash
git apply --ignore-space-change --ignore-whitespace --reject /path/to/.patch
```
substituting the path/name of the patchfile created above. This command will do a best-effort application of the patch. For each affected file: 1. If updates could be applied cleanly, the file will be updated with the contents of the applied patch. 2. If updates could not be applied cleanly, a new diff file called `.rej` will be created, showing the diff that could not be applied. If the file was partially updated then the file will be modified *and* the remaining changes will be visible in the `.rej` file. The last step of the upgrade process is to go through each file and: 1. If the file has been modified, look at the modifications, see if you want them, and commit/reject them as necessary. 2. If the file has a `.rej` file, look at the proposed diff and see if you want to manually apply it, or ignore it. After you have merged all changes to a file, you should delete the `.rej` file. To understand the format of the `.rej` files, take a look at the [unified diff format](https://en.wikipedia.org/wiki/Diff#Unified_format). Basically changes will look like the below, with a line starting with a minus sign, indicating a removal, and a plus sign indicating an addition. In this example, the type annotations were added to the function signature:
```bash
-def is_member(user, team):
+def is_member(user: CustomUser, team: apps.teams.models.Team) -> bool:
```
## Conflict Resolution Tips and Tricks [Section titled “Conflict Resolution Tips and Tricks”](#conflict-resolution-tips-and-tricks) The most time-consuming part of an upgrade is typically resolving conflicts between changes you’ve made and changes in the updated Pegasus release. Here are some tips and tricks for managing this process. ### Using an AI Agent [Section titled “Using an AI Agent”](#using-an-ai-agent-1) The [pegasus-skills repository](https://github.com/saaspegasus/pegasus-skills) contains a skill with many common conflict resolution patterns encoded in it. This is the easiest way to handle conflict resolutions. To use it, install the skills from the repo and then run the `/resolve-pegasus-conflicts` skill. ### Make sure resolutions are recorded [Section titled “Make sure resolutions are recorded”](#make-sure-resolutions-are-recorded) Git has a feature called [rerere](https://git-scm.com/book/en/v2/Git-Tools-Rerere), which stands for “reuse recorded resolution”. This feature allows you to ask Git to remember how you’ve resolved a conflict so that the next time it sees the same conflict, Git can resolve it for you automatically. Enabling this feature will make future merges much smoother as each conflict will only have to be resolved once. The easiest way to ensure rerere is enabled is to use the Github integration. If that’s not possible, you can enable it locally by running:
```bash
$ git config --global rerere.enabled true
```
### Resolving Database Migrations [Section titled “Resolving Database Migrations”](#resolving-database-migrations) It can be quite common for there to be conflicting database migrations during an upgrade. This can happen when Pegasus modifies data models, you change your project settings in a way that updates models, or you customize Pegasus models in your own code. To resolve any issues with database migrations, follow the following steps: 1. Always commit your migration files to source control. This makes it easier to have a consistent state across environments and builds. 2. Before doing an upgrade, make sure your project migrations are up to date (`manage.py makemigrations` should do nothing). 3. After doing an upgrade, if Pegasus adds or changes any migration files, *discard those changes*. 4. After merging the code and discarding any changes to migrations introduced by Pegasus, re-run `manage.py makemigrations` on the upgraded code, and commit the result to source control. 5. Run `manage.py migrate` to update the database. Basically, **you should always keep your migration history and throw away any changes Pegasus proposes to existing migration files.** Then re-run `makemigrations` on the merged code. ### Resolving Python Package “generated” files (e.g. `uv.lock`) [Section titled “Resolving Python Package “generated” files (e.g. uv.lock)”](#resolving-python-package-generated-files-eg-uvlock) Python packages in Pegasus have lock files that are dynamically generated from other files: * If you’re using uv, your `uv.lock` is generated from `pyproject.toml`. * If you’re using pip-tools, your `requirements.txt` is generated from `requirements.in`. In either case, the easiest way to resolve the conflicts in these files is: 1. *Merge* the source file (`pyproject.toml` or `requirements.in`). 2. Accept all of SaaS Pegasus’s proposed changes to the generated file. While merging your main branch to the pegasus update branch you can run `git checkout --ours uv.lock` to achieve this. 3. Regenerate the generated file, using `uv sync` or `pip-compile`. 4. Manually inspect the new library versions and update them as needed. The end result of this should be that you get all of Pegasus’s pinned versions, and then manually choose which of your own dependencies (if any) to update. ### Resolving bundled static files [Section titled “Resolving bundled static files”](#resolving-bundled-static-files) Conflicts in any built static files should be resolved by: 1. Deleting the files entirely. 2. Re-running `npm run build` / `npm run dev` and committing the result. ## Post-merge steps [Section titled “Post-merge steps”](#post-merge-steps) After upgrading you may also need to reinstall requirements (`pip install -r requirements.txt`), npm packages (`npm install`), etc. depending on what has changed. You will also need to rebuild your front end if you’ve made any changes there (`npm run dev` or `npm run build`) If you are using docker you can use the ‘upgrade’ make target to do this:
```bash
make upgrade
```
This will rebuild the Docker images and create and run any database migrations that are needed. **Note: your web container needs to be running when you run this or it will fail.** ## If you don’t have a “pure” Pegasus branch [Section titled “If you don’t have a “pure” Pegasus branch”](#if-you-dont-have-a-pure-pegasus-branch) In some cases you may not have a “clean” Pegasus branch. This could happen if you did substantial development before your first commit, merged Pegasus into another project, or did several upgrades. In this case you can fake a pure Pegasus branch by taking the following steps. *Note: this process destroys `git blame` for most of your project.* 1. Save your current code (from the “main” branch). 2. Make a new branch (e.g. called “pure-pegasus”) 3. Download your codebase from saaspegasus.com on the *last release your project used/upgraded to*. 4. Put the unmodified download of your *current* Pegasus version onto that branch, without any of your own code. The easiest way to do that is to copy the `.git` folder into your downloaded project and immediately commit the result. This will “brutally” overwrite all your customizations in your git history. *Note this commit id.* 5. Then repeat this process, but instead, do the reverse. Copy the `.git` folder from the Pegasus download back into your (unmodified) copy of the `main` code and again commit the result. This will create a single commit containing all customizations you’ve made to your project. 6. Review this code and merge it back into `main`. If you did everything correctly you should have two huge commits but the pull request will contain no changes. After this process git will believe that the pure pegasus code is fully merged to `main`. You can then use the commit id you noted in step 4 as the commit id on the site, or the starting point for your upgrade ([step 1](#1-create-a-branch-for-the-upgrade)), and jump to [step 2 above](#2-upgrade-the-code-in-the-branch).
# Wagtail CMS
> Add content management system capabilities with Wagtail CMS for blogs, marketing pages, and rich editorial experiences.
[Wagtail](https://wagtail.org/) is a powerful CMS (Content Management System) built on top of Django. You can use it to create rich websites that can be edited directly via an authoring admin interface without writing any code. It’s great for creating marketing sites, blogs, and other mostly-static content. Pegasus optionally ships with a built-in Wagtail instance that can be used as a starting point for adding a content section and blog to any Pegasus app. ## Video Overview [Section titled “Video Overview”](#video-overview) This video provides an overview of the Pegasus/Wagtail functionality: ## Pegasus and Wagtail [Section titled “Pegasus and Wagtail”](#pegasus-and-wagtail) If you want to try Wagtail make sure you enable the “Use Wagtail” option in the Pegasus codebase creator. After you set up your application run:
```bash
./manage.py bootstrap_content
```
to initialize a few pages of content. If you use Docker, the `make init` target will do this automatically for you. Out-of-the-box, Pegasus will create a “content” are of your site (available at the `/content/` URL), a blog index page (available at `/content/blog/`) and a few example blog posts. All your content can be edited via the Wagtail admin UI (available to superusers at `/cms/` by default). The data models for your app’s content are in the `apps/content/` folder, and can be modified or extended in the `models.py` folder there. For more information on Wagtail, check out their [excellent documentation](https://docs.wagtail.org/). ## Adding Blog Posts [Section titled “Adding Blog Posts”](#adding-blog-posts) For blog posts to show up properly, their parent page should be the “Blog” index page, and their type should be “Blog page”. You can add new blog posts by following these steps: 1. Open the Wagtail admin at the `/cms/` url. 2. In the sidebar, click on “Pages” and then the arrow (>) next to “Welcome to your content area!”, then click on “Blog”. 3. On the Blog page, click “add child page” and choose the “Blog page” option. 4. Fill in the details of your blog post 5. On the bottom of the page, click the up arrow (^), and click “Publish”. ## Customizing Wagtail [Section titled “Customizing Wagtail”](#customizing-wagtail) Pegasus’s default wagtail set up is intentionally bare-bones and is meant to provide a starting point for hosting a simple blog attached to your site. Wagtail can be used to build any complicated site and UI you can imagine. One of the most powerful features in Wagtail is the [`StreamField` functionality](https://docs.wagtail.org/en/stable/topics/streamfield.html) which allows you to combine other Wagtail components into a “stream-like” UI. Your blogs and content pages will have a basic implementation using `StreamField` to get your started. ### Wagtail CRX (CodeRed Extensions) [Section titled “Wagtail CRX (CodeRed Extensions)”](#wagtail-crx-codered-extensions) Some Pegasus customers recommend [Wagtail CRX](https://github.com/coderedcorp/coderedcms) as a great way to build more complicated websites with Wagtail. Wagtail CRX ships with a large number of components that can be used in StreamFields to build rich, dynamic content. The previous version of Wagtail CRX was called CodeRed, it only supported Bootstrap version 4. Wagtail CRX now supports Bootstrap 5 (the version used by Pegasus). ### Internationalization [Section titled “Internationalization”](#internationalization) Pegasus ships with Wagtail fully configured to support internationalization using the `wagtail.locales` and `wagtail.contrib.simple_translation` apps bundled with Wagtail. There are [alternative plugins](https://docs.wagtail.org/en/stable/advanced_topics/i18n.html#translation-workflow) available which provide more advanced translation support if necessary. By default, Wagtail is configured to use the same set of languages as Django:
```python
LANGUAGES = WAGTAIL_CONTENT_LANGUAGES = [
('en', 'English'),
('fr', 'French'),
]
```
Full details on Wagtail localization can be found in the Wagtail [documentation](https://docs.wagtail.org/en/stable/advanced_topics/i18n.html). Details on the Pegasus configuration for internationalization can be found on the [internationalization](/internationalization) page. ## Alternatives to Wagtail [Section titled “Alternatives to Wagtail”](#alternatives-to-wagtail) Some companies prefer to manage their marketing sites completely separate from their application. In this scenario it’s recommended to create a separate marketing site using something like Wordpress, Webflow, Wix, Squarespace, or any number of other options. You can host this site at `yourdomain.com` and then host your Pegasus app separately at `app.yourdomain.com` (or similar). If you choose to set up your content this way, you should build Pegasus without wagtail.
# AI Integrations for Development
> Set up AI-powered coding assistants with Cursor, Claude Code, and Junie, including rules files and MCP tools for enhanced Pegasus development workflow.
As of version 2025.4, Pegasus includes tooling for integrating with AI-powered coding assistants and tools. These will be constantly edited, expanded on, and improved as the community is able to provide more feedback on them. ## Video overview [Section titled “Video overview”](#video-overview) See below for a demo of how you can use these tools to help you with development. ## LLM-Friendly Documentation [Section titled “LLM-Friendly Documentation”](#llm-friendly-documentation) This documentation has llm-friendly markdown files that can be copy/pasted or linked to in any LLM or AI-coding assistant. There is an [llms.txt](/llms.txt) index file with further links to other files you can use, including the [llms-small.txt file](https://docs.saaspegasus.com/llms-small.txt) (a compact version of the documentation with the essentials), and the [llms-full.txt file](https://docs.saaspegasus.com/llms-full.txt), with the complete documentation. All llm files are formatted in markdown. ## Rules Files [Section titled “Rules Files”](#rules-files) Pegasus ships with a set of rules files that are designed to be used with coding assistants. These rules are broken out into various sections---e.g. architecture, general guidelines, and guidelines for specific programming languages and frameworks. The rules have been custom developed for Pegasus applications and contain best-practices and information for building on Pegasus. When the tool supports it (e.g. in Cursor), these rules files will be organized and labeled in ways that allow for them to be automatically included in the appropriate contexts. You can edit your rules freely after downloading your project. They are provided as a quick way to get started. ## MCP [Section titled “MCP”](#mcp) Pegasus also includes a default MCP setup containing two tools, a database inspector (Postgres builds only), and a web browser. You can use these tools to give your AI assistants access to your database + schema and let them work directly with your application in a browser. See the demo video above for more detail. ## Working with Cursor [Section titled “Working with Cursor”](#working-with-cursor) [Cursor](https://www.cursor.com/) is an AI code editor (IDE). If you enable the Cursor integration, your rules files will be saved to the `/.cursor/rules/` directory and will be labeled to be automatically included based on the context. For example, the Python coding guidelines will be included anytime you’re editing a `.py` file. The MCP setup for Cursor will be saved to `.cursor/mcp.json`, and should be discoverable by Cursor there. You can modify the rules files and MCP set up in your Cursor settings or by editing the files by hand. ## Working with Claude Code [Section titled “Working with Claude Code”](#working-with-claude-code) [Claude Code](https://docs.anthropic.com/en/docs/agents-and-tools/claude-code/overview) is a command-line coding assistant built by Anthropic. If you enable the Claude code integration, your rules will be collapsed into a single, organized `CLAUDE.md` file for Claude to use. Additionally, the MCP setup will be saved to `.mcp.json` and should be automatically discovered by claude. ### The Github Workflow file [Section titled “The Github Workflow file”](#the-github-workflow-file) You can also optionally enable a Github workflow file for Claude. When this is enabled, you will be able to mention @claude on any Github issue or pull request to trigger a claude code update. In order for this to work, you will have to add an `ANTHROPIC_API_KEY` to your repository secrets. You can learn more in the [Claude code docs](https://docs.anthropic.com/en/docs/claude-code/github-actions) and [Github secrets docs](https://docs.github.com/en/actions/security-for-github-actions/security-guides/using-secrets-in-github-actions). ## Working with Junie [Section titled “Working with Junie”](#working-with-junie) [Junie](https://www.jetbrains.com/junie/) is the coding assistant built by JetBrains (makers of PyCharm). If you enable the Junie integration, your rules will be collapsed into a single, organized `.junie/guidelines.md` file for Junie to use. ## Pegasus Skills for Claude Code [Section titled “Pegasus Skills for Claude Code”](#pegasus-skills-for-claude-code) [Pegasus Skills](https://github.com/saaspegasus/pegasus-skills) are specialized skill files that enhance Claude Code’s capabilities when working with Pegasus projects. Currently there is one skill available: * **resolve-pegasus-conflicts**: Helps resolve merge conflicts that arise during Pegasus upgrades. This skill can be invoked with `/resolve-pegasus-conflicts` or by asking Claude Code for help with merge conflicts during an upgrade. See the [Pegasus Skills repository](https://github.com/saaspegasus/pegasus-skills) for installation instructions and more details. ## Git Worktrees for Parallel Development (Experimental) [Section titled “Git Worktrees for Parallel Development (Experimental)”](#git-worktrees-for-parallel-development-experimental) Pegasus includes experimental support for git worktrees, allowing you to work on multiple branches simultaneously with isolated services. See the [Git Worktrees documentation](/experimental/git-worktrees/) for details. ## Other tools [Section titled “Other tools”](#other-tools) If you’re using a different tool, it is recommended to choose one of the two above when building your project and then copy the files to wherever your tool expects them. Choose “Cursor” if you want the rules files split up, and “Claude” if you prefer a single rules file. Also, if you’d like support or help configuring a different tool, email and let me know!
# Image Models
> Generate images with AI models including DALL-E-2, DALL-E-3, and Stability AI using OpenAI and Stability AI API keys in your Django application.
Pegasus includes an optional example app for generating images with multiple different models, including [Gemini/Nano Banana Pro](https://gemini.google/overview/image-generation/), [Dall-E-2](https://openai.com/index/dall-e-2) and [Dall-E-3](https://openai.com/index/dall-e-3) and [Stability AI](https://stability.ai/) (Stable Diffusion 3). ## Configuration [Section titled “Configuration”](#configuration) You will need to set the following environment/.env variables to use the different models: | Model | Environment Variable | | -------------------- | -------------------------------- | | Gemini / Nano Banana | `AI_IMAGES_GEMINI_API_KEY` | | Dall-E | `AI_IMAGES_OPENAI_API_KEY` | | Stability AI | `AI_IMAGES_STABILITY_AI_API_KEY` | You can choose which model you want to use from the dropdown on the image generation page.
# LLMs and Chat
> Integrate LLM chat interfaces with OpenAI, Claude, or local models using Pydantic AI. Supports streaming, async APIs, and agents.
Pegasus comes with an optional Chat UI for interacting with LLMs. This section covers how it works and the various supported options. ## Configuring your AI model [Section titled “Configuring your AI model”](#configuring-your-ai-model) All AI calls in Pegasus go through [Pydantic AI](https://ai.pydantic.dev/). You can configure the model used by setting the `DEFAULT_AI_MODEL` value in your `settings.py` or environment variables / `.env` file. The format is `provider:model_name`, for example:
```bash
DEFAULT_AI_MODEL="openai:gpt-5-mini"
```
The chat UI and all agents will use whatever is set in `DEFAULT_AI_MODEL`. You will also need to set the appropriate API key environment variables for your chosen provider, e.g. `OPENAI_API_KEY`, `ANTHROPIC_API_KEY`, etc. See the [Pydantic AI model docs](https://ai.pydantic.dev/models/overview/) for available models and configuration. ## Running open source LLMs [Section titled “Running open source LLMs”](#running-open-source-llms) To run models like Mixtral or Llama3, you will need to run an [Ollama](https://ollama.com/) server in a separate process. 1. [Download](https://ollama.com/download) and run Ollama or use the Docker [image](https://hub.docker.com/r/ollama/ollama) 2. Download the model you want to run:
```bash
ollama pull llama3
# or with docker
docker exec -it ollama ollama pull llama3
```
See the [Pydantic AI Ollama docs](https://ai.pydantic.dev/models/openai/#ollama) for more details. 3. Set `DEFAULT_AI_MODEL` in your `.env` file to point to the Ollama model. For example:
```bash
DEFAULT_AI_MODEL="ollama:llama3"
```
4. Restart your Django server. ## The Chat UI [Section titled “The Chat UI”](#the-chat-ui) The Chat UI has multiple different implementations, and the one that is used for your project will be determined by your build configuration. If you build with asynchronous functionality enabled *and* htmx then it will use a websocket-based Chat UI. This Chat UI supports streaming responses, and is the recommended option. It is also currently the only option that supports the chat widget that can be embedded on any page. If you build without asynchronous functionality enabled, the chat UI will instead use Celery and polling. The React version of the chat UI also uses Celery and polling. This means that [Celery must be running](/celery) to get responses from the LLM. ### The Chat widget [Section titled “The Chat widget”](#the-chat-widget) *The chat widget is currently only available if your project is using HTMX and Async.* The chat widget is a small component that can be embedded on any page of your app. By default, it is included in your `chat_home.html` file, so you can view a demo of the widget from the “AI Chat” tab in your app. #### Adding the chat widget to a page [Section titled “Adding the chat widget to a page”](#adding-the-chat-widget-to-a-page) To add the chat widget to a page, you can follow the example in `chat_home.html`. There are two steps: First, include the component at the end of the `` tag. If you’re extending the `app_base.html` file this will be at the end of the `{% block app %}` block.
```jinja
{% block app %}
...
{% include "chat/components/chat_overlay.html" %}
{% endblock app %}
```
Then, add the `ws_initialize.js` in your page JavaScript:
```jinja
{% block page_js %}
{% vite_asset 'assets/javascript/chat/ws_initialize.ts' %}
{% endblock page_js %}
```
If you want to add the chat widget to all pages in your app, you can add it to the `base.html` file. For all logged-in pages, you can add it to the `app_base.html` file. ## Using Agents [Section titled “Using Agents”](#using-agents) As of version 2025.9.1, Pegasus includes a set of example agents that you can use as a foundation for building your own agents. These agents are built with [Pydantic AI](https://docs.pydantic-ai.com/), and include: * A weather and location lookup agent, with tools to do geo-lookups and access current weather information. * A chatbot to interact with employee application data models, with tools to work with employee data. * A chatbot to interact with system database, with MCP tool to access postgres data. * A tool to send emails. For more information on these agents and how they work, you can watch this video: ### Setting the agent model and keys [Section titled “Setting the agent model and keys”](#setting-the-agent-model-and-keys) Agents use the same `DEFAULT_AI_MODEL` setting as the chat UI. See [Configuring your AI model](#configuring-your-ai-model) above for details. ## Previous versions (OpenAI / LiteLLM) [Section titled “Previous versions (OpenAI / LiteLLM)”](#previous-versions-openai--litellm) Previous versions of Pegasus used LiteLLM and had different configuration options including `LLM_MODELS`, `DEFAULT_LLM_MODEL`, and `DEFAULT_AGENT_MODEL`. As of version 2026.2.1, all AI calls go through Pydantic AI exclusively. For documentation on the previous setup, see [the older version of this page](https://github.com/saaspegasus/pegasus-docs/blob/f70c522ec14f469bfce42554f862d82824834289/src/content/docs/ai/llms.mdx).
# Bootstrap (Deprecated)
> Customize Bootstrap 5 themes in Pegasus with Sass variables, JavaScript integration, and Material Kit alternatives for responsive web design.
Deprecated Bootstrap is deprecated in Pegasus. It will continue to work for existing projects, but will not receive new features and support will be removed in a future release. New projects should use [TailwindCSS](/css/tailwind/). There are two Bootstrap themes, both of which use Bootstrap version 5. ## Choosing your theme [Section titled “Choosing your theme”](#choosing-your-theme) The default Bootstrap theme is based off the default settings that ship with Bootstrap. It provides a simple, practical starting point that is easy to customize and extend. This theme is recommended for all new projects using Bootstrap. There is also a deprecated theme is based on Creative Tim’s [Material Kit](https://www.creative-tim.com/product/material-kit) and [Material Dashboard](https://www.creative-tim.com/product/material-dashboard) products. White this theme is flashier than the default theme, it has been retired due to developer experience issues. It is not recommended except for legacy projects, as support will be dropped in the future. ## Customizing the theme [Section titled “Customizing the theme”](#customizing-the-theme) Pegasus’s file structure is based on [the Bootstrap documentation](https://getbootstrap.com/docs/5.0/customize/sass/#importing). Any of the variables used in Bootstrap can be changed by modifying the `assets/styles/site-bootstrap.scss` file. A complete list of available variables can be found in `./node_modules/bootstrap/scss/variables`. Try adding the following lines to your file (after importing `functions`) to see how it changes things:
```scss
// Configuration
@import "~bootstrap/scss/functions";
$primary: #2e7636; // change primary color to green
$body-color: #00008B; // change main text to blue
// rest of file here...
```
**You’ll have to run `npm run dev` to see the changes take.** For more details on building the CSS files, see the [front end documentation](/front-end/overview). The [Bootstrap documentation](https://getbootstrap.com/docs/5.0/customize/sass/) has much more detail on customizing your theme! ## Working with JavaScript in Django templates [Section titled “Working with JavaScript in Django templates”](#working-with-javascript-in-django-templates) If you want to call bootstrap JavaScript from a Django template file, you can make the bootstrap library (or subsets of it) available on the browser window. To make all of bootstrap available, you can modify `site-bootstrap.js`b to just be these lines:
```javascript
require('./styles/site-bootstrap.scss');
window.bootstrap = require('bootstrap');
```
After [rebuilding the front end](/front-end/overview) you can then call bootstrap in a Django template like this:
```jinja
{% block page_js %}
{% endblock %}
```
This example will open the modal with ID `onLoadModal` on page load. Alternatively, you can add individual bootstrap javascript modules via `site-bootstrap.js` like this:
```javascript
require('./styles/site-bootstrap.scss');
//
window.Modal = require('bootstrap/js/dist/modal'); // modals (used by teams)
```
And then call it in a Django template like this (with no `bootrap.` prefix):
```javascript
const onLoadModal = new Modal(document.getElementById('landing-page-modal'));
```
# Bulma (Deprecated)
> Customize Bulma CSS framework using Sass variables for colors, typography, and styling in your Pegasus application.
Deprecated Bulma is deprecated in Pegasus. It will continue to work for existing projects, but will not receive new features and support will be removed in a future release. New projects should use [TailwindCSS](/css/tailwind/). Bulma is readily customizable via [Sass variables](https://bulma.io/documentation/customize/variables/). Any of the variables used by Bulma can be changed by modifying the `assets/styles/site-bulma.scss` file. Try adding the following lines to the top of your file to see how it changes things:
```scss
$primary: #2e7636; // change primary color to green
$body-color: #00008B; // change main text to blue
```
**You’ll have to run `npm run dev` to see the changes take.** For more details on building the CSS files, see the [front end documentation](/front-end/overview).
# CSS File Structure
> Understand Pegasus CSS file organization with framework-independent styles and framework-specific overrides compiled from assets to static directories.
CSS source files live in the `assets/styles` folder, and are compiled into the `static/css` folder. Some Pegasus styles are written using [Sass](https://sass-lang.com/), which provides many benefits and features on top of traditional CSS. **Modifying CSS requires having a functional [front-end build setup](/front-end/overview).** All versions of Pegasus contain two main sets of styles: * Styles that are *framework-independent* are contained and imported in `assets/styles/app/base.sass` and compiled into `static/css/site-base.css`. * Styles that *extend or override the CSS framework* are contained in `assets/styles/app//` and compiled into `static/css/site-.css`. This split is not required, and you can optionally combine everything into a single file by importing the styles from `base.sass` into your framework file and deleting `site-base.css`.
# The Material Theme (deprecated)
> Legacy Material Design theme based on Creative Tim's Material Kit and Dashboard, now deprecated with maintenance-only support until 2025.
This feature is deprecated This theme was removed in version 2025.10. It is recommended to switch to [Tailwind CSS](/css/tailwind). This means that the theme is in maintenance-only mode, and support will be dropped by the end of 2025. Existing projects can continue using the theme, but new projects should not, and new Pegasus features will eventually not be developed and tested on the theme. The reason for this is that several Pegasus customers have complained about the lack of documentation and support for this theme from its maintainer, Creative Tim. Additionally, their process around updating the theme has entailed releasing large, poorly-documented updates which have been difficult for me to incorporate back into Pegasus. The following documentation is for people already using the material theme. ## Customizing the Material theme [Section titled “Customizing the Material theme”](#customizing-the-material-theme) The customization process outlined above largely works for the Material theme as well. For example, you can change the primary color from the default magenta to a dark green by adding the following lines towards the top of `assets/styles/site-bootstrap.scss`:
```scss
// Configuration
@import "~bootstrap/scss/functions";
// add these lines
$primary: #2e7636; // change primary color + gradients to green
$primary-gradient: #2e7676;
$primary-gradient-state: #2e7676;
```
You will also have to [build your front end](/front-end/overview) to see the changes. Material has more customization options than the default theme, which can be found in the [Material Dashboard documentation](https://www.creative-tim.com/learning-lab/bootstrap/overview/material-dashboard). The theme files live in the `assets/material-dashboard` folder. You can see the modifications that have been made for Pegasus support [on Github here](https://github.com/creativetimofficial/material-dashboard/compare/master...czue:pegasus-tweaks). In particular, a few bugs have been fixed, and the unused pro files have been removed. Creative Tim offers pro versions of [Material Dashboard](https://www.creative-tim.com/product/material-dashboard-pro) and [Material Kit](https://www.creative-tim.com/product/material-kit-pro) which are helpful if you want to have access to more pages / components. These should integrate seamlessly with the Pegasus theme. ### Enabling Material’s JavaScript [Section titled “Enabling Material’s JavaScript”](#enabling-materials-javascript) Pegasus doesn’t ship with the Material theme JavaScript built in. If you would like to use their JavaScript functionality (required for many of their components) you can take the following steps: 1. Download [the `material-kit.min.js` file from Creative Tim’s Github repository](https://github.com/creativetimofficial/material-kit/blob/master/assets/js/material-kit.min.js). 2. Copy it into your Django static directory. For example, to `/static/js` 3. Add it to the `` section of your `base.html` template (or wherever you want to use it):
```jinja
```
After completing these steps, the Material Kit JavaScript functionality should work.
# Choosing a CSS Theme
> Compare TailwindCSS, Bootstrap, Bulma, and Material Design themes with screenshots, features, and recommendations for Django projects.
### Tailwind CSS [Section titled “Tailwind CSS”](#tailwind-css) **Tailwind CSS is the recommended and actively supported CSS framework.** It is the most popular choice, easiest to customize, and supports themes and dark mode out-of-the-box. Here’s what it looks like. **Light mode**:  **Dark mode**:  ### Deprecated Themes [Section titled “Deprecated Themes”](#deprecated-themes) Previous versions of Pegasus included support for a [Bootstrap 5](https://getbootstrap.com/) theme, a [Bulma](https://bulma.io/) theme, and a theme based on Creative Tim’s [Material Kit](https://www.creative-tim.com/product/material-kit) and [Material Dashboard](https://www.creative-tim.com/product/material-dashboard) products. ***These alternate themes have all been deprecated and are no longer recommended for new projects.*** **Bootstrap Default Theme (Deprecated):**  **Bulma (Deprecated):**  **Bootstrap Material Theme (Deprecated):** 
# Pegasus CSS
> Cross-framework CSS classes with pg- prefixes for consistent styling across Bootstrap, TailwindCSS, and Bulma using Sass @extend and @apply.
Pegasus historically shipped a set of CSS classes prefixed with `pg-` to provide compatibility across its supported CSS frameworks (Tailwind, Bootstrap, and Bulma). These classes are proxies for similar classes provided by the underlying frameworks themselves, and are created using the Sass [`@extend` helper](https://sass-lang.com/documentation/at-rules/extend) or Tailwind’s [`@apply` helper](https://tailwindcss.com/docs/reusing-styles#extracting-classes-with-apply). ## Default Behavior (Tailwind) [Section titled “Default Behavior (Tailwind)”](#default-behavior-tailwind) **By default, Pegasus outputs native Tailwind and DaisyUI classes in your templates.** For example, a button that previously used `pg-button-primary` will output `btn btn-primary` (a DaisyUI class). A title that used `pg-title` will output `text-3xl font-bold mb-2` (Tailwind utilities). This means you can style your app using the standard Tailwind and DaisyUI documentation directly, without needing to learn or look up the Pegasus-specific class names. ## Legacy pg- Classes [Section titled “Legacy pg- Classes”](#legacy-pg--classes) If you prefer to continue using `pg-` prefixed classes, you can enable them by checking the “Use Pegasus CSS classes” checkbox in your project configuration. This will output the `pg-` classes in your templates instead of native Tailwind classes. This is useful if: * You are using **Bootstrap** or **Bulma** (where `pg-` classes are always used). * You have an existing project with significant custom code using `pg-` classes and aren’t ready to migrate yet. ### Migrating from pg- Classes to Native Classes [Section titled “Migrating from pg- Classes to Native Classes”](#migrating-from-pg--classes-to-native-classes) If you have an existing project using `pg-` classes and want to switch to native Tailwind/DaisyUI classes, there are two steps: **1. Uncheck “Use Pegasus CSS classes” in your project settings.** In your project on [saaspegasus.com](https://www.saaspegasus.com/), go to **Developer Setup → Advanced** and uncheck **Use Pegasus CSS classes**. This tells Pegasus to output native Tailwind/DaisyUI classes in future builds. **2. Migrate your existing templates and JavaScript files.** Use the `pegasus migrate-css` command from the [Pegasus CLI](https://github.com/saaspegasus/pegasus-cli) to convert any `pg-` classes in your own code to their native equivalents:
```bash
# Preview what would change
pegasus migrate-css --dry-run
# Do the migration
pegasus migrate-css
```
The command reads class mappings from your project’s `assets/styles/pegasus/tailwind.css`, so it should match the version of Pegasus your project was built with. See the [pegasus-cli README](https://github.com/saaspegasus/pegasus-cli#migrating-pg--css-classes) for additional options (e.g. `--css-file` and `--search-dir` for non-standard project layouts). ## Class Reference [Section titled “Class Reference”](#class-reference) Pegasus CSS classes are defined in `assets/styles/pegasus/.sass/css`. The following table shows the most common classes and their values across frameworks. | Pegasus Class | Description | Value in Bootstrap | Value in Tailwind | Value in Bulma | | --------------- | ------------------- | ------------------ | --------------------------------------------------------------- | --------------- | | `pg-columns` | Wrapper for columns | `row gy-4` | `flex flex-col space-y-4 lg:flex-row lg:space-x-4 lg:space-y-0` | `columns` | | `pg-column` | Individual column | `col-md` | `flex-1` | `column` | | `pg-title` | A title | `h3` (element) | `text-3xl font-bold mb-2` | `title` | | `pg-subtitle` | A subtitle | `lead` | `text-xl mb-1` | `subtitle` | | `pg-button-***` | A styled button | `btn btn-***` | `btn btn-***` (from daisyUI) | `button is-***` | | `pg-text-***` | Colored text | `text-***` | `text-***` (from daisyUI) | `has-text-***` | For a full list of classes and their mappings, see `assets/styles/pegasus/` in your project. ## Classes Not Covered by Migration [Section titled “Classes Not Covered by Migration”](#classes-not-covered-by-migration) Some `pg-` classes use more complex CSS (not simple utility mappings) and are not affected by the migration. These classes remain as `pg-` in all builds and should continue to be referenced by their `pg-` names: * **`pg-breadcrumbs`**, **`pg-breadcrumb-active`** — breadcrumb navigation with nested styling rules. * **`pg-select`** — wraps a nested `