This document describes the implementation of authentic netplay IDs in RomM, which allows users to have unique identifiers for netplay sessions that are separate from their login usernames.
Previously, RomM used user login usernames as identifiers in netplay sessions. This implementation introduces dedicated netplay IDs that:
- Are separate from login credentials
- Can be customized by users
- Support future federation capabilities
- Maintain full backwards compatibility
ALTER TABLE users ADD COLUMN netplayid VARCHAR(255) NULL UNIQUE;
CREATE INDEX ix_users_netplayid ON users(netplayid);Backwards Compatibility: The column is nullable, so existing databases continue to work without modification.
# In backend/models/user.py
netplayid: Mapped[str | None] = mapped_column(
String(length=TEXT_FIELD_LENGTH), nullable=True, unique=True, index=True
)
# Kiosk user also updated
return cls(
id=-1,
username="kiosk",
netplayid="kiosk", # Added for consistency
# ... other fields
)- Endpoint:
PUT /api/users/{id} - New Field:
netplayidin UserForm - Validation:
- 3-32 characters
- Alphanumeric + underscore/dash only
- Unique across all users
- Optional (can be null/empty)
# Added method in backend/handler/database/users_handler.py
@begin_session
def get_user_by_netplayid(
self,
netplayid: str,
session: Session = None,
) -> User | None:
query = select(User).filter(User.netplayid == netplayid)
return session.scalar(query.limit(1))# In backend/endpoints/sfu.py mint_sfu_token()
sfu_identifier = user.netplayid or user.username # Fallback for backwards compatibility
token_data = {
"sub": sfu_identifier, # Uses netplayid if available
# ... other claims
}# SFU verify endpoint now returns netplay_username from database
user = db_user_handler.get_user_by_netplayid(sub)
if not user:
user = db_user_handler.get_user_by_username(sub) # Backwards compatibility
netplay_username = user.netplayid if user else None
return SFUVerifyResponse(sub=sub, netplay_username=netplay_username)- Location:
frontend/src/views/Settings/UserProfile.vue - Conditional Display: Only shows when
EJS_NETPLAY_ENABLED = true - Field Position: Between password and email fields in Account Details section
// In frontend/src/stores/users.ts
const netplayIdLength = (v: string) =>
(v.length >= 3 && v.length <= 32) || i18n.global.t("settings.netplay-id-length");
const netplayIdChars = (v: string) =>
/^[a-zA-Z0-9_-]*$/.test(v) || i18n.global.t("settings.netplay-id-chars");
netplayIdRules: [
(v: string) => !v || netplayIdLength(v), // Optional field
(v: string) => !v || netplayIdChars(v), // Only validate if not empty
]Updated generated types in:
frontend/src/__generated__/models/UserSchema.tsfrontend/src/__generated__/models/UserForm.ts
- ✅ Existing users have
NULLnetplayid (no migration data loss) - ✅ Old RomM versions can read the database (unknown column is ignored)
- ✅ No breaking schema changes
- ✅ Existing API calls work unchanged
- ✅ New
netplayidfield is optional in requests - ✅ SFU tokens work with fallback logic
- ✅ Feature is hidden when
EJS_NETPLAY_ENABLED = false - ✅ Existing profile page functionality unchanged
- ✅ TypeScript types are backwards compatible
- ✅ Authentication works with username fallback
- ✅ Existing tokens continue to function
- ✅ No changes required to SFU server logic
# Enable netplay ID feature in UI
EJS_NETPLAY_ENABLED=true- When disabled: Netplay ID field is hidden, system uses usernames
- When enabled: Users can set custom netplay IDs, fallback to username
This implementation is designed to support cross-instance netplay federation:
-- Can store federated IDs like "federated-romm.com:user123"
netplayid VARCHAR(255) UNIQUE# Future federated identifier format
federated_id = f"{issuer}:{user_id}"
# Examples:
# "romm:sfu:localuser" (local)
# "federated.com:sfu:remoteuser" (federated)The SFU server can be extended to:
- Accept multiple trusted issuers
- Route federated users to appropriate instances
- Handle cross-instance communication protocols
# Run from backend directory
cd backend
alembic upgrade head# Add to your RomM environment
EJS_NETPLAY_ENABLED=true# Rebuild frontend to pick up type changes
npm run build
# or
npm run dev- Check user profile page shows "Netplay ID" field
- Test setting and updating netplay IDs
- Verify SFU authentication still works
- Confirm backwards compatibility with existing users
- Server-side validation prevents injection attacks
- Client-side validation provides immediate feedback
- Length and character restrictions prevent abuse
- Database-level UNIQUE constraint on netplayid
- Duplicate prevention at API level
- Case-sensitive uniqueness (follows SQL standard)
- Netplay IDs are public identifiers for netplay sessions
- Separate from private login credentials
- Users can change IDs (with appropriate validation)
- ✅ Existing user without netplayid can authenticate
- ✅ Old SFU tokens continue working
- ✅ Database queries work with NULL values
- ✅ User can set netplay ID via profile page
- ✅ Validation prevents invalid IDs
- ✅ Uniqueness prevents duplicate IDs
- ✅ SFU uses netplay ID for authentication
- ✅ Empty string clears netplay ID
- ✅ Username fallback when netplay ID not set
- ✅ Migration doesn't affect existing data
Migration Fails
# Check database permissions
# Ensure no duplicate netplayid values exist
# Verify alembic is properly configuredFrontend Doesn't Show Field
# Check EJS_NETPLAY_ENABLED=true
# Clear browser cache
# Rebuild frontendSFU Authentication Fails
# Check token generation uses correct identifier
# Verify database has netplayid values
# Check SFU server logs for authentication errorsbackend/alembic/versions/0064_add_netplayid.py- Database migrationbackend/models/user.py- User model updatesbackend/endpoints/user.py- API validationbackend/endpoints/sfu.py- Token generation/verificationbackend/handler/database/users_handler.py- Database queries
frontend/src/views/Settings/UserProfile.vue- Profile page UIfrontend/src/stores/users.ts- Validation rulesfrontend/src/__generated__/models/UserSchema.ts- TypeScript typesfrontend/src/__generated__/models/UserForm.ts- Form types
- Environment variable:
EJS_NETPLAY_ENABLED - Conditional feature display based on netplay support
This implementation provides a solid foundation for user-controlled netplay identities while maintaining full backwards compatibility and preparing for future federation capabilities.