Skip to content

ADR-001 - Centralized BU-Level Membership

ADR-001: Centralized BU-Level Membership Model

Section titled “ADR-001: Centralized BU-Level Membership Model”

Date: October 27, 2025
Status: ✅ Accepted
Deciders: Engineering Team, Product Team
Related: MEMBERSHIP_ARCHITECTURE_ANALYSIS.md


CloudAlt operates 3 Business Units (BUs) with 6 total divisions and ~30+ branded apps:

BU #1: Hospitality (stays/)

  • Division 1: Stay Overnight (apps: Pink Guest, Orange Guest, Green Host, Purple Guest)
  • Division 2: Roommate Works (app: Roommate Works)
  • Division 3: Stay Match (app: Stay Match)

BU #2: Discovery (discovery/)

  • Division 4: AltFinder (apps: AltFinder, Pink Finder, Orange Finder)

BU #3: Services (services/)

  • Division 5: Pride City (app: Pride City)
  • Division 6: Bonjour Locker (app: Bonjour Locker)

We need to decide how to structure user membership across divisions:

Option A: Centralized BU-Level Membership

  • One membership table per Business Unit
  • Single profile shared across all divisions within a BU
  • Example: Kate joins Pink Guest → Gets membership “STAY-000001” → Can access Roommate Works and Stay Match with same account

Option B: Decentralized Division-Level Membership

  • Separate membership table per division
  • Each division has independent user accounts
  • Example: Kate joins Pink Guest → Creates account → Opens Roommate Works → Must create new account
  1. Downloads Pink Guest app (women-only travel, Stay Overnight division)
  2. Creates account → Where does her profile live?
  3. Later discovers Roommate Works → Should she create a new account or sign in?
  4. Wants to list on Stay Match → New account or existing?

We will use centralized BU-level membership (Option A).

Each Business Unit will have one unified membership table that serves all divisions within that BU:

  • stays/members/MemberProfile → Shared by Stay Overnight, Roommate Works, Stay Match
  • discovery/members/MemberProfile → Used by AltFinder only (single division)
  • services/members/MemberProfile → Shared by Pride City, Bonjour Locker
stays/members/models.py
class MemberProfile(models.Model):
"""
Centralized membership for BU #1: Hospitality
Shared across: Stay Overnight, Roommate Works, Stay Match
"""
# Universal identity
supabase_user_id = models.UUIDField(unique=True, db_index=True)
membership_id = models.CharField(max_length=20, unique=True) # "STAY-000001"
email = models.EmailField(unique=True)
# Core profile (shared across all divisions)
username = models.CharField(max_length=50, unique=True)
real_name = models.CharField(max_length=100)
age_range = models.CharField(max_length=10)
gender = models.CharField(max_length=20)
photo_url = models.URLField()
bio = models.TextField(blank=True)
# Division enrollment tracking
joined_stay_overnight = models.BooleanField(default=False)
joined_stay_overnight_date = models.DateTimeField(null=True)
joined_roommate = models.BooleanField(default=False)
joined_roommate_date = models.DateTimeField(null=True)
joined_stay_match = models.BooleanField(default=False)
joined_stay_match_date = models.DateTimeField(null=True)
# Initial join tracking (for analytics and marketing)
initial_division = models.CharField(max_length=50) # "stay_overnight", "roommate", "stay_match"
initial_app = models.CharField(max_length=50) # "pink_guest", "orange_guest", "roommate_app", etc.
# App usage tracking
apps_used = models.JSONField(default=list) # ["pink_guest", "roommate_app", ...]
# Trust & Safety (shared across all divisions)
verified_email = models.BooleanField(default=False)
verified_phone = models.BooleanField(default=False)
government_id_verified = models.BooleanField(default=False)
# Division role flags
is_host = models.BooleanField(default=False) # Stay Overnight host
is_traveler = models.BooleanField(default=False) # Stay Overnight traveler
is_roommate_seeker = models.BooleanField(default=False) # Roommate Works
is_property_owner = models.BooleanField(default=False) # Stay Match

Division-specific data lives in OneToOne relationships:

stays/stay_overnight/models.py
class HostProfile(models.Model):
member = models.OneToOneField('members.MemberProfile', on_delete=models.CASCADE)
hosting_since = models.DateField()
max_guests = models.IntegerField()
# ... host-specific fields
class TravelerProfile(models.Model):
member = models.OneToOneField('members.MemberProfile', on_delete=models.CASCADE)
travel_style = models.CharField()
# ... traveler-specific fields
# stays/roommate/models.py
class RoommateProfile(models.Model):
member = models.OneToOneField('members.MemberProfile', on_delete=models.CASCADE)
budget_min = models.DecimalField()
budget_max = models.DecimalField()
# ... roommate-specific fields

✅ Pros of Centralized BU-Level Membership

Section titled “✅ Pros of Centralized BU-Level Membership”

1. Superior User Experience

  • Single Sign-On within BU: Kate signs up on Pink Guest → Automatically logged into Roommate Works and Stay Match
  • No duplicate accounts: One account per BU, not one per app
  • Profile portability: Update photo once → shows everywhere
  • Reputation carries over: Build trust in Stay Overnight → Recognized in Roommate Works

2. Cross-Brand Marketing Opportunities

# Show contextual offers based on division membership
if member.joined_stay_overnight and not member.joined_roommate:
show_banner("Looking for a permanent roommate? Try Roommate Works!")
if member.is_host and not member.joined_stay_match:
show_banner("Want to earn money hosting? List on Stay Match!")

3. Business Intelligence & Analytics

-- How many Stay Overnight users also use Roommate?
SELECT COUNT(*) FROM member_profiles
WHERE joined_stay_overnight = TRUE AND joined_roommate = TRUE;
-- Which branded app drives most cross-division engagement?
SELECT initial_app,
COUNT(*) as total_users,
AVG(CASE WHEN joined_roommate THEN 1 ELSE 0 END) as roommate_conversion_rate
FROM member_profiles
GROUP BY initial_app;
-- Lifetime value across divisions
SELECT membership_id,
COUNT(divisions_joined) as divisions_count,
SUM(total_bookings) as total_engagement
FROM member_profiles
WHERE is_cross_division_member = TRUE;

4. Simpler Data Management

  • GDPR compliance: One user record per BU to delete (not 3+ separate records)
  • Unified support: Support team sees complete user history across divisions
  • Account recovery: Reset password once, not per division
  • Verification once: Email/phone verification carries across divisions

5. Natural Brand Architecture

  • Users understand: “I have a CloudAlt Hospitality account”
  • Not confusing: “I have separate accounts for Pink Guest, Roommate Works, and Stay Match”
  • Builds brand loyalty and ecosystem lock-in

6. Enables Premium/Subscription Models

# Future: CloudAlt Hospitality Premium
class MemberProfile(models.Model):
is_premium = models.BooleanField(default=False)
premium_since = models.DateField(null=True)
# Premium applies to ALL divisions in BU

1. BU Coupling

  • Risk: If we sell/spin off Roommate Works, need data migration
  • Mitigation: Use separate Supabase databases per division (optional), maintain clear division boundaries in code

2. Schema Changes Affect Multiple Apps

  • Risk: Adding field to MemberProfile affects all divisions
  • Mitigation: Use JSONField for app-specific metadata, keep core fields minimal, version API schemas

3. Scaling Complexity

  • Risk: 1M Stay Overnight users → Roommate queries slower
  • Mitigation: Proper indexing, database read replicas, query optimization, consider horizontal sharding by division if needed

Database Schema Updates:

  • Add division tracking fields to MemberProfile
  • Add initial join tracking (division, app)
  • Add apps_used JSON field
  • Update indexes for new query patterns

API Changes:

  • Update onboarding endpoint to set division flags
  • Create “join another division” endpoint
  • Add cross-division membership check to all endpoints
  • Return division membership status in profile responses

Authentication Flow:

User opens Pink Guest
Supabase Auth (email/password)
Backend checks: member_profiles.supabase_user_id
If exists: Return existing profile (may already be in other divisions)
If new: Create MemberProfile with initial_division="stay_overnight", initial_app="pink_guest"
  • Create division-specific profile models (RoommateProfile, StayMatchProfile)
  • Build “Discover Other CloudAlt Services” in-app promotions
  • Analytics dashboard for cross-division conversion rates
  • Unified notification system across divisions
  • Consider optional cross-BU linking (if we want “CloudAlt Universal Premium”)
  • Unified wallet/credits system across all CloudAlt services
  • Cross-BU recommendation engine

Docker Compose:
├─ postgres_stays (port 5432)
├─ postgres_discovery (port 5433)
└─ postgres_services (port 5434)

Recommended: Single Database Per BU

stays_hospitality_db (Supabase)
├─ member_profiles (shared table for all 3 divisions)
├─ host_profiles (Stay Overnight)
├─ traveler_profiles (Stay Overnight)
├─ overnight_offerings (Stay Overnight)
├─ roommate_profiles (Roommate Works)
├─ roommate_listings (Roommate Works)
├─ stay_match_properties (Stay Match)
└─ ...
discovery_db (Supabase)
└─ member_profiles (AltFinder only)
services_db (Supabase)
├─ member_profiles (shared for Pride City + Bonjour Locker)
└─ ...

Alternative: Separate Databases with Shared Auth (if spin-off likely)

Each division gets own database, all link via supabase_user_id
Trade-off: Better independence, more complex joins

# Kate signs up on Pink Guest
POST /api/v1/onboarding/
{
"username": "kate_traveler",
"real_name": "Kate Smith",
"age_range": "25-34",
"gender": "female",
"photo_url": "https://...",
"division": "stay_overnight",
"app": "pink_guest"
}
Response:
{
"success": true,
"membership_id": "STAY-000001",
"message": "Welcome to CloudAlt Hospitality!",
"divisions_joined": ["stay_overnight"],
"can_join": ["roommate", "stay_match"]
}
# Kate opens Roommate Works (already has STAY-000001)
POST /api/v1/join-division/
{
"division": "roommate",
"app": "roommate_app"
}
Response:
{
"success": true,
"membership_id": "STAY-000001", # Same ID!
"message": "Welcome to Roommate Works, Kate!",
"divisions_joined": ["stay_overnight", "roommate"],
"profile_prefilled": true,
"verification_status": {
"email": true, # Already verified in Stay Overnight
"phone": true
}
}
GET /api/v1/members/me/
Response:
{
"membership_id": "STAY-000001",
"username": "kate_traveler",
"divisions_joined": ["stay_overnight", "roommate"],
"initial_division": "stay_overnight",
"initial_app": "pink_guest",
"apps_used": ["pink_guest", "roommate_app"],
"is_cross_division_member": true,
"verified_email": true,
"verified_phone": true,
"is_host": false,
"is_traveler": true,
"is_roommate_seeker": true
}

Alternative 1: Fully Decentralized (Division-Level)

Section titled “Alternative 1: Fully Decentralized (Division-Level)”

Rejected because:

  • ❌ Terrible UX: User must create separate accounts for each division
  • ❌ Duplicate data: kate@example.com exists 3 times
  • ❌ No cross-selling: Can’t market Roommate to Stay Overnight users
  • ❌ Fragmented trust: Verification doesn’t carry over
  • ❌ Complex GDPR: Must find and delete 3+ separate records

Alternative 2: Fully Centralized (Global CloudAlt Account)

Section titled “Alternative 2: Fully Centralized (Global CloudAlt Account)”

Rejected because:

  • ❌ Too much coupling: Can’t spin off any BU
  • ❌ Schema conflicts: Hospitality fields mixed with Discovery fields
  • ❌ Single point of failure: One database outage affects all BUs
  • ❌ Complex permissions: BU-specific access controls harder

Considered but deferred:

class UniversalCloudAltMembership(models.Model):
supabase_user_id = models.UUIDField(unique=True)
hospitality_member = models.ForeignKey('stays.MemberProfile')
discovery_member = models.ForeignKey('discovery.MemberProfile')
services_member = models.ForeignKey('services.MemberProfile')

Why deferred:

  • Adds complexity without immediate value
  • Can be added later if we want cross-BU premium features
  • YAGNI principle: Don’t build until needed

User Experience:

  • Cross-division signup rate (target: >30% of users join 2+ divisions)
  • Account creation friction (target: <2 minutes for division 2+)
  • Profile completion rate across divisions

Business Value:

  • Lifetime value of cross-division users vs single-division users
  • Cross-division conversion rate from in-app promotions
  • Support ticket reduction (fewer duplicate accounts)

Technical:

  • Query performance on member_profiles table (<100ms p95)
  • GDPR deletion completion time (<5 minutes)
  • Database size growth rate


DateVersionChangesAuthor
2025-10-271.0Initial decisionEngineering Team