plain.preflight

System checks that validate your settings and environment before running your application.

Overview

Preflight checks help you catch configuration problems early. You can run checks to verify that settings are valid, directories exist, URL patterns are correct, and security requirements are met before your application starts.

from plain.preflight import PreflightCheck, PreflightResult, register_check


@register_check("custom.database_connection")
class CheckDatabaseConnection(PreflightCheck):
    """Verify database is reachable."""

    def run(self) -> list[PreflightResult]:
        from plain.models import connection

        try:
            connection.ensure_connection()
        except Exception as e:
            return [
                PreflightResult(
                    fix=f"Database connection failed: {e}. Check your DATABASE_URL.",
                    id="custom.database_unreachable",
                )
            ]
        return []

When you run plain preflight, your check runs alongside the built-in checks:

$ plain preflight
Running preflight checks...
Check: custom.database_connection ✔
Check: files.upload_temp_dir ✔
Check: settings.unused_env_vars ✔
Check: urls.config 4 passed

Running preflight checks

Development

Run preflight checks at any time:

plain preflight

If you use plain.dev for local development, preflight checks run automatically when you start plain dev.

Deployment

Add --deploy to include deployment-specific checks like SECRET_KEY strength, DEBUG mode, and ALLOWED_HOSTS:

plain preflight --deploy

This should be part of your deployment process. If any check fails (returns errors, not warnings), the command exits with code 1.

JSON output

For CI/CD pipelines or programmatic access, use JSON output:

plain preflight --format json
{
  "passed": true,
  "checks": [
    {
      "name": "files.upload_temp_dir",
      "passed": true,
      "issues": []
    }
  ]
}

Use --quiet to suppress progress output and only show errors.

Built-in checks

Plain includes these checks out of the box:

Check Description Deploy only
files.upload_temp_dir Validates FILE_UPLOAD_TEMP_DIR exists No
settings.unused_env_vars Detects env vars that look like settings but aren't used No
urls.config Validates URL patterns for common issues No
security.secret_key Validates SECRET_KEY strength Yes
security.secret_key_fallbacks Validates SECRET_KEY_FALLBACKS strength Yes
security.debug Ensures DEBUG is False Yes
security.allowed_hosts Ensures ALLOWED_HOSTS is not empty Yes

Custom preflight checks

Basic checks

Create a check by subclassing PreflightCheck and using the @register_check decorator:

from plain.preflight import PreflightCheck, PreflightResult, register_check


@register_check("custom.redis_connection")
class CheckRedisConnection(PreflightCheck):
    """Verify Redis cache is reachable."""

    def run(self) -> list[PreflightResult]:
        from plain.cache import cache

        try:
            cache.set("preflight_test", "ok", timeout=1)
        except Exception as e:
            return [
                PreflightResult(
                    fix=f"Redis connection failed: {e}",
                    id="custom.redis_unreachable",
                )
            ]
        return []

Place this in a preflight.py file in your app directory. Plain autodiscovers preflight.py modules when running checks.

Deployment-only checks

For checks that only matter in production, add deploy=True:

@register_check("custom.ssl_certificate", deploy=True)
class CheckSSLCertificate(PreflightCheck):
    """Verify SSL certificate is valid and not expiring soon."""

    def run(self) -> list[PreflightResult]:
        # Check certificate expiration...
        if days_until_expiry < 30:
            return [
                PreflightResult(
                    fix=f"SSL certificate expires in {days_until_expiry} days.",
                    id="custom.ssl_expiring_soon",
                )
            ]
        return []

Warnings vs errors

By default, PreflightResult represents an error that fails the preflight. For non-critical issues, use warning=True:

PreflightResult(
    fix="Consider enabling gzip compression for better performance.",
    id="custom.gzip_disabled",
    warning=True,  # Won't cause preflight to fail
)

Warnings display with a yellow indicator but don't cause the command to exit with an error code.

Silencing checks

Silencing entire checks

To skip a check entirely, add its name to PREFLIGHT_SILENCED_CHECKS:

# app/settings.py
PREFLIGHT_SILENCED_CHECKS = [
    "security.debug",  # We intentionally run with DEBUG=True in staging
]

Silencing specific results

To silence individual result IDs (not the whole check), use PREFLIGHT_SILENCED_RESULTS:

# app/settings.py
PREFLIGHT_SILENCED_RESULTS = [
    "security.secret_key_weak",  # Using a known weak key in testing
]

FAQs

What's the difference between a check name and a result ID?

The check name (like security.secret_key) identifies the check class. The result ID (like security.secret_key_weak) identifies a specific issue that check can report. A single check can return multiple different result IDs.

Where should I put custom preflight checks?

Create a preflight.py file in your app directory. Plain autodiscovers these modules when running plain preflight.

How do I run checks programmatically?

Use the run_checks function:

from plain.preflight import run_checks

for check_class, name, results in run_checks(include_deploy_checks=True):
    for result in results:
        print(f"{name}: {result.fix}")

Can I attach additional context to a result?

Use the obj parameter to attach a related object:

PreflightResult(
    fix="Invalid URL pattern",
    id="urls.invalid_pattern",
    obj=some_url_pattern,  # Will be included in output
)

Installation

plain.preflight is included with the plain package. No additional installation is required.