Username
The username plugin adds username support to the email/password authenticator. Users can sign up with a username, sign in with that username instead of their email, update it later, and check availability.
This plugin is implemented in the Ruby port. Ruby uses snake_case option names
and auth.api method names, while HTTP route paths and JSON keys preserve the
upstream wire contract.
Installation
Add the plugin on the server:
require "better_auth"
auth = BetterAuth.auth(
secret: ENV.fetch("BETTER_AUTH_SECRET"),
base_url: ENV.fetch("BETTER_AUTH_URL", "http://localhost:3000"),
email_and_password: {
enabled: true
},
plugins: [
BetterAuth::Plugins.username
]
)Migrate The Database
The plugin adds username and displayUsername fields to the user schema.
For Rails, configure the plugin before generating the migration:
BetterAuth::Rails.configure do |config|
config.plugins = [
BetterAuth::Plugins.username
]
endbin/rails generate better_auth:migration
bin/rails db:migrateFor direct SQL apps, generate or apply the SQL schema for your configured
adapter after the plugin is present in plugins.
Usage
Sign Up
Use the existing email sign-up endpoint and include username. If
displayUsername is omitted, it is set from the pre-normalized username.
result = auth.api.sign_up_email(
body: {
email: "email@domain.com",
name: "Test User",
password: "password1234",
username: "test",
displayUsername: "Test User123"
}
)HTTP route:
POST /api/auth/sign-up/email{
"email": "email@domain.com",
"name": "Test User",
"password": "password1234",
"username": "test",
"displayUsername": "Test User123"
}Sign In
The plugin adds /sign-in/username and the matching Ruby API method
sign_in_username.
result = auth.api.sign_in_username(
body: {
username: "test",
password: "password1234"
}
)
token = result[:token]
user = result[:user]HTTP route:
POST /api/auth/sign-in/username{
"username": "test",
"password": "password1234"
}Update Username
Use the core update-user endpoint with an authenticated session cookie.
result = auth.api.update_user(
headers: {
"cookie" => request.env["HTTP_COOKIE"]
},
body: {
username: "new-username"
}
)HTTP route:
POST /api/auth/update-user{
"username": "new-username"
}You can update the display value separately:
auth.api.update_user(
headers: {
"cookie" => request.env["HTTP_COOKIE"]
},
body: {
username: "new-username",
displayUsername: "New Username"
}
)Check Availability
response = auth.api.is_username_available(
body: {
username: "new-username"
}
)
if response[:available]
# username is available
else
# username is already taken
endHTTP route:
POST /api/auth/is-username-available{
"username": "new-username"
}Options
Min Username Length
The minimum length of the username. Default is 3.
auth = BetterAuth.auth(
email_and_password: {
enabled: true
},
plugins: [
BetterAuth::Plugins.username(
min_username_length: 5
)
]
)Max Username Length
The maximum length of the username. Default is 30.
auth = BetterAuth.auth(
email_and_password: {
enabled: true
},
plugins: [
BetterAuth::Plugins.username(
max_username_length: 100
)
]
)Username Validator
Provide a callable that returns false when a username should be rejected. By
default, usernames may contain letters, numbers, underscores, and dots.
auth = BetterAuth.auth(
email_and_password: {
enabled: true
},
plugins: [
BetterAuth::Plugins.username(
username_validator: ->(username) {
username != "admin" && username.match?(/\A[a-zA-Z0-9_.]+\z/)
}
)
]
)Display Username Validator
Provide a callable to validate displayUsername. By default, display usernames
do not get additional validation.
auth = BetterAuth.auth(
email_and_password: {
enabled: true
},
plugins: [
BetterAuth::Plugins.username(
display_username_validator: ->(display_username) {
display_username.match?(/\A[a-zA-Z0-9_\-\s]+\z/)
}
)
]
)Username Normalization
By default, usernames are normalized to lowercase. Set
username_normalization: false to disable normalization or provide a callable.
auth = BetterAuth.auth(
email_and_password: {
enabled: true
},
plugins: [
BetterAuth::Plugins.username(
username_normalization: ->(username) {
username
.downcase
.tr("034", "oea")
}
)
]
)Display Username Normalization
By default, display usernames are preserved as provided. You can provide a callable to normalize them.
auth = BetterAuth.auth(
email_and_password: {
enabled: true
},
plugins: [
BetterAuth::Plugins.username(
display_username_normalization: ->(display_username) {
display_username.strip
}
)
]
)Validation Order
By default, username and display username validation happens before
normalization. Set a field to "post-normalization" to validate the normalized
value instead.
auth = BetterAuth.auth(
email_and_password: {
enabled: true
},
plugins: [
BetterAuth::Plugins.username(
validation_order: {
username: "post-normalization",
display_username: "post-normalization"
}
)
]
)Disable Availability Checks
The plugin exposes /is-username-available by default. You can disable the route
through the core disabled_paths option.
auth = BetterAuth.auth(
email_and_password: {
enabled: true
},
disabled_paths: ["/is-username-available"],
plugins: [
BetterAuth::Plugins.username
]
)Schema
The plugin adds these fields to the user table:
| Field | Type | Required | Description |
|---|---|---|---|
username | string | No | Normalized username. It is unique and returned in user output. |
displayUsername | string | No | Non-normalized username for display. |
Errors
| Error | Meaning |
|---|---|
INVALID_USERNAME_OR_PASSWORD | The username/password pair is invalid. |
EMAIL_NOT_VERIFIED | Email verification is required before sign-in. |
USERNAME_IS_ALREADY_TAKEN | Another user already owns the username. |
USERNAME_TOO_SHORT | The username is shorter than min_username_length. |
USERNAME_TOO_LONG | The username is longer than max_username_length. |
INVALID_USERNAME | The username validator rejected the value. |
INVALID_DISPLAY_USERNAME | The display username validator rejected the value. |