Runbook: Enable Family Manager for an Org (Feature Flags)
Owner: Platform Team Backup owner: On-call engineer Last validated: 2026-06-14 Validation method: Per-household allowlist on a
consumer_householddogfood org viascripts/fm_beta_enable.py, verified against the asyncis_feature_enabled_for_orgresolution path Severity trigger: SEV3 (feature inoperable / wrong cohort sees a surface — not data-loss) Customer impact: None on B2B traffic. FM mobile routes return404 feature_not_enabledfor any household not yet allowlisted; the surface stays hidden until granted. Required access: SSH (VPS), thecurateme-backend-b2bcontainer (B2B MongoDB) Related services: curateme-backend-b2b, MongoDB (B2B DBorg_feature_flag_overrides) Time to complete: ~3 minutes per household
Family Manager is the consumer-household mobile facade built on the platform as a dogfood/reference org. Its surfaces are gated by eight FM_* feature flags (src/config/feature_flags.py). In production every FM_* flag is explicit-OFF — the only ways to arm a household are an env-global FF_FM_*=true (heavy-handed, all orgs) or, the sanctioned path, a per-org override row in org_feature_flag_overrides.
This runbook covers the per-org enable: what each flag does, how the production explicit-OFF + per-org-override resolution actually works, the exact upsert via the sanctioned scripts/fm_beta_enable.py tool, and how to verify a flag resolved ON for the org.
Prerequisites
Before starting, confirm:
- SSH access to the platform VPS —
ssh curateme@178.105.8.25 - The target org id (e.g.
org_abc123) and that it is aconsumer_householdtenant — the script refuses any other tenant profile without--force(a B2B workspace must never gain consumer surfaces by a typo’d org id; seescripts/fm_beta_enable.py). - The flag work runs against the B2B app DB, so commands execute inside
docker exec curateme-backend-b2b(APP_MODULE=src.main_b2b:app,CURATE_API_MODE=b2b).
The eight FM_* flags
Each flag gates one sibling mobile router; flag OFF → the router’s routes return 404 with code="feature_not_enabled" (the hidden surface never confirms it exists). Source of truth: FeatureFlag in src/config/feature_flags.py.
Flag (flag_name) | Surface |
|---|---|
fm_assistant_profile | Household assistant profile (Phase 1) |
fm_inbound_email | Forwarding alias + sender verify + inbound webhook (Phases 2-3) |
fm_documents | Document vault + parsing + previews (Phase 4) |
fm_memory | Household memory facts + timeline (Phase 6) |
fm_expenses | Consumer expense/invoice ledger (Phase 5) |
fm_ask | Grounded Ask over docs + memory (Phase 7) |
fm_llm_extraction | Sensitive — third-party LLM extraction/polish egress (M2 + Phase 7) |
fm_premium_identity | Sensitive — M365 AgentIdentity adapter (real, paid mailboxes) (Phase 8) |
The Phase-2 “wedge” default set the tool writes is the first seven (everything except fm_premium_identity); see default_beta_flags() in scripts/fm_beta_enable.py.
Two sensitive flags — treat both as exceptions:
fm_llm_extractionis the kill switch on the third-party LLM egress seam itself (everyGatewayLLMExtractormodel call for capture/email escalation + Ask polish). It is distinct from the per-memberthird_party_aiconsent gate, which always applies regardless of this flag. Enabling it means household data (pseudonymized — member names tokenized, emails/phones/addresses regex-tokenized; seeservices/mobile/llm_extraction.py) can leave to a model provider. Only enable for a household that has agreed to LLM-assisted extraction.fm_premium_identityprovisions real, paid M365 mailboxes (licensing cost). It is pinned OFF in every environment by_fm_default()and is deliberately absent from the tool’s default set. It is refused even when named explicitly unless you pass--force.
How production resolution works (why prod is OFF)
The prod curateme-backend-b2b container runs with ENVIRONMENT=production (docker-compose.production.yml). is_feature_enabled / is_feature_enabled_for_org resolve an FM_* flag in this precedence (highest first):
- Per-org override —
org_feature_flag_overridesdoc{org_id, flag_name, enabled}(only the asyncis_feature_enabled_for_orgpath reads this; the member routes use it viaservices/mobile/feature_gate.require_member_feature). FF_FM_*env var —true/falseenv-global kill switch._fm_default(flag)— env-aware default._fm_environment()readsENVIRONMENT→CURATE_ME_ENV→APP_ENVand fails CLOSED: production and any unrecognized label (a typo’dprodution) → everyFM_*flag isFalse.fm_premium_identityisFalsehere regardless of environment.DEFAULT_FLAGS.
So in production, with no override and no env var, every FM_* flag is False (_fm_default step 3). A per-org override is the surgical way to turn the wedge ON for exactly one household.
Verification: Confirm the box is genuinely production-resolving before relying on overrides:
ssh curateme@178.105.8.25 docker exec curateme-backend-b2b printenv ENVIRONMENT # expect: production
Step 1: Enable the wedge flags for the household
Use the sanctioned tool — it carries the consumer_household guard, the fm_premium_identity refusal, and prints the resolved per-org state. Run it inside the B2B container.
Default wedge set (the first seven flags; fm_premium_identity excluded):
ssh curateme@178.105.8.25
docker exec curateme-backend-b2b \
python scripts/fm_beta_enable.py org_abc123To enable only a subset:
docker exec curateme-backend-b2b \
python scripts/fm_beta_enable.py org_abc123 --flags fm_documents fm_askFor each flag the tool upserts one org_feature_flag_overrides row with the exact {org_id, flag_name} filter, setting enabled: true and updated_at (see apply_overrides()). The filter is always the org+flag pair, so the tool can never touch another tenant’s overrides.
If the org is not a consumer_household tenant, the script exits 2 with refused: ... and writes nothing — fix the org id rather than reaching for --force.
Verification: The command prints a per-org flag-state table. Confirm each requested flag shows effective=True, override=True, source=org_override:
flag effective override source
fm_assistant_profile True True org_override
fm_inbound_email True True org_override
...
fm_llm_extraction True True org_override
fm_premium_identity False - env_defaulteffective is computed via the same async is_feature_enabled_for_org path the routes use, so this column is the ground truth for what the household will experience.
Step 2: Verify the override row landed in MongoDB
Cross-check the written rows directly (read-only sanity check), inside the B2B container:
docker exec curateme-backend-b2b python - <<'PY'
import asyncio
from src.database.mongo_db import get_b2b_db
async def main():
coll = get_b2b_db().get_collection("org_feature_flag_overrides")
cursor = coll.find({"org_id": "org_abc123"})
async for doc in cursor:
print(doc["flag_name"], doc["enabled"], doc.get("updated_at"))
asyncio.run(main())
PYVerification: Each enabled flag appears once with enabled=True. The doc shape is exactly {org_id, flag_name, enabled, updated_at} (lock-step with is_feature_enabled_for_org). fm_premium_identity must not appear unless you deliberately forced it.
Step 3: Confirm the member routes resolve ON for the org
The mobile routers gate on require_member_feature(request, flag), which awaits is_feature_enabled_for_org after auth and raises 404 feature_not_enabled when the surface is not enabled. Re-running the tool’s resolver is the fastest confirmation (no override write):
docker exec curateme-backend-b2b python - <<'PY'
import asyncio
from src.config.feature_flags import FeatureFlag, is_feature_enabled_for_org
async def main():
for flag in (FeatureFlag.FM_INBOUND_EMAIL, FeatureFlag.FM_DOCUMENTS,
FeatureFlag.FM_ASK, FeatureFlag.FM_LLM_EXTRACTION):
on = await is_feature_enabled_for_org(flag, "org_abc123")
print(flag.value, on)
asyncio.run(main())
PYVerification: Each enabled flag prints True. A member-authed request to the corresponding surface (e.g. the household’s app hitting an fm_documents route) now returns its real payload instead of 404 feature_not_enabled. No restart is needed — the override is read live from Mongo on each async resolution.
Rollback / If it goes wrong
Disable the household (per-org kill, surgical)
Write enabled=false overrides for the same org — this beats any env default and immediately re-hides the surfaces:
docker exec curateme-backend-b2b \
python scripts/fm_beta_enable.py org_abc123 --disableVerification: Re-run Step 1’s table or Step 3’s resolver — effective should now be False with source=org_override. Member routes return 404 feature_not_enabled again.
Accidentally allowlisted a non-household org
The guard normally prevents this. If --force was used in error, run --disable --force for that org, then delete the stray rows:
docker exec curateme-backend-b2b python - <<'PY'
import asyncio
from src.database.mongo_db import get_b2b_db
async def main():
coll = get_b2b_db().get_collection("org_feature_flag_overrides")
res = await coll.delete_many({"org_id": "org_WRONG", "flag_name": {"$regex": "^fm_"}})
print("deleted", res.deleted_count)
asyncio.run(main())
PYfm_premium_identity (paid M365) got enabled
If fm_premium_identity shows effective=True for any org, disable it immediately and confirm no mailbox was provisioned. It should never be ON without an explicit, reviewed --force decision (real licensing cost):
docker exec curateme-backend-b2b \
python scripts/fm_beta_enable.py org_abc123 --flags fm_premium_identity --disable --forceEnv-global flag set by mistake
If FF_FM_*=true was added to the VPS ~/platform/.env.production (arming all orgs, not just the allowlisted household), remove that line, then redeploy the backend so the container picks up the change:
# On the VPS, edit ~/platform/.env.production — remove the FF_FM_* line.
# Then, from your local machine:
./scripts/deploy-to-vps.sh --backend--backend rebuilds and restarts backend-b2b (plus gateway, runner, celery). After it, re-run Step 3 to confirm only the intended household resolves ON.
Related
- Agent Identity Provisioning — the M365 provisioning path that
fm_premium_identityrides; relevant before enabling premium identity. services/backend/src/config/feature_flags.py—FeatureFlagenum,_fm_default/_fm_environmentresolver,is_feature_enabled/is_feature_enabled_for_org.services/backend/scripts/fm_beta_enable.py— the sanctioned per-household allowlist tool (--flags,--disable,--force,--json).services/backend/src/services/mobile/feature_gate.py—require_member_feature(the404 feature_not_enabledgate on member-authed routes).services/backend/src/services/mobile/tenancy.py—CONSUMER_HOUSEHOLD/get_tenant_profile(the tenant guard the tool enforces).