diff --git a/docs/ops/feature-flags.md b/docs/ops/feature-flags.md new file mode 100644 index 0000000..049abff --- /dev/null +++ b/docs/ops/feature-flags.md @@ -0,0 +1,80 @@ +# Feature flags: whether this project wants a manager + +Findings for #318. Tool capabilities and pricing were checked on 2026-09-08; everything about this repository was read from the code on 2026-09-09. + +**Status: do not adopt a feature flag manager. Not yet, and possibly not ever at this size.** This is written down so the question does not get reopened from scratch — including the conditions that would change the answer. + +## The short version + +This project already has two flag mechanisms, uses both idiomatically, and is not short of a third. What it lacks is a boolean in one of them, which is about twenty lines of work and needs a reason before it is worth doing. + +No tool was compared until that was established, because a spike that starts by comparing platforms will always find one. + +## What already exists + +**Environment variables, for per-environment gating.** `DEMO_MODE`, `REMBG_URL`, `BREVO_TRACKER_KEY`, `ANTHROPIC_API_KEY`, the USPS and SMTP credentials. The convention is consistent and documented: unset means the feature does not exist, and the application is *working* rather than broken. QA runs with several deliberately empty, which is how it cannot reach PayPal, cannot email anyone, and cannot report browsing into the live Brevo account. + +That is deploy-time gating, and it is genuinely a feature flag system — it just is not called one. + +**`admin_settings`, for runtime tuning by whoever runs the shop.** A typed key/value store with an admin UI, ten settings, per-setting fallbacks, and one place to add another. Its own header says the point plainly: adding a setting means adding a row and nothing else. `drafting_model` and `intake_notify_email` are there rather than in the environment for exactly the reason a flag would be — they are changed by the person running the shop, not the person deploying it, and a redeploy to change an email address would be absurd. + +The two are complementary and the split is already the right one. + +## What is actually missing + +`admin_settings` supports `hours`, `count`, `text` and `choice`. **There is no boolean.** + +So the one thing a flag manager would give this project that it does not have — an admin-flippable on/off for a code path, without a rebuild — is a missing value type in a store that already does everything else. Adding it is a definition entry, a parse branch, and a control in the settings tab. + +That is the whole gap. It is not a reason to run another service on the NAS. + +## Is there anything to flag right now + +No, and the issue said a spike that cannot name one should say so. + +Nine issues are open. Seven are the Passkeys epic and #313, and neither wants a flag: + +- **Passkeys (#37–#42)** is naturally incremental — schema, then registration, then the authentication ceremony, then management, then the login page. Nothing is user-visible until #41 puts it on the login form, so the ordering does the gating that a flag would otherwise do. The epic has also been untouched since 2026-08-21 and its parent #36 is closed, so its status is undecided in a way no tooling addresses. +- **#313's `trust proxy` change cannot be flagged.** It is only correct once Cloudflare is actually in front, so it must ship with the DNS cutover. A runtime toggle would let it be wrong on purpose, which is worse than not having one. + +**The strongest hypothetical is a kill switch for passkey login**, because #41 records that it is the one feature in the project that cannot be fully proven in QA — credentials bind to the RP ID, so production is the first place it runs for real. Being able to turn it off without a rebuild would have value there. + +That is a real argument, and it is still not enough today: passkeys are not being built, and **production is not live**, so there is no customer-facing behaviour to protect. Revisit it when #41 is actually being written. + +## What flags would buy here, honestly + +The usual headline — decouple deploy from release — buys much less than it does elsewhere, because there is no CD. Every change already reaches production through a manual NAS rebuild, so the deploy is a deliberate act either way. + +What is left is narrower: + +- **A kill switch**, avoiding a rebuild. The strongest case, because the rebuild is this project's slowest and most error-prone step. +- **Shipping half-finished work** behind a flag rather than holding a long branch. Weak here: branches are short and merged the same day. +- **Per-environment behaviour** without another env var. Already solved, by the env vars. + +Against all of it: every flag is a branch in the code that has to be removed later, and stale flags are their own debt. For a single-developer shop, that cost is paid by the same person who would benefit. + +## If a tool is ever wanted + +Constraints that decide it: the NAS is CPU and memory limited and already runs Gitea, the CI runner, QA and production; Portainer manages the stacks; cost should stay at zero. + +| Tool | Fit | +| --- | --- | +| **Flipt** | Lightest — single binary, no heavy datastore. The only one that obviously fits this machine. | +| **Flagsmith** | Open source, self-host free with no feature limits, Docker Compose. Heavier than Flipt. | +| **GrowthBook** | Free self-hosted and a free cloud starter. Flags and experiments together, which is more than this project needs. | +| **Unleash** | Best known, but **OSS Edge sunsets 2026-12-31**, after which self-hosters at scale need Enterprise Edge. A free tool acquiring a paid dependency inside a year is the wrong shape here. | + +A hosted free tier is also possible and is worse for this specific project: it makes the storefront depend on a third party being reachable in order to decide its own behaviour, which is a new outage mode for a site that currently has none. + +**Whatever is chosen, fail-safe behaviour matters more than features.** A flag service that takes the shop down when it is unreachable is worse than having no flags. `admin_settings` reads from the database the application already cannot run without, so it has no failure mode of its own — which is a real advantage over every option in that table. + +## What would change this answer + +Any one of these, and it is worth reopening: + +- A concrete change that genuinely needs to be dark in production while it settles — passkey login being the likely first. +- Percentage rollouts or per-customer targeting becoming something anyone actually wants. Nothing in the project has ever asked for either. +- More than one person deploying, so "who turned that off" needs an audit trail. +- CD arriving, which would make decoupling deploy from release mean something. + +Until one of those, the answer is a boolean column type in `admin_settings`, written when there is a flag to put in it.