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 choiceKey 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 choiceWhy 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.
| State | You See Them | They 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 windowAlternative: 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 β
// 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 β
// 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 SpeakeasyUI 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:
- Header: Total frens count, filter/search
- Lit Now: Mutual frens with active lanterns (prioritized)
- Broadcasting To: One-way saves (you broadcast to them)
- Receiving From: People who broadcast to you (mutual section)
- 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 β
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
Consent at Every Level
- Save = "I consent to broadcast to you"
- Venue-by-venue opt-in
- Can remove venues or entire connection anytime
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
Ephemeral Chat
- Messages only at venues
- No persistent inbox
- Natural boundaries
β οΈ Potential Issues β
User Confusion
- "Why can't I see my fren?" β They haven't saved you back
- Solution: Clear onboarding and UI messaging
Asymmetric Expectations
- One person thinks it's mutual, other hasn't saved them
- Solution: Show broadcasting status clearly
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 β
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")
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
Venue expansion prompts timing?
- Current: Immediate when both at new venue
- Alternative: After 2nd or 3rd co-presence?
- Proposal: Immediate with explanation of intentionality
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
Related Documentation β
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:
// 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:
- Build component scaffolding with mock data
- Create Storybook stories for all states
- User test with pilot group
- Iterate based on feedback
- Implement Firebase backend