Status: Architecture Issue Date: 2026-03-06 Impact: Medium - Causes developer confusion, complicates monitoring, blocks clean protocol evolution
The videocall-rs protocol uses two overlapping identifiers for participants, causing confusion throughout the codebase:
session_id(uint64) - Server-assigned GUID, the true unique identifieremail(string) - Misnamed field that actually contains user-provided display name
This dual-identity system creates maintenance burden, developer confusion, and complicates features like monitoring (vcprobe) and user identity management.
Protobuf Definition (packet_wrapper.proto):
message PacketWrapper {
PacketType packet_type = 1;
string email = 2; // ❌ MISLEADING NAME
bytes data = 3;
uint64 session_id = 4; // ✅ True unique identifier
}What email field actually contains:
- Non-OAuth mode: Whatever user types in "Username" field
- Examples:
"JayAtHome","Alice123","bob_work"
- Examples:
- OAuth mode: Sometimes actual email (
alice@company.com), sometimes display name - Never validated as email format
| Identifier | Type | Assigned By | Scope | Content |
|---|---|---|---|---|
session_id |
uint64 |
Server on connect | Connection lifetime | Unique numeric GUID (e.g., 12345) |
email |
string |
Client/User | User choice | Display name OR email (e.g., "JayAtHome" or "jay@company.com") |
Home Page (yew-ui/src/pages/home.rs:156-166):
<label for="username">{"Username"}</label> // UI says "Username"
<input
id="username"
placeholder="Enter your name" // Placeholder says "name"
ref={username_ref}
// ...
/>
// But stored as:
let username = username_ref.cast::<HtmlInputElement>().unwrap().value();
save_username_to_storage(&username);
username_ctx.set(Some(username)); // Called "username" in codeWhen creating VideoCallClient (yew-ui/src/components/attendants.rs:288):
let opts = VideoCallClientOptions {
userid: email.clone(), // Variable named "email" but contains username!
// ...
};Inconsistency:
- UI labels: "Username", "Enter your name"
- Code variables:
email,username,userid - Actual content: User-provided display name (not validated as email)
VideoCallClientOptions (src/client/video_call_client.rs:65):
pub struct VideoCallClientOptions {
pub userid: String, // Called "userid" but used as display name
// ...
}When creating packets:
// connection.rs:250
PacketWrapper {
email: userid.to_owned(), // userid → email field
// ...
}Health Reporter (src/health_reporter.rs:107-111):
pub fn new(session_id: String, reporting_peer: String, health_interval_ms: u64) -> Self {
Self {
session_id, // Numeric GUID
reporting_peer, // Display name (from userid)
// ...
}
}
// Called from video_call_client.rs:199-202:
let mut reporter = HealthReporter::new(
session_id,
options.userid.clone(), // Display name goes into reporting_peer
// ...
);PacketWrapper stamps ALL packets:
message PacketWrapper {
string email = 2; // Contains display_name
uint64 session_id = 4; // Added by server
}Server adds session_id (actix-api/src/actors/chat_server.rs:228-230):
if let Ok(mut packet_wrapper) = PacketWrapper::parse_from_bytes(&msg.data) {
if packet_wrapper.session_id == 0 {
packet_wrapper.session_id = session; // Server stamps with session GUID
}
}HEALTH packets use BOTH (protobuf/types/health_packet.proto):
message HealthPacket {
string session_id = 1; // Session GUID (as string)
string reporting_peer = 3; // Display name
map<string, PeerStats> peer_stats = 7; // Keys are session_id!
}Session assignment on connect (src/session_manager.rs:123-132):
/// Build SESSION_ASSIGNED packet: server sends immediately after connect.
pub fn build_session_assigned_packet(session_id: u64) -> Vec<u8> {
let wrapper = PacketWrapper {
packet_type: PacketType::SESSION_ASSIGNED.into(),
email: SYSTEM_USER_EMAIL.to_string(), // System sender
session_id, // The GUID being assigned
..Default::default()
};
wrapper.write_to_bytes().unwrap_or_default()
}Flow:
- Client connects
- Server generates unique
session_id(uint64) - Server sends
SESSION_ASSIGNEDpacket - Client stores
session_id, uses it for packet filtering - All subsequent packets stamped with
session_idby server
Example from actual codebase:
// yew-ui/src/pages/meeting.rs:187-193
// Use the user's email as userid so the server can match
// push-notification `target_email` to this observer client.
let email_for_userid = (*current_user_email).clone()
.unwrap_or_else(|| display_name.clone());
let opts = VideoCallClientOptions {
userid: email_for_userid, // Is this email or display_name?
// ...
};Questions developers face:
- Is
useridan email address or display name? - Should I validate it as email format?
- Can two users have the same
userid? - Is
session_idoremailthe unique identifier?
vcprobe needs to display participant names but faces:
- MEDIA packets have:
email(display name) +session_id(GUID) - HEALTH
peer_statskeys are:session_idas strings - Must build mapping:
session_id→ display name
Code (vcprobe/src/state.rs:99-101, 143-147):
/// Maps session IDs (numbers as strings) to email addresses
/// Built from MEDIA packets to resolve HEALTH packet peer_stats keys
session_to_email: HashMap<String, String>,
// Track session_id → email mapping for HEALTH packet resolution
if pkt.session_id > 0 {
let session_key = pkt.session_id.to_string();
self.session_to_email.insert(session_key, email.clone()); // "email" is display_name
}Reality check: Comment says "email" but it's actually display name. The HashMap is misnamed.
OAuth flow (meeting-api/src/token.rs:36-41):
pub struct SessionTokenClaims {
pub sub: String, // ✅ Actual email from OAuth provider
pub name: String, // ✅ Display name from OAuth
// ...
}OAuth has proper separation, but protocol doesn't:
- OAuth:
sub(email) separate fromname(display name) - Protocol:
emailfield conflates both concepts
Result:
- OAuth users:
useridmight bealice@company.com(real email) - Non-OAuth users:
useridisAliceAtHome(arbitrary display name) - Same field, completely different semantics
Structure:
message PacketWrapper {
string email = 2; // Display name (from userid)
uint64 session_id = 4; // Server-assigned GUID
}
message MediaPacket {
string email = 2; // Also display name (duplicate!)
// ...
}Flow:
- Client populates
PacketWrapper.emailwithoptions.userid(display name) - Client sends with
session_id = 0 - Server stamps
session_idfrom connection state - Server broadcasts to room
Key point: Both email (display name) and session_id (GUID) transmitted together.
Structure (health_packet.proto:54-65):
message HealthPacket {
string session_id = 1; // Numeric session_id as string
string meeting_id = 2;
string reporting_peer = 3; // Display name
map<string, PeerStats> peer_stats = 7; // Keys are peer session_ids!
}Creation (videocall-client/src/health_reporter.rs:521-523):
pb.session_id = session_id.to_string(); // Own session_id
pb.reporting_peer = reporting_peer.to_string(); // Own display name
// peer_stats keys:
for (peer_id, peer_data) in health_map {
// peer_id is session_id (numeric string) from diagnostics
pb.peer_stats.insert(peer_id.clone(), peer_stats);
}Problem: peer_stats map keys are session_ids, but reporting_peer is display name. No consistent identifier.
Diagnostics events use display names (videocall-client/src/health_reporter.rs:237-244):
// Diagnostics metrics include:
"from_peer" => { // Source peer display name
reporting_peer = Some(s.clone());
}
"to_peer" => { // Target peer display name
target_peer = Some(s.clone());
}But HEALTH packet uses session_ids for peer keys. Requires mapping layer.
Original assumption:
- All users authenticated via OAuth
emailfield would always contain actual email- Email would serve as unique identifier
Reality:
- Non-OAuth mode added later
- Users type arbitrary display names
emailfield retained for backward compatibility
Timeline:
- Initial:
emailfield as unique identifier - Added:
session_idfor server-side session management - Result: Two overlapping identifiers, never reconciled
Current state: Band-aids everywhere:
- Client uses
useridbut populatesemailfield - Server uses
session_idinternally - vcprobe builds
session_to_emailmapping - Developers confused about which to use
Fields named for original intent, not current usage:
email→ should beuser_identifierordisplay_nameuserid→ could be email OR display name depending on contextreporting_peer→ is a display name, not a peer ID
1. Add inline documentation:
pub struct VideoCallClientOptions {
/// User identifier for display and packet tagging.
/// - In OAuth mode: usually the user's email from the OAuth provider
/// - In non-OAuth mode: arbitrary display name entered by user
/// This value is sent in PacketWrapper.email field (despite the name).
pub userid: String,
}2. Rename variables in new code:
// Instead of:
let email = username_ref.value();
// Use:
let display_name = username_ref.value();
let user_identifier = username_ref.value();3. Update comments in protobuf:
message PacketWrapper {
string email = 2 [deprecated = true]; // Actually contains display_name/user_identifier
uint64 session_id = 4; // Server-assigned unique session GUID
}Protobuf V2 (backward compatible):
message PacketWrapper {
PacketType packet_type = 1;
string email = 2 [deprecated = true]; // Kept for backward compat
bytes data = 3;
uint64 session_id = 4;
// New fields (optional for backward compatibility):
string user_identifier = 5; // OAuth email OR non-OAuth username
string display_name = 6; // Friendly display name (can differ from identifier)
}Migration strategy:
- Clients populate both
email(old) anduser_identifier(new) - Server prefers
user_identifierif present, falls back toemail - After 6-month grace period, deprecate
emailfield - Eventually remove
emailfield in V3
Proper separation:
// User identity (immutable)
message UserIdentity {
string user_id = 1; // Email (OAuth) or username (non-OAuth)
uint64 session_id = 2; // Server-assigned session GUID
}
// Display metadata (mutable)
message UserProfile {
string display_name = 1; // Friendly name shown in UI
string avatar_url = 2; // Future: profile picture
}
message PacketWrapper {
PacketType packet_type = 1;
UserIdentity identity = 2;
UserProfile profile = 3;
bytes data = 4;
}Benefits:
- Clear separation: identity vs presentation
- Session management uses
session_idexclusively - Display uses
display_nameexclusively - Supports future features (avatars, nicknames)
Tasks:
- Add clarifying comments to all
emailfields - Document
useridbehavior in OAuth vs non-OAuth - Update developer onboarding docs
- Add this document to
docs/
Impact: Zero code changes, clarifies existing behavior
Tasks:
- Rename internal variables:
email→user_identifier - Update function parameters with clear names
- Add type aliases:
type UserIdentifier = String; - Update tests to use clear names
Impact: Internal refactor, no protocol changes
Tasks:
- Add
user_identifieranddisplay_namefields to protobuf - Update clients to send both old and new fields
- Update server to prefer new fields, fall back to old
- Add feature flag for V2 protocol
Impact: Backward compatible protocol evolution
Tasks:
- Remove
emailfield population from clients - Server logs warnings when old fields used
- Remove backward compatibility shims
Impact: Clean protocol, technical debt eliminated
Add tests documenting current behavior:
#[test]
fn test_userid_can_be_non_email() {
let opts = VideoCallClientOptions {
userid: "JayAtHome".to_string(), // Not an email!
// ...
};
// Should work - userid is actually display_name
}
#[test]
fn test_session_id_is_unique_identifier() {
// Verify two users with same display_name get different session_ids
}
#[test]
fn test_health_packet_peer_stats_keys_are_session_ids() {
// Verify peer_stats keys are numeric session_ids, not display names
}Ensure backward compatibility:
#[test]
fn test_v2_packet_readable_by_v1_server() {
// V2 client sends user_identifier + email (old)
// V1 server should read email field
}
#[test]
fn test_v1_packet_readable_by_v2_server() {
// V1 client sends only email field
// V2 server should fall back to email
}-
Should
useridbe validated?- Require email format?
- Allow arbitrary strings?
- Different rules for OAuth vs non-OAuth?
-
Is
session_idthe source of truth for uniqueness?- Can two users have same display name?
- Should server enforce unique display names?
-
HEALTH packet keys: session_id or display_name?
- Current: session_id (requires mapping)
- Alternative: Use display_name (simpler but less robust)
-
What does "Username" mean to users?
- Is it their identity (like email)?
- Is it a nickname/display name?
- Should we rename the UI label?
-
OAuth users: show email or display name?
- In participant list:
alice@company.comorAlice Smith? - In vcprobe: which identifier?
- In participant list:
-
Renaming priority?
- Start with UI layer (most visible)?
- Start with protocol (most foundational)?
- Incremental across all layers?
-
Backward compatibility window?
- How long to support V1 protocol?
- Can we force upgrade for security reasons?
-
vcprobe fix: temporary or permanent?
- Keep
session_to_emailmapping as-is? - Wait for protocol V2?
- Rename to
session_to_display_namenow?
- Keep
Protocol:
protobuf/types/packet_wrapper.proto- PacketWrapper definitionprotobuf/types/media_packet.proto- MediaPacket definitionprotobuf/types/health_packet.proto- HealthPacket definition
Client:
videocall-client/src/client/video_call_client.rs:65- VideoCallClientOptionsvideocall-client/src/health_reporter.rs:107- HealthReporter::newvideocall-client/src/connection/connection.rs:250- Packet email field population
UI:
yew-ui/src/pages/home.rs:156- Username input fieldyew-ui/src/components/attendants.rs:288- VideoCallClient creationyew-ui/src/pages/meeting.rs:189- OAuth vs non-OAuth userid
Backend:
actix-api/src/actors/chat_server.rs:228- Session ID stampingactix-api/src/session_manager.rs:125- SESSION_ASSIGNED packet
Monitoring:
vcprobe/src/state.rs:99- session_to_email mappingvcprobe/src/state.rs:143- Mapping construction
The session_id vs email (actually display_name) confusion is a technical debt from protocol evolution. It causes:
- Developer confusion
- Complicated monitoring code
- Inconsistent identity semantics
- Blocked protocol improvements
Recommended fix: Incremental migration to explicit user_identifier and display_name fields, deprecating the misleading email field.
Timeline: 1 week (docs) → 1 month (renaming) → 2 months (protocol V2) → 6 months (deprecation)
Priority: Medium - Not blocking current functionality, but impedes future work and onboarding.
my 2cents:
session_id
display_name
displayName(ordisplay_name, depending on case style)email, and sometimes username as described above
change to be called
uuid(user unique id) throughout to avoid confusionif authenticated,
if not authenticated,
stored in session cookie (part of jwt token)
should packet wrapper also contain
uuid?