Skip to content

Frens System β€” Privacy-First Reconnection ​

Date: January 9, 2026 Status: 🚧 Design Phase


Overview ​

The Frens system enables users to save connections from in-person meetings and reconnect later while maintaining Lantern's core privacy principles.

Core Philosophy ​

Saving someone = Broadcasting YOUR lantern to THEM

  • No tracking other people's locations
  • No stalking possible
  • Venue-bound, context-preserved connections
  • Consent-based visibility at every step

How It Works ​

The Flow ​

1. Meet at Coffee House β†’ Wave accepted β†’ Chat
2. Option to "Save Connection" (optional for both)
3. If you save them:
   β†’ They can now see YOUR lantern at Coffee House
   β†’ You cannot see theirs (unless they save you back)
4. If they save you back:
   β†’ πŸŽ‰ Mutual connection notification
   β†’ Both can see each other at shared venues
5. Meet again at different venue:
   β†’ Option to add that venue to broadcast list
   β†’ Each venue is an intentional choice

Key Principles ​

1. Venue-Bound Broadcasting ​

You don't broadcast everywhereβ€”only at specific venues.

You save "Sapphire Crystal" at Coffee House
β†’ They see your lantern ONLY at Coffee House
β†’ Not at The Speakeasy, not at The Bar, nowhere else

Later, you both meet at The Speakeasy:
β†’ Option to "Also broadcast at The Speakeasy?"
β†’ Each venue is a separate, intentional choice

Why this matters:

  • Prevents routine tracking (can't see someone's weekly patterns)
  • Keeps connections context-specific ("we're Coffee House people")
  • You control exactly which slices of your social life are visible

2. Asymmetric Visibility ​

Visibility is reciprocal to the save action, not symmetric.

StateYou See ThemThey See You
Neither saved❌❌
You saved themβŒβœ… (at broadcast venues)
They saved youβœ… (at broadcast venues)❌
Mutual saveβœ… (at shared venues)βœ… (at shared venues)

Why asymmetric?

  • You're broadcasting TO them, not tracking them
  • Like saying "if you want to find me again, here's my signal"
  • No surveillance possible

3. No Persistent Chat ​

Chat is only available when both users are at venues with lit lanterns.

βœ… Chat available when:
   - Both at same venue with lanterns lit
   - Both at any venue (if mutual frens at that venue)
   - 2-hour window after meeting (logistics only)

❌ Chat NOT available:
   - When either person isn't at a venue
   - Outside the 2-hour post-meetup window

Alternative: Beacon Invites

  • "I'll be at Coffee House Thursday 7pm"
  • One-way notification, no back-and-forth
  • Shows intent without creating inbox burden

Data Model ​

Connection Document ​

javascript
// Firestore: connections/{connectionId}
{
  connectionId: "abc123",
  user1Id: "user123",
  user2Id: "user456",
  
  // Where each user broadcasts their lantern
  user1Broadcasts: [
    {
      venueId: "coffee-house",
      venueName: "Coffee House",
      addedDate: "2026-01-09T10:30:00Z"
    },
    {
      venueId: "the-speakeasy",
      venueName: "The Speakeasy",
      addedDate: "2026-01-15T19:00:00Z"
    }
  ],
  
  user2Broadcasts: [
    {
      venueId: "coffee-house",
      venueName: "Coffee House",
      addedDate: "2026-01-09T11:00:00Z"
    }
  ],
  
  // Connection metadata
  metAt: "Coffee House",
  metDate: "2026-01-09T10:30:00Z",
  
  // User info (cached from profile)
  user1LanternName: "Amber Beacon",
  user1Interests: ["Jazz", "Coffee", "Art"],
  user2LanternName: "Sapphire Crystal",
  user2Interests: ["Coffee", "Books", "Late Night"],
  
  // Computed fields (set via cloud function)
  mutualVenues: ["coffee-house"], // Venues both broadcast at
  isMutual: true,
  
  // Timestamps
  createdAt: "2026-01-09T10:30:00Z",
  updatedAt: "2026-01-15T19:00:00Z"
}

Querying Frens with Lit Lanterns ​

javascript
// Get my frens who are currently lit at venues I broadcast to them
const myConnections = await db.collection('connections')
  .where('user1Id', '==', currentUserId)
  .where('user1Broadcasts', 'array-contains-any', myCurrentVenues)
  .get();

// Then cross-reference with active lanterns
const activeFrenIds = myConnections.docs.map(doc => doc.data().user2Id);
const activeLanterns = await db.collection('lanterns')
  .where('userId', 'in', activeFrenIds)
  .where('isLit', '==', true)
  .where('venueId', 'in', broadcastVenueIds) // Only venues they broadcast to me
  .get();

User Flows ​

Flow 1: Save Someone (One-Way) ​

User A and User B meet at Coffee House
↓
User A accepts wave from User B
↓
After chatting:
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Save Sapphire Crystal?          β”‚
β”‚ ────────────────────────────────│
β”‚ They'll be able to see when you β”‚
β”‚ light your lantern at Coffee    β”‚
β”‚ House                            β”‚
β”‚                                  β”‚
β”‚ [Maybe Later] [Save Fren]        β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
↓
User A taps [Save Fren]
↓
Connection created with:
- user1Id: User A
- user2Id: User B
- user1Broadcasts: [Coffee House]
- user2Broadcasts: [] (empty)
↓
User B sees nothing (no notification)
↓
Next time User A lights lantern at Coffee House:
User B sees "Amber Beacon is at Coffee House"

Result:

  • User A is broadcasting to User B
  • User B can see User A's lantern at Coffee House
  • User A cannot see User B's lantern anywhere
  • No notification sent to User B

Flow 2: Mutual Save ​

Same as Flow 1, but User B also saves User A
↓
When User B saves User A:
System detects mutual save
↓
Both users get notification:
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ πŸŽ‰ Mutual Connection!            β”‚
β”‚ ────────────────────────────────│
β”‚ Sapphire Crystal saved you too!  β”‚
β”‚                                  β”‚
β”‚ You can now see each other when β”‚
β”‚ lanterns are lit at Coffee House β”‚
β”‚                                  β”‚
β”‚ [View Frens]                     β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Result:

  • Both broadcasting at Coffee House
  • Both can see each other's lanterns there
  • Priority wave option
  • Still can't see each other at other venues

Flow 3: Expanding to New Venue ​

User A and User B (mutual frens) both happen to be at The Speakeasy
↓
Both see each other in normal lantern feed (not as frens)
↓
System shows prompt:
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Sapphire Crystal is here!       β”‚
│─────────────────────────────────│
β”‚ You're mutual frens from        β”‚
β”‚ Coffee House                     β”‚
β”‚                                  β”‚
β”‚ Also broadcast at The Speakeasy?β”‚
β”‚ [No thanks] [Add Venue]          β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
↓
If User A taps [Add Venue]:
β†’ User B can now see User A at The Speakeasy
β†’ User A still can't see User B there (until they also add venue)
↓
If User B also adds venue:
β†’ Both can now see each other at Coffee House AND The Speakeasy

UI Components ​

1. SaveConnectionPrompt ​

Purpose: Modal shown after chatting to save a connection

Triggers:

  • After accepting wave and chatting
  • Manual "Save Connection" button in chat
  • Post-meetup (2-hour window)

States:

  • Default: Explain venue-bound broadcasting
  • Success: Confirmation message
  • Already saved: "You've already saved this person"

Location: src/components/SaveConnectionPrompt.jsx


2. FrensList ​

Purpose: Main frens screen showing all connections

Sections:

  1. Header: Total frens count, filter/search
  2. Lit Now: Mutual frens with active lanterns (prioritized)
  3. Broadcasting To: One-way saves (you broadcast to them)
  4. Receiving From: People who broadcast to you (mutual section)
  5. Inactive: Mutual frens not currently lit

Features:

  • Real-time updates when frens light lanterns
  • Venue context for each fren
  • Quick wave action
  • Badge showing lantern status (lit/not lit)

Location: src/screens/frens/FrensList.jsx


3. FrenProfile ​

Purpose: Detailed view of a single fren

Shows:

  • Lantern name and interests
  • Where you met (original venue)
  • Shared venues (if mutual)
  • Current lantern status
  • Broadcast venues management

Actions:

  • Wave (if currently lit)
  • Manage venues
  • Send beacon invite
  • Remove connection

Location: src/screens/frens/FrenProfile.jsx


4. ManageVenuesModal ​

Purpose: Choose which venues you broadcast to for this fren

Features:

  • List of all venues where you've both been
  • Checkboxes to enable/disable broadcasting
  • Add new venues (only after both present there)
  • Explanation of what broadcasting means

Location: src/components/ManageVenuesModal.jsx


Privacy & Safety Features ​

βœ… What's Protected ​

  1. No Location Tracking

    • Only see lanterns at venues you both broadcast at
    • No GPS coordinates ever shared
    • Can't build profile of someone's routines
  2. Consent at Every Level

    • Save = "I consent to broadcast to you"
    • Venue-by-venue opt-in
    • Can remove venues or entire connection anytime
  3. No Stalking

    • Can't see where someone is unless they broadcast there
    • Can't see when they were last active (except at shared venues)
    • No "last seen" timestamps outside context
  4. Ephemeral Chat

    • Messages only at venues
    • No persistent inbox
    • Natural boundaries

⚠️ Potential Issues ​

  1. User Confusion

    • "Why can't I see my fren?" β†’ They haven't saved you back
    • Solution: Clear onboarding and UI messaging
  2. Asymmetric Expectations

    • One person thinks it's mutual, other hasn't saved them
    • Solution: Show broadcasting status clearly
  3. Venue Spam

    • Users adding every venue "just in case"
    • Solution: Prompt only when both actually present
    • Encourage intentional venue selection

Implementation Phases ​

Phase 1: Core UI (Frontend Only) βœ… This PR ​

  • SaveConnectionPrompt component
  • FrensList screen
  • FrenProfile screen
  • ManageVenuesModal component
  • Storybook stories
  • Mock data integration
  • Routing setup

Phase 2: Firebase Integration ​

  • Firestore connection schema
  • Real-time lantern queries
  • Cloud Functions for mutual detection
  • Notification system

Phase 3: Beacon Invites ​

  • Invite creation UI
  • Notification delivery
  • Calendar integration

Phase 4: Analytics & Refinement ​

  • Track save rates
  • Monitor venue expansion patterns
  • A/B test messaging
  • User feedback integration

Open Questions ​

  1. Should we limit number of frens?

    • Pro: Keeps connections meaningful
    • Con: Arbitrary limit feels restrictive
    • Proposal: Soft limit with warning ("You have 50+ frens. Consider quality over quantity")
  2. What if someone never saves you back?

    • Current: You can still broadcast to them forever
    • Alternative: Auto-expire after 30 days of no mutual save?
    • Proposal: Keep forever but show "Not mutual" status clearly
  3. Venue expansion prompts timing?

    • Current: Immediate when both at new venue
    • Alternative: After 2nd or 3rd co-presence?
    • Proposal: Immediate with explanation of intentionality
  4. Can you remove individual venues?

    • Current: Yes, manage anytime
    • Alternative: Can only remove if not mutually broadcasting?
    • Proposal: Full control, but show warning if removing mutual venue

Success Metrics ​

User Engagement ​

  • % of wave acceptances that lead to saves
  • Average number of frens per user
  • Average number of shared venues per mutual connection
  • Reconnection rate (met again after 7+ days)

Privacy Health ​

  • % of users with >10 broadcast venues (spam indicator)
  • Venue expansion rate (how fast people add venues)
  • Connection removal rate

Feature Adoption ​

  • % of users with at least 1 fren
  • % of users with at least 1 mutual fren
  • Frens feature DAU/MAU ratio


Technical Notes ​

Real-Time Updates ​

Frens list must update in real-time when:

  • A fren lights their lantern at a broadcast venue
  • A fren extinguishes their lantern
  • A mutual save occurs
  • A venue is added/removed

Use Firestore listeners for efficiency:

javascript
// Listen to connections where I broadcast
db.collection('connections')
  .where('user1Id', '==', currentUserId)
  .onSnapshot(snapshot => {
    // Update frens list
  });

// Listen to active lanterns from my frens
db.collection('lanterns')
  .where('userId', 'in', myFrenIds)
  .where('isLit', '==', true)
  .onSnapshot(snapshot => {
    // Update lit status in real-time
  });

Performance Considerations ​

  • Cache lantern names and interests in connection doc (avoid extra reads)
  • Use compound queries for efficient filtering
  • Limit real-time listeners to active frens only
  • Paginate frens list if >50 connections

Next Steps:

  1. Build component scaffolding with mock data
  2. Create Storybook stories for all states
  3. User test with pilot group
  4. Iterate based on feedback
  5. Implement Firebase backend

Built with VitePress