alumni_lookup

Phase 20 — Feature Flags & Phased Launch

Canonical sources: Portal philosophy, posture, and language live in /docs/planning/alumni-network/source/README.md. Prefer quoting/paraphrasing over inventing new language.

Status: ✅ Complete (20.1, 20.2, 20.3 + content-surface follow-up all shipped) — pending release tag Scope: belmontalum.com portal (Cp::) ONLY. Nothing in this phase touches the alumnichampions.com public signup flow (Phase 19). Related: FEATURE_FLAGS.md (existing env-var pattern), Cp::DashboardVisibility


1. Overview

Problem

The Champion Portal has grown too large to get approved as one launch. We need to ship a stripped-down, approvable core and re-introduce the heavier features one at a time as each gets sign-off — without deleting or rebuilding anything.

Solution

Staff-toggleable feature flags that hide five portal features from users while leaving all data, models, background jobs, and staff-side management fully intact. Flip a flag on when the feature is approved; flip it off instantly if needed.

The five flags

| Flag key | Feature | Default (prod) | |———-|———|—————-| | news | Community News (Phase 1.10 + Phase 14 submissions) | OFF | | photos | Photo Albums (Phase 5.5) | OFF | | events | Events (Phase 1.11 + Phase 14 submissions) | OFF | | champion_role | Champion Role microsite + role cards + role_ideas (Phase 11/13) | OFF | | your_impact | Your Impact dashboard card (Phase 13) | OFF |

The launchable “basics” core (everything with all five OFF)

Auth / Profile / Directory / Communities (info + members + discussions) / Connections + Messaging / Careers. A coherent “alumni directory + communities” MVP that can be approved on its own.


2. Decisions (Planning Interview, 2026-07-20)

Question Decision
Mechanism DB-backed admin toggle UI. cp_feature_flags table + a Champion Admin page where portal_admins flip switches live — no deploy/dyno restart. Env var retained as an emergency kill-switch override.
Staff side while dark Keep staff side fully live. Flags hide ONLY champion-facing surfaces. Staff keep creating/editing news, events, albums, role ideas in Champion Admin so content is staged and ready the moment a flag flips on.
Direct-URL when OFF Redirect to dashboard (cp_home_path), no error. A logged-in user who bookmarked a gated page is bounced home.
Champion Role reach Portal (belmontalum.com) ONLY. Nothing touches the just-launched alumnichampions.com Phase 19 signup flow. The public interest-spectrum quiz is entirely out of scope.
Profile-wizard role step Skip entirely when champion_role is OFF (wizard flows straight past it). Users never encounter roles anywhere in the portal.
Your Impact ↔ Role Independent flags. No hard coupling — five fully independent switches. (Operational note: don’t run your_impact ON with champion_role OFF, since Impact stats are role-framed.)

3. Per-Environment Behavior

Each environment has its own database, so a single enabled boolean per flag is naturally per-environment. No per-env columns needed.

Resolution order (first match wins) for cp_feature_enabled?(:news):

  1. ENV override — if FEATURE_NEWS is set ("true"/"false"), it wins. Emergency kill-switch, matches existing FEATURE_CHAMPION_PORTAL pattern.
  2. DB valueCp::FeatureFlag row’s enabled column.
  3. Defaultfalse (fail-closed).

Environment defaults after deploy (implemented: rows are created with enabled: !Rails.env.production?, so existing dev/staging behavior is unchanged and production is the only env that starts dark): | Env | How flags arrive | Expected state | |—–|——————|—————-| | Development | Migration data-insert (or db/seeds.rb for schema:load), enabled: true | All ON (local dev unaffected) | | Test | Fixtures test/fixtures/cp/feature_flags.yml, all enabled: true | All ON (existing suite unaffected; individual tests disable as needed) | | Staging | Migration inserts rows enabled: true; staff flip OFF to preview the stripped launch | Toggle per feature during QA | | Production | Migration inserts rows enabled: false | All OFF at launch; flip ON per approval |


4. Feature Footprint (what each flag must reach)

Flag hides the user-facing column only. Everything in Staff/admin and Data preserved stays live.

Feature User-facing surfaces to gate Staff/admin (STAYS LIVE) Data preserved Flow amendment when OFF
News Community show “Latest News” block; News index/show; toggle_like; news submit flow; My Submissions (news items); dashboard What’s-New / activity feed news items champions/news_posts CRUD; Phase 14 submission review cp_news_posts, submissions, likes Community left column reflows to Discussions-only; submission CTA hidden; activity feed filters out news
Photos Community “Photo Albums” carousel; album index/show; event→album “create album” link champions/photo_albums + photos CRUD albums, cp_photos, ActiveStorage blobs Community carousel removed; event create-album hidden
Events Dashboard sidebar :events (desktop + mobile “events first” block); community “Upcoming Events” + RSVP click; event index/show; event submit flow; My Submissions (event items) champions/events CRUD; submission review cp_events, RSVPs, submissions Community right column reflows; dashboard drops events sidebar; mobile events block gone
Champion Role Roles microsite (/roles, /roles/:role, /champion-info); role select/remove; role_idea_clicks; dashboard role_ideas main section + role card; champion_microsite_nav; profile-wizard role quiz step (skipped); has_champion_role? branches champions/role_ideas CRUD; click analytics roles on cp_champions, quiz answers, role_idea content Wizard skips role step; microsite nav hidden; dashboard drops role_ideas + role card
Your Impact Dashboard sidebar :your_impact card (no standalone route) derived stats (no dedicated table) Sidebar drops the card

5. Architecture

5.1 Model & Schema

# cp_feature_flags
t.string  :key,         null: false      # "news", "photos", "events", "champion_role", "your_impact"
t.boolean :enabled,     null: false, default: false
t.string  :name                          # display label, e.g. "Community News"
t.text    :description                   # admin-facing explanation of what it gates
t.references :last_toggled_by, foreign_key: { to_table: :users }, null: true
t.timestamps
# add_index :cp_feature_flags, :key, unique: true
# app/models/cp/feature_flag.rb
module Cp
  class FeatureFlag < ApplicationRecord
    KEYS = %w[news photos events champion_role your_impact].freeze
    validates :key, presence: true, uniqueness: true, inclusion: { in: KEYS }

    # Resolution: ENV override → DB → false. Cached; busted on write.
    def self.enabled?(key)
      env = ENV["FEATURE_#{key.to_s.upcase}"]
      return env == "true" if env.present?
      cached_map.fetch(key.to_s, false)
    end

    def self.cached_map
      Rails.cache.fetch("cp_feature_flags/map", expires_in: 1.hour) do
        where(enabled: true).pluck(:key).index_with { true }
      end
    end

    after_commit { Rails.cache.delete("cp_feature_flags/map") }
  end
end

Caching: flags are read on nearly every portal request/view — the cached_map (a small hash of enabled keys) is cached and invalidated on write. Watch Heroku memory (see /performance): cache the compact map, not full records.

5.2 Helper API

5.3 Integration seams

  1. Controllersbefore_action :require_feature!, only: [...] (or class-level) in Cp::NewsController, EventsController, PhotoAlbumsController, RolesController, NewsSubmissionsController, EventSubmissionsController. Off → redirect_to cp_home_path.
  2. DashboardCp::DashboardVisibility#show? also consults the flag map: events → :events, your_impact → :your_impact, role_ideas → :champion_role. Flag OFF wins regardless of journey stage. (Single choke point covers most dashboard rendering.)
  3. Views — wrap community-show News/Photos/Events sections and nav/footer/mobile-nav links in cp_feature_enabled?. Reflow community columns gracefully when a section is hidden (no empty gaps).
  4. Profile wizard — skip the role step when champion_role OFF (step sequencing in the wizard controller/service).
  5. Activity feed / What’s-New — exclude news (and event) items when their flags are OFF.
  6. Admin UIChampions::FeatureFlagsController (index + update) at /champions/feature_flags, portal_admin-guarded, listed in Champion Admin nav (Settings area). Toggle switches with the flag name + description; stamp last_toggled_by.

6. Sub-Phases

20.1 — Flag Foundation + Admin UI ✅ COMPLETE

What Was Implemented (20.1):

Spec deviation (20.1): §3 originally drafted staging rows as enabled: false. Implemented as enabled: !Rails.env.production? instead, so existing dev/staging behavior is untouched and only production starts dark — fewer surprise regressions. §3 table updated to match.

20.2 — Gate Content Features (News, Photos, Events) ✅ COMPLETE

What Was Implemented (20.2):

Deferred (noted, not built): The weekly digest mailer (cp/notification_mailer/weekly_digest) still lists news/events/photos content. Not gated in 20.2 — digests are a separate notifications concern and, with content flags off at launch, there’s little new content to surface; any links resolve to the dashboard via the controller guards rather than erroring. Follow-up candidate → BACKLOG.

20.3 — Gate Champion Role + Your Impact ✅ COMPLETE

What Was Implemented (20.3):

Spec deviation (20.3): The plan said “skip the profile-wizard role step.” There is no champion_role step in the wizard’s STEPS array — the stale doc comment in ProfileWizardController (lines 6-13) implied one, but the role quiz is a set of dedicated actions (quiz/save_quiz_answer/quiz_results/select_quiz_role) reached from the role card / nudge / roles microsite, not a linear step. So there was nothing to skip; instead those quiz actions are gated directly. Also added beyond the literal spec: gating the champion_role_badge helper so role badges disappear everywhere at once (cleaner than editing ~10 views), and gating the champion_nudge (it is entirely role-promotion copy → “Find Your Role”).

Content-Surface Follow-Up (post-20.3, from user litmus-test feedback)

A “search for news/events/champion role” sweep surfaced non-functional references the controller/dashboard gating didn’t reach:


7. Testing Requirements

8. Data Preservation (answers “don’t rebuild when re-enabled”)

9. Activity Tracking

10. Launch Guide

Create docs/planning/qa/PHASE_20_LAUNCH_GUIDE.md during implementation: the migration, the seed/fixture step, per-environment default states, and the “flip to launch” runbook (which flags ON for the basics launch = none; re-enable order as approvals land).

11. Deliverables Checklist