Feature flags let you ship code in a disabled state and then activate it for specific users, groups, or percentage rollouts — without a redeploy. The flag store is pluggable so you can start with an in-memory store during development, switch to Postgres in production, and keep a consistent API throughout.


Quick start

1. Register the flag store

Rust
use autumn_web::feature_flags::InMemoryFlagStore;

autumn_web::app()
    .with_flag_store(InMemoryFlagStore::new())
    .routes(routes![dashboard])
    .run()
    .await;

In production, replace InMemoryFlagStore with the Postgres-backed store so that flags survive restarts and propagate across replicas:

Rust
use autumn_web::feature_flags::pg::PgFlagStore;

autumn_web::app()
    .with_flag_store(PgFlagStore::new(&config.database.primary_url))
    .run()
    .await;

Run the bundled migration to create the autumn_feature_flags and feature_flag_changes tables:

Shell
autumn migrate

2. Gate a handler

The Flags extractor resolves the current actor from the session and exposes a flags.enabled(key) method:

Rust
use autumn_web::prelude::*;
use autumn_web::feature_flags::Flags;

#[get("/dashboard")]
async fn dashboard(flags: Flags) -> Markup {
    html! {
        @if flags.enabled("beta_inbox") {
            (render_beta_inbox())
        } @else {
            (render_classic_inbox())
        }
    }
}

3. Gate a whole handler with the macro

The #[feature_flag] attribute macro returns 404 when the flag is disabled and the actor is not in any allow-list:

Rust
use autumn_web::prelude::*;

#[get("/new-dashboard")]
#[feature_flag("beta_dashboard")]
async fn new_dashboard() -> &'static str {
    "New dashboard"
}

Supply a custom fallback handler for a nicer response:

Rust
#[get("/new-dashboard")]
#[feature_flag("beta_dashboard", fallback = upgrade_prompt)]
async fn new_dashboard() -> &'static str {
    "New dashboard"
}

async fn upgrade_prompt() -> impl IntoResponse {
    (StatusCode::FORBIDDEN, "This feature is not yet enabled for your account.")
}

Evaluation order

For a given (flag, actor) pair, rules are checked in this order:

PriorityRuleResult when…
1Kill switchenabled = falsealways off (overrides all other rules)
2Global onenabled = true AND rollout_pct >= 100always on for every actor
3Actor allowlistenabled = true AND the actor's ID is in actor_allowlist → on for that actor
4Group allowlistenabled = true AND the actor belongs to a group in group_allowlist → on
5Percent rolloutenabled = true AND actor's deterministic bucket < rollout_pct → on
6DefaultReturns false (fail-closed)

Note: enabled = true alone does not enable a flag for all actors — it only means the kill switch is off. To enable for everyone set rollout_pct = 100 (e.g. autumn flags enable <key> which sets both enabled = true and rollout_pct = 100). Setting enabled = false is an instant kill switch that overrides rollout percentages and all allowlists.


Kill switches

A flag with enabled = false and no allow-lists acts as a kill switch: it is off for every actor regardless of rollout percentage. You can disable any feature instantly at the CLI:

Shell
autumn flags disable dark_mode

This writes to the database and broadcasts a NOTIFY autumn_flags message. All replicas listening on that channel invalidate their cache and pick up the new state within one cache-refresh window (default: 1 second).


Percent rollouts

Percent-rollout buckets are computed with a FNV-1a hash over the UTF-8 encoding of "<flag_name>:<actor_id>" modulo 100. This means:

  • Stable: the same actor always lands in the same bucket across restarts.
  • Independent: changing rollout_pct only shifts the cohort boundary, it does not reassign any existing actor's bucket.
  • No external dependency: the hash is computed in-process without a third-party library.
Shell
autumn flags set-rollout dark_mode 25   # enable for 25% of actors
autumn flags set-rollout dark_mode 100  # roll out to everyone

Actor allowlists

Add individual actors to the allowlist for early-access testing:

Shell
autumn flags allow dark_mode user:42

Allowlists are evaluated before the percent rollout, so allowlisted actors always see the feature even if their bucket is above the rollout threshold.


Group allowlists

Register a group resolver at startup to enable named-group gates:

Rust
use autumn_web::feature_flags::{FeatureFlagService, GroupResolver, InMemoryFlagStore};
use std::sync::Arc;

let store = Arc::new(InMemoryFlagStore::new());
let service = FeatureFlagService::new(store)
    .with_group_resolver(Arc::new(|actor_id, group| {
        // Return true when actor_id is a member of group.
        is_in_group(actor_id, group)
    }));

Then add groups to a flag:

Rust
service.add_group("beta_feature", "beta_testers", Some("cli")).unwrap();

CLI reference

Code
autumn flags list                       # list all flags with their current state
autumn flags enable <key>               # globally enable a flag (all actors)
autumn flags disable <key>              # globally disable a flag
autumn flags set-rollout <key> <pct>    # enable for pct% of actors (0–100)
autumn flags allow <key> <actor_id>     # add actor_id to the explicit allowlist

Admin UI

Register FeatureFlagAdminModel with the admin plugin for a web-based flag management panel:

Rust
use autumn_admin_plugin::{AdminPlugin, prelude::*};
use autumn_admin_plugin::feature_flags::FeatureFlagAdminModel;

autumn_web::app()
    .plugin(
        AdminPlugin::new()
            .register(FeatureFlagAdminModel::default()),
    )
    .run()
    .await;

The panel is mounted at /admin/feature-flags/ and provides:

  • List view: key, enabled status, rollout %, actor allowlist
  • Edit view: toggle enabled, set rollout %, manage allowlists
  • History tab: per-flag audit trail from feature_flag_changes

Testing

Use InMemoryFlagStore and TestApp::with_flag_store to control flags in tests without touching a database:

Rust
use autumn_web::feature_flags::InMemoryFlagStore;
use autumn_web::test::TestApp;
use std::sync::Arc;

#[tokio::test]
async fn beta_inbox_is_hidden_by_default() {
    let client = TestApp::new()
        .with_flag_store(InMemoryFlagStore::new())
        .routes(routes![dashboard])
        .build();

    client.get("/dashboard").send().await.assert_ok();
    // beta inbox is NOT present when flag is off
}

#[tokio::test]
async fn beta_inbox_renders_when_flag_enabled() {
    let store = InMemoryFlagStore::new();
    store.enable("beta_inbox", None).unwrap();

    let client = TestApp::new()
        .with_flag_store(store)
        .routes(routes![dashboard])
        .build();

    client
        .get("/dashboard")
        .send()
        .await
        .assert_ok()
        .assert_body_contains("beta-inbox");
}

Pluggable FlagStore

Implement FlagStore to back flags with any storage system (Redis, etcd, LaunchDarkly SDK, etc.):

Rust
use autumn_web::feature_flags::{
    FlagChangeRecord, FlagConfig, FlagStore, FlagStoreError,
};

struct MyFlagStore { /* ... */ }

impl FlagStore for MyFlagStore {
    fn get(&self, key: &str) -> Result<Option<FlagConfig>, FlagStoreError> {
        // ...
    }
    // implement the remaining methods
}

autumn_web::app()
    .with_flag_store(MyFlagStore { /* ... */ })
    .run()
    .await;