Skip to content

Two-Layer Social Model

Objectuve's social features consist of two distinct layers that can operate independently but often interact.

Layer 1: Allies (Foundational)

The ally layer (v2.0) is the base social structure. An ally is a user you have explicitly connected with through a request-and-accept flow.

Model: UserAlly{user_id, ally_id, status: pending|accepted|blocked}

State machine:

  • pending: User A sent a request; User B hasn't responded
  • accepted: Both users see each other in their ally list
  • blocked: User A has blocked User B (unidirectional; one-way relationship)

What you can do as an ally:

  • See ally activity in your activity feed
  • Share goals and progress for accountability (via other features)
  • Participate in ally-exclusive features (as they ship)

Operations:

  • Send request → creates pending row
  • Accept request → transitions to accepted
  • Decline request → deletes row
  • Remove ally → deletes row (works bidirectionally)
  • Block ally → creates/updates row to blocked status

Scope: All authenticated users; no tier lock; core free feature.

Layer 2: Accountability Partners (Upgrade)

The accountability-partner layer (v1.0+) is an optional upgrade within an ally relationship. An accountability partner is an ally you've explicitly upgraded to "accounting to each other for goals."

Model: UserAlly{..., accountability_partner: boolean} — a flag on the ally row

What you get as accountability partners:

  • 25 XP bonus per user per check-in (when both partners check in the same day)
  • Designated "accountability partner" label in UI
  • First-class visibility in goal-tracking and streaks

Operations:

  • Upgrade to partner → SendPartnerRequest mutation sets accountability_partner: true on the ally row
  • Decline partnership → DeclinePartnerRequest clears accountability_partner_requested/accountability_partner_requested_by_id; the UserAlly row is not deleted
  • End partnership → EndPartnership clears accountability_partner, resets mutual_streak_count, and stamps partner_ended_at; the UserAlly row is not deleted
  • Send nudge → SendPartnerNudge notification to partner

Scope: Same as allies (all users); this is an enhancement, not a gating mechanism.

Interaction Model

Ally Lifecycle          Partnership Upgrade
─────────────────      ──────────────────

SendAllyRequest

User B gets
ally_request notif

AcceptAllyRequest

UserAlly{
  status: 'accepted'
  [no partner flag yet]
}     ↑

      └─── SendPartnerRequest
           (optional upgrade)

           UserAlly{
             status: 'accepted'
             accountability_partner: true
           }

Both layers are independent:

  • You can have allies without partnerships (Layer 1 only)
  • You cannot have a partnership without being allies first (Layer 2 requires Layer 1)

Naming Conventions

For Code Contributors

  • Model columns/fields: Use ally / allies to refer to Layer 1. Use accountability_partner / partner to refer to Layer 2.

    • ✅ Good: UserAlly.where(status: 'accepted')
    • ✅ Good: user.allies_with_accepted_status.find { |a| a.accountability_partner }
    • ❌ Avoid: user.partners (ambiguous; use user.accountability_partners)
  • GraphQL fields: Use userAllies query for the Layer 1 relationship data. Use partner_status enum on UserAllyType to expose upgrade state.

    • ✅ Good: userAllies { id status accountabilityPartner }
    • ✅ Good: userAllies { id partnerStatus }
    • ❌ Avoid: userPartners (use userAllies + filter by accountabilityPartner: true)
  • Mutations: Use sendAllyRequest / acceptAllyRequest for Layer 1 ops. Use sendPartnerRequest / acceptPartnerRequest for Layer 2 ops.

    • ✅ Good: mutation sendAllyRequest($targetId: ID!)
    • ✅ Good: mutation sendPartnerRequest($targetId: ID!)
    • ❌ Avoid: mutation addPartner (use sendPartnerRequest)
  • Notifications: kind: :ally_request and kind: :ally_accepted are Layer 1. kind: :partner_request, kind: :partner_accepted, etc. are Layer 2.

  • Telemetry: Event names follow layer:

    • Layer 1: ally_request_sent, ally_request_accepted, ally_request_declined, ally_search_performed
    • Layer 2: partner_request_sent, partner_request_accepted, etc. (if added)

When to Use Each Layer

Build ally features when:

  • You want to connect users for accountability without XP rewards
  • You want flexible relationship management (accept/decline/remove/block)
  • You're building a feed, activity, or visibility feature for connected users

Build on the accountability-partner layer when:

  • You want to reward mutual participation (XP bonus)
  • You're designing a feature specific to formal partnerships
  • You need the upgrade/downgrade contract (partner → not-partner)

Example: The activity feed uses the ally layer (show me what my allies are doing), not the partner layer (it doesn't require partnership, only acceptance).

Future Extensions

Likely future work:

  • Mutual allies / "people you may know" — suggest users with common allies (built on ally layer)
  • Partner-only features — goal-sharing details, streaks, milestones (built on partner layer)
  • Friend groups / squads — multi-user accountability teams (new, orthogonal layer)
  • Public profiles — show ally count, activity (built on existing layers + public/private separation)

All extensions should respect the two-layer separation and maintain the independence of Layer 1 from Layer 2.


Last updated: 2026-08-24 — corrected the decline/end partnership operations: neither deletes the UserAlly row (v4.45 Phase 4, OBJ-2882)

Loading…