Username sign-in & optional email

Let users sign in with a username, allow accounts without an email address, and handle recovery for shared or missing emails.

#Username sign-in & optional email

By default EmuView accounts are identified by a unique email address. Instances migrating from systems like WordPress or phpBB often have users who share one email across several accounts (families) or who have no email at all. Username sign-in solves both: usernames become an additional unique login identifier, and email becomes optional.

Both behaviors are opt-in settings under Settings → General → Authentication:

Setting Default Effect
Username Sign-In Disabled Users can sign in with a username as well as an email. Sign-up and user import accept a username.
Require Email at Signup Enabled When disabled (requires Username Sign-In), accounts can be created with only a username.
Reserved Usernames Instance-specific names nobody may register, merged with the built-in list (admin, root, support, …).

Changes take effect immediately — no redeploy or restart. Disabling Username Sign-In keeps all usernames in place; re-enabling restores them.

Warning

Accounts created without an email can only sign in by username. Disabling Username Sign-In locks them out until it is re-enabled — the settings page warns you with a count before you do this.

#How sign-in works

One identifier field serves both cases: usernames can never contain @, so anything with an @ is treated as an email and anything without one as a username.

// SDK — pick whichever form fits your UI
await sdk.auth.signIn({ email: 'user@example.com', password });
await sdk.auth.signIn({ username: 'mattsmith', password });
await sdk.auth.signIn({ identifier: formValue, password }); // routes automatically

Check what the instance supports before rendering your login form:

const caps = await sdk.auth.getCapabilities();
// { usernameEnabled: boolean, requireEmail: boolean, socialProviders: {...} }

#Username rules

  • 3–30 characters, letters/digits/underscore/dot ([a-zA-Z0-9_.])
  • Case-insensitive: stored lowercase, the typed form is kept for display
  • Reserved names (built-in + the Reserved Usernames setting) are rejected
  • Usernames are changed by admins only (Users → edit)

Availability check (shares the strict auth rate limit — call on blur/submit, not per keystroke):

const free = await sdk.auth.isUsernameAvailable('newname');

#Accounts without an email

When Require Email at Signup is disabled, sign-up and import accept a username with no email. Internally the account holds a non-routable placeholder address (…@noemail.invalid) that satisfies the unique-email requirement; it is never displayed and never mailed. API responses mask it (email: null), and the SDK exports isPlaceholderEmail() if you handle raw auth responses.

A real — possibly shared — address can be stored as the account's contact email, which is outbound-only: it is used for password resets and nothing else, and it does not need to be unique.

#Password recovery

Reset requests accept an email or a username:

await sdk.auth.requestPasswordReset({ identifier: 'mattsmith', redirectTo: '/reset' });
  • The response is always a generic success (no account enumeration).
  • Username-based accounts get the reset mail at their contact email; because that address may be shared by a family, the email always names the account it resets.
  • Magic links remain email-only — placeholder addresses are never mailed.
  • Accounts with no reachable email are recovered by an admin: Users → edit → Issue one-time reset link creates a 1-hour reset URL (audit-logged as user.recovery_link_issued) to pass on out-of-band.

#Importing users with shared or missing emails

POST /api/v1/users/import accepts an optional username per row. With Username Sign-In enabled:

Row Result
Unique email (± username) Imported normally
Duplicate email + username Imported: real address becomes the contact email, login slot gets a placeholder
No email + username (Require Email off) Imported as a username-only account
Duplicate email, no username Error already_exists (unchanged)
Duplicate username Error username_exists

Imported legacy passwords (WordPress/phpBB) keep working exactly as before — the lazy-rehash migration is identifier-agnostic, so first sign-in by username upgrades the hash the same way sign-in by email does.

#Security notes

  • Sign-in errors and timings do not distinguish unknown-username from wrong-password.
  • All auth endpoints share a per-IP rate limit (Settings → Security → Auth Rate Limit, default 5/min). is-username-available is deliberately under the same budget since it can enumerate usernames.
  • emailVerified never gates access in EmuView; placeholder addresses are always unverified.