Banata

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

typescript
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 + password

When to Use Username Auth

ScenarioRecommended?Reason
Gaming platformsYesPlayers identify by gamertag/handle, not email
Social platformsYesUsernames are the public identity
Internal toolsYesEmployees may use a handle instead of corporate email
Developer platformsYesDevelopers often prefer usernames (e.g., GitHub)
E-commerceNoEmail is needed for order confirmations and receipts
Enterprise SaaSNoEmail is the standard identity in business contexts

Configuration

Enable username authentication in your BanataAuthConfig:

typescript
// 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:

typescript
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:

RuleValue
Minimum length3 characters
Maximum length32 characters
Allowed charactersLowercase letters (a-z), numbers (0-9), underscores (_), hyphens (-)
Must start withA letter (a-z)
Case sensitivityCase-insensitive -- JaneDoe and janedoe are treated as the same username
UniquenessGlobally 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, administrator
  • system, root, superuser
  • support, help, contact
  • null, 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

typescript
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

typescript
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:

typescript
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

tsx
"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

tsx
"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:

  1. On sign-up -- The server checks if the normalized (lowercase) username already exists before creating the user.
  2. On username change -- If you allow users to change their username, the same uniqueness check applies.
  3. 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):

typescript
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:

typescript
// 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:

  1. Require email on sign-up -- Make email mandatory even with username auth, so password reset is always available.
  2. Admin reset -- Allow administrators to reset passwords via the admin SDK.
  3. Security questions -- Implement a custom recovery flow (not built-in).
typescript
// 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:

typescript
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

EventWhen
user.createdNew user signs up with username
session.createdUser signs in with username
user.updatedUsername is changed
session.revokedUser signs out

See the Audit Logs guide for more details.


Error Handling

ErrorHTTP StatusWhen
AuthenticationError401Invalid username or password
ForbiddenError403Account banned
ConflictError409Username already taken
ValidationError422Invalid username format, too short/long, or password too short
RateLimitError429Too many sign-in attempts

Rate Limits

EndpointLimit
Sign In (username)30 requests per minute
Sign Up (username)10 requests per minute
Check Availability60 requests per minute

Security Considerations

  1. Username enumeration -- The sign-in error message is intentionally vague ("Invalid username or password") to prevent attackers from determining whether a username exists. The checkAvailability endpoint is rate-limited for the same reason.
  2. Case normalization -- Usernames are case-insensitive to prevent confusion (e.g., JaneDoe and janedoe are the same user).
  3. Rate limiting -- Built-in rate limits prevent brute-force attacks against username/password combinations.
  4. 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.
  5. Reserved usernames -- The reserved list prevents users from claiming system-level or potentially confusing usernames.
  6. 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