plain.flags

Local feature flags via database models.

Overview

You write custom flags as subclasses of Flag. Each flag defines a "key" (to identify who/what the flag applies to) and an initial value. The results are stored in the database, allowing you to override them later via the admin.

# app/flags.py
from plain.flags import Flag


class FooEnabled(Flag):
    def __init__(self, user):
        self.user = user

    def get_key(self):
        return self.user

    def get_value(self):
        # Initially all users will have this feature disabled
        # and we'll enable them manually in the admin
        return False

To check a flag, import it and access the .value property:

from app import flags

if flags.FooEnabled(user).value:
    # Feature is enabled for this user
    ...

You can also use flags directly in boolean expressions since they implement __bool__:

if flags.FooEnabled(user):
    # Feature is enabled
    ...

Usage in templates

You can use flags directly in HTML templates:

{% if flags.FooEnabled(get_current_user()) %}
    <p>Foo is enabled for you!</p>
{% else %}
    <p>Foo is disabled for you.</p>
{% endif %}

Usage in Python

from app import flags


# Check as a boolean
if flags.FooEnabled(user):
    print("Foo is enabled!")

# Get the actual value
print(flags.FooEnabled(user).value)

Advanced usage

You can do whatever you want inside of get_key and get_value. For example, you might want to check URL parameters to temporarily enable a feature during development:

class OrganizationFeature(Flag):
    url_param_name = ""

    def __init__(self, request=None, organization=None):
        # Both of these are optional, but will usually both be given
        self.request = request
        self.organization = organization

    def get_key(self):
        if (
            self.url_param_name
            and self.request
            and self.url_param_name in self.request.query_params
        ):
            return None

        if not self.organization:
            # Don't save the flag result for PRs without an organization
            return None

        return self.organization

    def get_value(self):
        if self.url_param_name and self.request:
            if self.request.query_params.get(self.url_param_name) == "1":
                return True

            if self.request.query_params.get(self.url_param_name) == "0":
                return False

        if not self.organization:
            return False

        # All organizations will start with False,
        # and I'll override in the DB for the ones that should be True
        return False


class AIEnabled(OrganizationFeature):
    pass

FAQs

How do flags get stored in the database?

When you first use a flag, plain.flags creates a Flag record in the database to track the flag itself, and a FlagResult record for each unique key. The FlagResult stores the computed value so subsequent calls return the cached result.

How do I override a flag value?

You can modify flag results directly in the database or through the admin interface. Each FlagResult has a value field that you can update to override the computed value.

What if I want to temporarily compute the value without storing it?

Return a falsy value (like None) from get_key(). When there's no key, the flag will compute the value fresh each time without storing it in the database.

How do I disable a flag entirely?

Each Flag record has an enabled field. Set it to False to disable the flag. In debug mode, accessing a disabled flag raises a FlagDisabled exception. In production, it logs an error and returns None.

Installation

Install the plain.flags package from PyPI:

uv add plain.flags

Add to your INSTALLED_PACKAGES:

INSTALLED_PACKAGES = [
    ...
    "plain.flags",
]

Create a flags.py at the top of your app (or point settings.FLAGS_MODULE to a different location).