Authentication
Username Authentication
Allow users to sign in with a username and password instead of email.
Username authentication allows users to sign up and sign in using a username and password combination instead of (or in addition to) an email address. This is common in gaming platforms, social apps, internal tools, and any application where usernames are the primary identity rather than email addresses.
Banata Auth supports username auth via the username plugin, which adds username fields to the user model and provides dedicated sign-up/sign-in endpoints.
How It Works
1. User enters a desired username and password on the sign-up page
2. Client calls authClient.signUp.username({ username, password })
3. Server validates the username (format, length, uniqueness)
4. Server validates the password (minimum length)
5. Server creates the user record with the username
6. Session is created and the user is authenticated
7. On subsequent visits, user signs in with username + passwordWhen to Use Username Auth
| Scenario | Recommended? | Reason |
|---|---|---|
| Gaming platforms | Yes | Players identify by gamertag/handle, not email |
| Social platforms | Yes | Usernames are the public identity |
| Internal tools | Yes | Employees may use a handle instead of corporate email |
| Developer platforms | Yes | Developers often prefer usernames (e.g., GitHub) |
| E-commerce | No | Email is needed for order confirmations and receipts |
| Enterprise SaaS | No | Email is the standard identity in business contexts |
Configuration
Enable username authentication in your BanataAuthConfig:
// convex/banataAuth/auth.ts
function buildConfig(): BanataAuthConfig {
return {
appName: "My App",
siteUrl: process.env.SITE_URL!,
secret: process.env.BETTER_AUTH_SECRET!,
authMethods: {
username: true, // Enable username/password authentication
},
};
}No email callbacks are required for username-only auth, since there is no email to verify or send reset links to.
Username with Optional Email
In many cases, you want to support both username and email. You can enable both methods and make email optional during sign-up:
authMethods: {
username: true, // Username/password sign-in
emailPassword: true, // Email/password sign-in (optional email on sign-up)
},When both are enabled, users can sign up with a username and optionally provide an email. If an email is provided, it follows the standard email verification flow.
Username Validation Rules
Banata Auth enforces the following rules on usernames:
| Rule | Value |
|---|---|
| Minimum length | 3 characters |
| Maximum length | 32 characters |
| Allowed characters | Lowercase letters (a-z), numbers (0-9), underscores (_), hyphens (-) |
| Must start with | A letter (a-z) |
| Case sensitivity | Case-insensitive -- JaneDoe and janedoe are treated as the same username |
| Uniqueness | Globally unique -- no two users can have the same username |
Usernames are stored in lowercase. When a user signs up with JaneDoe, it is normalized to janedoe for storage and comparison, but the original casing may be preserved for display purposes.
Reserved Usernames
The following usernames are reserved and cannot be registered:
admin,administratorsystem,root,superusersupport,help,contactnull,undefined,anonymous- Any username that matches a route in your application (e.g.,
settings,dashboard,api)
Tip: You can extend the reserved username list in your configuration to include application-specific terms.
Client-Side API
Sign Up with Username
import { authClient } from "@/lib/auth-client";
const { data, error } = await authClient.signUp.username({
username: "janedoe",
password: "securePassword123",
name: "Jane Doe", // Optional display name
email: "jane@example.com", // Optional -- only if emailPassword is also enabled
});
if (error) {
// Handle errors:
// - "Username already taken" (409)
// - "Invalid username format" (422)
// - "Username too short" (422)
// - "Username too long" (422)
// - "Password too short" (422)
console.error(error.message);
} else {
console.log(data.user); // { id, username, name, ... }
console.log(data.session); // { id, token, expiresAt, ... }
window.location.href = "/dashboard";
}Sign In with Username
const { data, error } = await authClient.signIn.username({
username: "janedoe",
password: "securePassword123",
});
if (error) {
// Handle errors:
// - "Invalid username or password" (401)
// - "Account is banned" (403)
// - "Too many attempts" (429)
console.error(error.message);
} else {
window.location.href = "/dashboard";
}Check Username Availability
Before sign-up, you can check if a username is available:
const { data, error } = await authClient.username.checkAvailability({
username: "janedoe",
});
if (data) {
console.log(data.available); // true or false
}This is useful for providing real-time feedback in the sign-up form as the user types.
Complete Sign-Up Form Example
"use client";
import { authClient } from "@/lib/auth-client";
import { useState, useEffect } from "react";
export default function UsernameSignUpPage() {
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
const [name, setName] = useState("");
const [email, setEmail] = useState("");
const [error, setError] = useState("");
const [usernameStatus, setUsernameStatus] = useState<
"idle" | "checking" | "available" | "taken"
>("idle");
// Check username availability with debounce
useEffect(() => {
if (username.length < 3) {
setUsernameStatus("idle");
return;
}
setUsernameStatus("checking");
const timer = setTimeout(async () => {
const { data } = await authClient.username.checkAvailability({
username,
});
setUsernameStatus(data?.available ? "available" : "taken");
}, 500);
return () => clearTimeout(timer);
}, [username]);
async function handleSubmit(e: React.FormEvent) {
e.preventDefault();
setError("");
const { data, error } = await authClient.signUp.username({
username,
password,
name: name || undefined,
email: email || undefined,
});
if (error) {
setError(error.message ?? "Sign up failed");
} else {
window.location.href = "/dashboard";
}
}
return (
<form onSubmit={handleSubmit}>
<h1>Create Account</h1>
<div>
<label htmlFor="username">Username</label>
<input
id="username"
type="text"
placeholder="janedoe"
value={username}
onChange={(e) => setUsername(e.target.value.toLowerCase())}
minLength={3}
maxLength={32}
pattern="[a-z][a-z0-9_-]*"
required
/>
{usernameStatus === "checking" && (
<span style={{ color: "#888" }}>Checking...</span>
)}
{usernameStatus === "available" && (
<span style={{ color: "green" }}>Available</span>
)}
{usernameStatus === "taken" && (
<span style={{ color: "red" }}>Already taken</span>
)}
</div>
<div>
<label htmlFor="password">Password</label>
<input
id="password"
type="password"
placeholder="Minimum 8 characters"
value={password}
onChange={(e) => setPassword(e.target.value)}
minLength={8}
required
/>
</div>
<div>
<label htmlFor="name">Display Name (optional)</label>
<input
id="name"
type="text"
placeholder="Jane Doe"
value={name}
onChange={(e) => setName(e.target.value)}
/>
</div>
<div>
<label htmlFor="email">Email (optional)</label>
<input
id="email"
type="email"
placeholder="jane@example.com"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
<small>
Adding an email enables password reset and email notifications.
</small>
</div>
{error && <p style={{ color: "red" }}>{error}</p>}
<button
type="submit"
disabled={usernameStatus === "taken" || usernameStatus === "checking"}
>
Create Account
</button>
</form>
);
}Sign-In Form
"use client";
import { authClient } from "@/lib/auth-client";
import { useState } from "react";
export default function UsernameSignInPage() {
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
const [error, setError] = useState("");
async function handleSubmit(e: React.FormEvent) {
e.preventDefault();
setError("");
const { data, error } = await authClient.signIn.username({
username,
password,
});
if (error) {
setError(error.message ?? "Sign in failed");
} else {
window.location.href = "/dashboard";
}
}
return (
<form onSubmit={handleSubmit}>
<h1>Sign In</h1>
<input
type="text"
placeholder="Username"
value={username}
onChange={(e) => setUsername(e.target.value)}
required
/>
<input
type="password"
placeholder="Password"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
/>
{error && <p style={{ color: "red" }}>{error}</p>}
<button type="submit">Sign In</button>
</form>
);
}Username Uniqueness Enforcement
Usernames are enforced as globally unique across all users in your application. The uniqueness check is performed at the database level:
- On sign-up -- The server checks if the normalized (lowercase) username already exists before creating the user.
- On username change -- If you allow users to change their username, the same uniqueness check applies.
- Concurrent requests -- The database enforces a unique index on the username field, preventing race conditions where two users attempt to claim the same username simultaneously.
Handling Conflicts
If a username is taken, the server returns a ConflictError (409):
const { error } = await authClient.signUp.username({
username: "janedoe",
password: "securePassword123",
});
if (error?.status === 409) {
// Username is taken -- prompt the user to choose another
}Combining Username with Email
When both username and emailPassword are enabled, users can sign in with either their username or their email:
// Sign in with username
await authClient.signIn.username({
username: "janedoe",
password: "securePassword123",
});
// Sign in with email (if provided during sign-up)
await authClient.signIn.email({
email: "jane@example.com",
password: "securePassword123",
});Both methods authenticate the same user and create the same session.
Password Reset with Username Auth
If a user signed up with only a username (no email), password reset via email is not available. Consider these alternatives:
- Require email on sign-up -- Make email mandatory even with username auth, so password reset is always available.
- Admin reset -- Allow administrators to reset passwords via the admin SDK.
- Security questions -- Implement a custom recovery flow (not built-in).
// Admin password reset via SDK
const banata = new BanataAuth({
apiKey: "your-api-key",
baseUrl: "https://your-deployment.convex.site",
});
await banata.users.updateUser({
userId: "usr_01HXYZ...",
password: "newTemporaryPassword",
});Server-Side User Management
Use the admin SDK to manage username-based users:
import { BanataAuth } from "@banata-auth/sdk";
const banata = new BanataAuth({
apiKey: "your-api-key",
baseUrl: "https://your-deployment.convex.site",
});
// Create a user with username
const user = await banata.users.createUser({
projectId: "proj_01HXYZ...",
username: "janedoe",
password: "securePassword123",
name: "Jane Doe",
});
// Look up a user by username
const user = await banata.users.getUserByUsername({
username: "janedoe",
});
// Update a username
await banata.users.updateUser({
userId: "usr_01HXYZ...",
username: "jane_doe_2",
});Audit Events
| Event | When |
|---|---|
user.created | New user signs up with username |
session.created | User signs in with username |
user.updated | Username is changed |
session.revoked | User signs out |
See the Audit Logs guide for more details.
Error Handling
| Error | HTTP Status | When |
|---|---|---|
AuthenticationError | 401 | Invalid username or password |
ForbiddenError | 403 | Account banned |
ConflictError | 409 | Username already taken |
ValidationError | 422 | Invalid username format, too short/long, or password too short |
RateLimitError | 429 | Too many sign-in attempts |
Rate Limits
| Endpoint | Limit |
|---|---|
| Sign In (username) | 30 requests per minute |
| Sign Up (username) | 10 requests per minute |
| Check Availability | 60 requests per minute |
Security Considerations
- Username enumeration -- The sign-in error message is intentionally vague ("Invalid username or password") to prevent attackers from determining whether a username exists. The
checkAvailabilityendpoint is rate-limited for the same reason. - Case normalization -- Usernames are case-insensitive to prevent confusion (e.g.,
JaneDoeandjanedoeare the same user). - Rate limiting -- Built-in rate limits prevent brute-force attacks against username/password combinations.
- No email fallback -- If a user signs up with only a username (no email), there is no automated password reset path. Plan your recovery strategy accordingly.
- Reserved usernames -- The reserved list prevents users from claiming system-level or potentially confusing usernames.
- Password security -- The same password validation rules apply as with email/password auth (minimum 8 characters, maximum 128 characters).
Troubleshooting
"Invalid username format"
The username does not meet the validation rules. Ensure the username:
- Is at least 3 characters long
- Is at most 32 characters long
- Starts with a letter
- Contains only lowercase letters, numbers, underscores, and hyphens
"Username already taken"
Another user has registered with the same username. Suggest alternatives or prompt the user to choose a different one. Remember that usernames are case-insensitive.
"Cannot reset password"
The user signed up with username only and has no email address linked. Use the admin SDK to reset their password, or ask them to add an email to their account first.
What's Next
- Email & Password -- Traditional email-based authentication
- Anonymous Auth -- Guest access with optional upgrade
- Social OAuth -- Add Google, GitHub, etc. alongside username auth
- Roles & Permissions -- Fine-grained access control