Plain is headed towards 1.0! Subscribe for development updates →

Runtime

Leverage user-settings at runtime.

Settings

Single-file

All of your settings go in app/settings.py. That's how you do it!

The file itself is not much different than how Django does it, but the location, and a strong recommendation to only use the one file makes a big difference.

Environment variables

It seems pretty well-accepted these days that storing settings in env vars is a good idea (12factor.net).

Your settings file should be looking at the environment for secrets or other values that might change between environments. For example:

# app/settings.py
STRIPE_SECRET_KEY = environ["STRIPE_SECRET_KEY"]

Local development

In local development, you should use .env files to set these values. The .env should then be in your .gitignore!

It would seem like .env.dev would be a good idea, but there's a chicken-and-egg problem with that. You would then have to prefix most (or all) of your local commands with PLAIN_ENV=dev or otherwise configure your environment to do that for you. Generally speaking, a production .env shouldn't be committed in your repo anyway, so using .env for local development is ok. The downside to this is that it's harder to share your local settings with others, but these also often contain real secrets which shouldn't be committed to your repo either! More advanced .env sharing patterns are currently beyond the scope of Plain...

Production

TODO

Minimum required settings

# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = environ["SECRET_KEY"]

# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = environ.get("DEBUG", "false").lower() in ("true", "1", "yes")

MIDDLEWARE = [
    "plain.middleware.security.SecurityMiddleware",
    "plain.sessions.middleware.SessionMiddleware",
    "plain.middleware.common.CommonMiddleware",
    "plain.csrf.middleware.CsrfViewMiddleware",
    "plain.auth.middleware.AuthenticationMiddleware",
]

if DEBUG:
    INSTALLED_PACKAGES += [
        "plain.dev",
    ]
    MIDDLEWARE += [
        "plain.dev.RequestsMiddleware",
    ]

TIME_ZONE = "America/Chicago"
  1"""
  2Default Plain settings. Override these with settings in the module pointed to
  3by the PLAIN_SETTINGS_MODULE environment variable.
  4"""
  5from pathlib import Path
  6
  7from plain.runtime import APP_PATH as default_app_path
  8
  9####################
 10# CORE             #
 11####################
 12
 13DEBUG: bool = False
 14
 15PLAIN_TEMP_PATH: Path = default_app_path.parent / ".plain"
 16
 17# Hosts/domain names that are valid for this site.
 18# "*" matches anything, ".example.com" matches example.com and all subdomains
 19ALLOWED_HOSTS: list[str] = []
 20
 21# Local time zone for this installation. All choices can be found here:
 22# https://en.wikipedia.org/wiki/List_of_tz_zones_by_name (although not all
 23# systems may support all possibilities). This is interpreted as the default
 24# user time zone.
 25TIME_ZONE: str = "UTC"
 26
 27# Default charset to use for all Response objects, if a MIME type isn't
 28# manually specified. It's used to construct the Content-Type header.
 29DEFAULT_CHARSET = "utf-8"
 30
 31# List of strings representing installed packages.
 32INSTALLED_PACKAGES: list = []
 33
 34# Whether to append trailing slashes to URLs.
 35APPEND_SLASH = True
 36
 37# A secret key for this particular Plain installation. Used in secret-key
 38# hashing algorithms. Set this in your settings, or Plain will complain
 39# loudly.
 40SECRET_KEY: str
 41
 42# List of secret keys used to verify the validity of signatures. This allows
 43# secret key rotation.
 44SECRET_KEY_FALLBACKS: list[str] = []
 45
 46ROOT_URLCONF = "urls"
 47
 48# List of upload handler classes to be applied in order.
 49FILE_UPLOAD_HANDLERS = [
 50    "plain.internal.files.uploadhandler.MemoryFileUploadHandler",
 51    "plain.internal.files.uploadhandler.TemporaryFileUploadHandler",
 52]
 53
 54# Maximum size, in bytes, of a request before it will be streamed to the
 55# file system instead of into memory.
 56FILE_UPLOAD_MAX_MEMORY_SIZE = 2621440  # i.e. 2.5 MB
 57
 58# Maximum size in bytes of request data (excluding file uploads) that will be
 59# read before a SuspiciousOperation (RequestDataTooBig) is raised.
 60DATA_UPLOAD_MAX_MEMORY_SIZE = 2621440  # i.e. 2.5 MB
 61
 62# Maximum number of GET/POST parameters that will be read before a
 63# SuspiciousOperation (TooManyFieldsSent) is raised.
 64DATA_UPLOAD_MAX_NUMBER_FIELDS = 1000
 65
 66# Maximum number of files encoded in a multipart upload that will be read
 67# before a SuspiciousOperation (TooManyFilesSent) is raised.
 68DATA_UPLOAD_MAX_NUMBER_FILES = 100
 69
 70# Directory in which upload streamed files will be temporarily saved. A value of
 71# `None` will make Plain use the operating system's default temporary directory
 72# (i.e. "/tmp" on *nix systems).
 73FILE_UPLOAD_TEMP_DIR = None
 74
 75USE_X_FORWARDED_HOST = False
 76USE_X_FORWARDED_PORT = False
 77
 78# User-defined overrides for error views by status code
 79HTTP_ERROR_VIEWS: dict[int] = {}
 80
 81# If your Plain app is behind a proxy that sets a header to specify secure
 82# connections, AND that proxy ensures that user-submitted headers with the
 83# same name are ignored (so that people can't spoof it), set this value to
 84# a tuple of (header_name, header_value). For any requests that come in with
 85# that header/value, request.is_secure() will return True.
 86# WARNING! Only set this if you fully understand what you're doing. Otherwise,
 87# you may be opening yourself up to a security risk.
 88SECURE_PROXY_SSL_HEADER = None
 89
 90##############
 91# MIDDLEWARE #
 92##############
 93
 94# List of middleware to use. Order is important; in the request phase, these
 95# middleware will be applied in the order given, and in the response
 96# phase the middleware will be applied in reverse order.
 97MIDDLEWARE = [
 98    "plain.middleware.security.SecurityMiddleware",
 99    "plain.middleware.common.CommonMiddleware",
100    "plain.csrf.middleware.CsrfViewMiddleware",
101]
102
103###########
104# SIGNING #
105###########
106
107COOKIE_SIGNING_BACKEND = "plain.signing.TimestampSigner"
108
109########
110# CSRF #
111########
112
113# Settings for CSRF cookie.
114CSRF_COOKIE_NAME = "csrftoken"
115CSRF_COOKIE_AGE = 60 * 60 * 24 * 7 * 52
116CSRF_COOKIE_DOMAIN = None
117CSRF_COOKIE_PATH = "/"
118CSRF_COOKIE_SECURE = True
119CSRF_COOKIE_HTTPONLY = False
120CSRF_COOKIE_SAMESITE = "Lax"
121CSRF_HEADER_NAME = "HTTP_X_CSRFTOKEN"
122CSRF_TRUSTED_ORIGINS: list[str] = []
123CSRF_USE_SESSIONS = False
124
125###########
126# LOGGING #
127###########
128
129# Custom logging configuration.
130LOGGING = {}
131
132###############
133# ASSETS #
134###############
135
136# Whether to redirect the original asset path to the fingerprinted path.
137ASSETS_REDIRECT_ORIGINAL = True
138
139# If assets are served by a CDN, use this URL to prefix asset paths.
140# Ex. "https://cdn.example.com/assets/"
141ASSETS_BASE_URL: str = ""
142
143####################
144# PREFLIGHT CHECKS #
145####################
146
147# List of all issues generated by system checks that should be silenced. Light
148# issues like warnings, infos or debugs will not generate a message. Silencing
149# serious issues like errors and criticals does not result in hiding the
150# message, but Plain will not stop you from e.g. running server.
151SILENCED_PREFLIGHT_CHECKS = []
152
153#######################
154# SECURITY MIDDLEWARE #
155#######################
156SECURE_REDIRECT_EXEMPT = []
157SECURE_SSL_HOST = None
158SECURE_SSL_REDIRECT = True
159
160SECURE_DEFAULT_HEADERS = {
161    # "Content-Security-Policy": "default-src 'self'",
162    # https://hstspreload.org/
163    # "Strict-Transport-Security": "max-age=31536000; includeSubDomains; preload",
164    "Cross-Origin-Opener-Policy": "same-origin",
165    "Referrer-Policy": "same-origin",
166    "X-Content-Type-Options": "nosniff",
167    "X-Frame-Options": "DENY",
168}
169
170#############
171# Templates #
172#############
173
174JINJA_LOADER = "jinja2.loaders.FileSystemLoader"
175JINJA_ENVIRONMENT = "plain.templates.jinja.defaults.create_default_environment"