feat(flags): add the Unleash stack and its runbook
Its own Portainer stack, belonging to neither application stack: a staging redeploy must not be able to disturb production's flag state. One instance serves both. OSS Unleash ships development and production environments with environment-scoped client tokens, so the same flag holds independent state in each — which is what lets a feature be on in staging, where the E2E journeys exercise it, while production stays dark. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015mWQnpye9F299NVRCCSRvj
This commit is contained in:
1 parent
c339c2f1a1
commit
01ccbb8e82
2 files changed
+124
No files matched your search
@@ -150,3 +150,54 @@ token Gitea Actions provides automatically (`secrets.GITEA_TOKEN` — no setup
|
||||
needed), and fails the check only when a finding is rated
|
||||
**severe** (would break prod, leak data, or corrupt data). Minor findings are
|
||||
informational and never block a merge.
|
||||
|
||||
## Feature flags (Unleash)
|
||||
|
||||
Flag state lives in a self-hosted Unleash instance, deployed as its own
|
||||
Portainer stack from `docker-compose.portainer.unleash.yml`. It is separate
|
||||
from the application stacks on purpose — redeploying staging must not be able
|
||||
to disturb production's flags.
|
||||
|
||||
The flags themselves are declared in `backend/flags.py`. Unleash holds the
|
||||
state; the registry holds the list. A flag in the UI that is not in the
|
||||
registry is orphaned and nothing reads it.
|
||||
|
||||
### First-time setup
|
||||
|
||||
1. Deploy the stack in Portainer. Set `UNLEASH_DB_PASSWORD`,
|
||||
`UNLEASH_ADMIN_PASSWORD` and (optionally) `UNLEASH_IP`.
|
||||
2. Log in to the UI at `http://<UNLEASH_IP>:4242` as `admin`.
|
||||
3. Create one **client** API token per environment:
|
||||
- `schoolcompare-staging`, environment **development**
|
||||
- `schoolcompare-prod`, environment **production**
|
||||
|
||||
Client tokens, not admin tokens — the backend only reads.
|
||||
4. Put each token in the matching Portainer stack's `UNLEASH_API_TOKEN`
|
||||
variable, and set `UNLEASH_URL` to `http://<UNLEASH_IP>:4242/api`.
|
||||
5. Redeploy the application stacks.
|
||||
|
||||
### Turning a feature on
|
||||
|
||||
Toggle the flag in the environment you want. Flags appear in the Unleash UI
|
||||
after the backend has evaluated them once, so a newly declared flag shows up
|
||||
shortly after the deploy that introduced it.
|
||||
|
||||
A flip reaches school pages within about five minutes and place pages within
|
||||
the hour. Next's ISR does the propagating — it revalidates a route at the
|
||||
*lowest* `revalidate` among that route's fetches, which is 300s for
|
||||
`/school/[slug]` and 3600s for the place pages. There is no webhook, and
|
||||
adding one would only be worth it if flips ever needed to be instant.
|
||||
|
||||
### When Unleash is unreachable
|
||||
|
||||
Every flag evaluates to `False` and the site serves as though nothing were
|
||||
switched on. That is deliberate — an unfinished feature staying hidden is the
|
||||
safe direction — but it means a *released* feature disappears if a backend
|
||||
container cold-starts with an empty cache while Unleash is down. The SDK's
|
||||
disk cache is on a named volume so restarts keep last-known state, and flags
|
||||
are removed from the code within 90 days (enforced by a test), which bounds
|
||||
how long any feature is exposed to this.
|
||||
|
||||
If `UNLEASH_URL` is unset, every flag is `False` and no connection is
|
||||
attempted. That is the correct behaviour for local development and CI, and it
|
||||
means the test suites need no flag server.
|
||||
Reference in new issue
Block a user