Canonical sources: Portal philosophy, posture, and language live in
/docs/planning/champion-portal/source/README.md.
Use these sources (includingCHRIST_CENTERED__IDENTITY_STATEMENT.md) when writing specs or user-facing copy. Prefer quoting/paraphrasing over inventing new language.
Status: ✅ Complete (February 2026) Estimated Effort: 6-8 weeks Prerequisites: Phase 5 Complete, Phase 8 Complete (Notifications & Digests)
Completion Summary
All 6 sub-phases delivered. The Connections system fully replaces the old Contacts and open-messaging systems.
Key deliverables:
cp_connection_requestsandcp_connectionstables with canonical pair ordering- Connection request flow: 3 types (Say Hi, Career Advice, Networking), rate limiting, requestee preferences
- Two-tab Connections hub replacing Messages nav (Connections + Requests tabs)
- Connection-gated messaging (must be connected to message, except support threads)
- Directory & profile integration: “open to” type pills, connect button, filter by connection type
- Contacts → Connections migration: mutual contacts converted, one-way removed, ChampionContact model deleted
- Admin connection metrics dashboard (stats, super-connectors, request rates)
- Engagement score service hardened for blank event_type edge case
Tests: 3197 runs, 8143 assertions, 0 failures, 0 errors
Deferred: 7 items moved to BACKLOG.md (see bottom of this file)
Related Documents:
- ../README.md — Phase Index
- ../../JOBS-TO-BE-DONE.md — User needs (Jobs C7, C9)
- ../../development/DESIGN-GUIDELINES.md — Visual standards
- ../../development/LANGUAGE_STYLE_GUIDE.md — Voice & tone
- ../../BACKLOG.md — Mentorship Center (superseded by this phase)
| Sub-Phase | Name | Status |
|---|---|---|
| 10.1 | Data Model & Connection Settings | ✅ Complete |
| 10.2 | Connection Request Flow | ✅ Complete |
| 10.3 | Connections & Messaging Transformation | ✅ Complete |
| 10.4 | Directory & Profile Integration | ✅ Complete |
| 10.5 | Contacts Migration & Cleanup | ✅ Complete |
| 10.6 | Admin Connection Metrics | ✅ Complete |
Phase 10 introduces the Connections system — a relationship-first communication model that replaces the current open messaging and contacts systems. Instead of anyone being able to message anyone, Champions must first request a Connection, and communication begins only after the request is accepted.
“Help alumni find people 5 steps ahead or 5 steps behind them, and make it easy to start a real conversation.”
The Champion Portal exists to remind alumni they belong to a greater community and to drive meaningful connection. Phase 10 operationalizes this by giving Champions a structured, low-pressure way to reach out — with clear expectations, respectful rate limits, and tools that make the first message feel natural.
| Feature | Description |
|---|---|
| Connection Requests | Structured request flow: select a person, pick a reason, write an initial message |
| Connection Settings | Per-champion preferences: what types of requests to accept, rate limits, pause mode |
| Two-Tab Inbox | “Requests” (incoming + outgoing) and “Connections” (active conversations) replace Messages |
| Directory Filters | Filter by connection type; show “open to” indicators on cards and profiles |
| Contacts → Connections Migration | Mutual contacts become Connections; one-way contacts removed; “Add to Contacts” removed |
| Admin Metrics | Connection stats, super-connector identification, request/acceptance rates |
| Today | After Phase 10 |
|---|---|
| Anyone can message anyone (privacy permitting) | Must have an accepted Connection to message |
| “Add to Contacts” (one-way) with mutual detection | Removed — replaced by Connection system |
| Messages nav item | Connections nav item (with Requests + Connections tabs) |
| No structured reason for reaching out | Connection type required (Say Hi, Career Advice, etc.) |
| No rate limiting on messages | Daily cap for requestors (10/day), daily cap for requestees (10/day default, configurable) |
| Support threads mixed in Messages UI | Support threads remain separate (unchanged) |
The #1 thing we hear from alumni and students: “I want to connect with other alumni who can speak into my life, or that I can speak into.” This is especially true for:
Connections solve these by:
| Not Doing | Why |
|---|---|
| Full CRM / relationship management | Too much bloat; keep it simple |
| Conversational scripts / guided prompts | Will be a Career Center resource (Phase 7), not part of Connections |
| Notes / next-steps tracker on connections | Keep lightweight; backlog if demand emerges |
| Connection expiration / inactivity rules | Connections persist forever; backlog inactive status detection |
| Proactive outbound match suggestions (“You should reach out to X”) | Requestee-side push suggestions add complexity; existing Alumni Like Me recommendations are retained and unchanged |
| Delayed/queued sends when cap is hit | Just show a “try again tomorrow” message; iterate later |
| Group connections / group threads | 1:1 only for now |
| Real-time messaging (WebSocket) | Separate backlog item |
The current “Add to Contacts” feature is being removed. The existing messaging system evolves into the Connections system. The key change: you need an accepted Connection to message someone (except for support threads).
Rationale: Contacts was confusing and low-value. Open messaging lacked structure. Connections combine the relationship aspect of Contacts with the communication channel of Messages, with added intent and consent.
Champions configure which types of connections they’re open to. When requesting a connection, the requestor selects from the types the requestee has enabled. The four types:
| Type | Description |
|---|---|
| Say Hi / Reconnect | Casual — catching up, reconnecting with a fellow Bruin |
| Career Advice / Professional Development | Seeking or offering career guidance |
| Networking / Job Seeking | Professional networking, job leads, industry connections |
| Community Connection | Getting involved locally, finding community. Scoped: only available between members of shared communities |
A Connection Request includes: the requestor, the requestee, the selected connection type, and a freeform initial message. When the request is accepted, a message thread is created with the initial message as the first message. This makes the transition from request to conversation seamless.
When a requestee ignores a request, the requestor is not notified. The request simply remains in the requestor’s “Sent Requests” as pending. Ignored requests are hidden from the requestee’s inbox by default, with an option to view and reconsider them.
When a requestor hits their cap: “You’ve reached your daily limit. Come back tomorrow!” When a requestee’s cap is full: “This person has reached their daily cap of incoming connections, try again tomorrow.”
Either party can disconnect. This archives the thread and requires a new request to reconnect. It does not block the other person. Blocking (separate feature) does break connections.
Alumni, Almost Alumni, and Faculty/Staff can all send and receive connection requests. The only opt-out is per-champion: a setting to not receive requests from Almost Alumni.
During migration, all mutual ChampionContact records become Connection records. One-way contacts are removed. The “Add to Contacts” feature is removed entirely.
The existing staff support thread system (Cp::SupportThreadMailer, support message types) is completely untouched by this phase. Support threads remain accessible from Help/Settings.
| Term | Definition |
|---|---|
| Connection | An accepted relationship between two Champions that enables messaging |
| Connection Request | A structured ask to connect, including type and initial message |
| Requestor | The Champion initiating the connection request |
| Requestee | The Champion receiving the connection request |
| Connection Type | The reason for connecting (Say Hi, Career Advice, Networking, Community) |
| Pause Mode | Temporarily stop receiving new connection requests (with optional end date) |
Terminology we are NOT using: mentor, mentee, mentorship, contact, friend
cp_connection_requests| Column | Type | Notes |
|---|---|---|
id |
bigint | PK |
requestor_id |
bigint | FK → cp_champions, NOT NULL |
requestee_id |
bigint | FK → cp_champions, NOT NULL |
connection_type |
string | One of: say_hi, career_advice, networking, community_connection |
message |
text | Initial message (required, freeform) |
status |
integer | Enum: pending (0), accepted (1), ignored (2), cancelled (3) |
responded_at |
datetime | When accepted/ignored (nullable) |
created_at |
datetime | |
updated_at |
datetime |
Indexes:
[requestor_id, requestee_id] — unique (only one active request between a pair)[requestee_id, status] — for inbox queries[requestor_id, status] — for sent queries[requestee_id, created_at] — for rate limit checksValidations:
community_connection type requires requestor and requestee to share at least one communitycp_connections| Column | Type | Notes |
|---|---|---|
id |
bigint | PK |
champion_a_id |
bigint | FK → cp_champions, NOT NULL (lower ID) |
champion_b_id |
bigint | FK → cp_champions, NOT NULL (higher ID) |
connection_request_id |
bigint | FK → cp_connection_requests (nullable — null for migrated contacts) |
message_thread_id |
bigint | FK → cp_message_threads, NOT NULL |
connection_type |
string | From the original request (nullable for migrated) |
connected_at |
datetime | NOT NULL |
disconnected_at |
datetime | Nullable — set when either party disconnects |
disconnected_by_id |
bigint | FK → cp_champions (nullable) |
created_at |
datetime | |
updated_at |
datetime |
Indexes:
[champion_a_id, champion_b_id] — unique (canonical ordering: lower ID first)[champion_a_id, disconnected_at] — for active connection queries[champion_b_id, disconnected_at] — for active connection queries[message_thread_id] — uniqueDesign note: champion_a_id always holds the lower of the two IDs to ensure uniqueness without needing both orderings. Query helpers will abstract this away.
cp_champions — New Columns| Column | Type | Notes |
|---|---|---|
connection_preferences |
jsonb | Default: { "open_to": ["say_hi", "career_advice", "networking", "community_connection"], "daily_cap": 10, "accept_almost_alumni": true } |
connections_paused_until |
datetime | Nullable — when set and in the future, requests are blocked |
connection_count |
integer | Default: 0 — counter cache for active connections |
pending_requests_count |
integer | Default: 0 — counter cache for pending incoming requests |
Note: unread_messages_count column stays as-is — messaging infra unchanged.
cp_message_threads — New Column| Column | Type | Notes |
|---|---|---|
thread_type |
string | Default: "connection" — values: "connection", "support" |
This distinguishes champion-to-champion connection threads from support threads in queries.
| Table | Disposition |
|---|---|
cp_champion_contacts |
Migrated to cp_connections, then dropped |
| Table | Notes |
|---|---|
cp_message_threads |
Evolves with thread_type column; otherwise unchanged |
cp_messages |
Completely unchanged |
cp_message_thread_participants |
Completely unchanged |
cp_message_reactions |
Completely unchanged |
Goal: Create the data model and settings UI so Champions can configure their connection preferences.
cp_connection_requests tablecp_connections tableconnection_preferences, connections_paused_until, connection_count, pending_requests_count to cp_championsthread_type to cp_message_threads (default: "connection", backfill existing support threads)Cp::ConnectionRequest with validations, enums, scopesCp::Connection with canonical ID ordering, scopes, helper methodsCp::Champion model: connection preference methods, connected_to?, connection_with, active_connectionsconnection_preferences_updatedAdd a new Connections step to the Profile Wizard to ensure every champion configures their connection preferences during onboarding. This also serves as the introduction to the Connections system.
Wizard step changes:
connections step — after profession (work) and before photochampion_role step — reintroduced as a dashboard card in Phase 11: Champion Role Dashboard CardUpdated STEPS constant:
# BEFORE:
STEPS = %w[help_find_you confirm_education location champion_role profession photo bio affinities join_communities].freeze
# AFTER:
STEPS = %w[help_find_you confirm_education location profession connections photo bio affinities join_communities].freeze
Updated progress indicator steps:
# BEFORE (8 visual steps):
# Education, Location, Champion Role, Profession, Photo, Bio, Affinities, Communities
# AFTER (8 visual steps):
# Education, Location, Profession, Connections, Photo, Bio, Affinities, Communities
Icon for Connections step: :link or :user_plus (TBD based on available Heroicons)
Connections wizard step content (_step_connections.html.erb):
Note: Pause Mode is intentionally omitted from the wizard — it’s an advanced setting available in Settings for existing users.
Implementation requirements:
connections step to STEPS constantchampion_role step from STEPS constant (moved to dashboard card in Phase 11)_step_connections.html.erb partialload_step_data case for connections stepprocess_step_update handler: save connection_preferences JSONBThe Connection Preferences section replaces the current “Messaging Privacy” section in Settings. It should include:
Goal: Enable Champions to send, receive, accept, ignore, and cancel connection requests.
Cp::ConnectionRequestService — create, accept, ignore, cancel with all validationconnection_preferences.daily_cap)connection_requested, connection_accepted, connection_ignored, connection_cancelledGoal: Transform the Messages UI into the Connections UI with two tabs, and gate messaging behind accepted connections.
disconnected_at and disconnected_by_id on the Connectionpending_requests_count + unread_messages_countthread_type: "connection" (already defaulted in 10.1 migration)Goal: Surface connection availability in the Directory and on Profile pages.
?| operator query filters champions by selected types AND excludes paused championsopen_to_filter_controller.jsAlumniLikeMeService unchanged — existing scoring factors work naturallycp_settings_path(section: "connections")Goal: Migrate existing data, remove the Contacts system, and update all references.
ChampionContact pairs → create Connection records + message threadsCp::ChampionContact modelCp::ContactsController (or repurpose for connections)Cp::Champion:
has_contact?, contact_of?, mutual_contact?, add_contact!, remove_contact!contact_ids, clear_contact_ids_cache!, recent_contacts, mutual_contactsconnected_to?, connection_with, active_connections, connection_idscontacts_only param)messaging_privacy JSONB column (no longer needed — messaging is gated by connections)contact_preference integer column (legacy, already unused)location_privacy groups: replace “contacts” with “connections”contact_added → connection_established, contact_removed → connection_ended (or add new types and deprecate old)Cp::NotificationJob / notification templates for connection languagecp_champion_contacts table (after data migration is confirmed)Goal: Give staff visibility into Connection program health and identify super-connectors.
The sub-phases must be implemented roughly in order (10.1 → 10.2 → 10.3 → 10.4 → 10.5 → 10.6), but some parallelism is possible:
bin/rake connections:migrate_contactsThe user has very few (if any) existing message threads. Strategy:
thread_type: "support", otherwise untouched| Metric | Target |
|---|---|
| Champions with at least 1 connection type enabled | > 80% of active Champions |
| Connection requests sent in first month | > 50 |
| Request acceptance rate | > 60% |
| “Add to Contacts” references remaining in codebase | 0 |
| Average time from request to acceptance | < 72 hours |
| Champions who send 3+ messages in a connection thread | > 40% of those who connect |
Each sub-phase must include tests for all new:
| Scenario | Sub-Phase |
|---|---|
| Can’t request yourself | 10.2 |
| Can’t request someone you’ve blocked / who blocked you | 10.2 |
| Can’t request if already connected | 10.2 |
| Can’t request if pending request exists | 10.2 |
| Requestor daily cap enforced (10/day) | 10.2 |
| Requestee daily cap enforced (configurable) | 10.2 |
| Can’t request when requestee is paused | 10.2 |
| Can’t request a type the requestee doesn’t accept | 10.2 |
community_connection type requires shared community membership |
10.2 |
| Accept creates Connection + Thread + first message | 10.2/10.3 |
| Ignore is silent (requestor still sees “pending”) | 10.2 |
| Disconnect archives thread, requires re-request | 10.3 |
| Can’t send message without active connection (except support) | 10.3 |
| Directory filters show only open-to-connect Champions | 10.4 |
| Mutual contacts become Connections in migration | 10.5 |
| One-way contacts are removed | 10.5 |
| Privacy settings updated correctly | 10.5 |
| Admin stats are accurate | 10.6 |
When Phase 10 is complete, update:
docs/CHANGELOG.md — Full feature entrydocs/features/champion_portal/FUNCTIONALITY_OVERVIEW.md — Add Connections, remove Contactsdocs/development/MODEL_RELATIONSHIPS.md — New models and associationsdocs/planning/champion-portal/BACKLOG.md — Mark “Mentorship Center” as superseded by Phase 10docs/planning/champion-portal/ai/AI_06_CURRENT_STATE_VS_FUTURE_STATE.md — Update feature statusdocs/planning/champion-portal/ai/AI_03_USER_ROLES_AND_PERMISSIONS.md — Connection permissionsdocs/planning/champion-portal/ai/AI_04_USER_JOURNEYS_AND_FLOWS.md — Connection request flowdocs/planning/champion-portal/ai/AI_05_LANGUAGE_AND_GLOSSARY.md — New terminologyconfig/faq.yml — Add FAQ entries about Connections.github/copilot-instructions.md / AGENTS.md — Update Contact → Connection referencesItems explicitly deferred for future consideration:
| Item | Reason | Potential Phase |
|---|---|---|
| Conversational scripts / guides | Will live in Career Center as downloadable PDF resources | Phase 7 (Career Center) |
| Notes / next-steps tracker per connection | Avoid CRM bloat; revisit if demand emerges | Backlog |
| Connection expiration / inactivity detection | Connections persist forever for now | Backlog |
| Proactive outbound match suggestions (“You should reach out to X”) | Requestee-side push suggestions add complexity; existing Alumni Like Me recommendations are retained | Backlog |
| Delayed/queued sends when cap is hit | Simple “try again tomorrow” for now | Backlog |
| Connection-based recommendation algorithm changes | Current AlumniLikeMeService factors already work well |
Backlog |
| Group connections / multi-party threads | 1:1 only for now | Backlog |