Banata

Authentication

Email & Password

Configure email/password authentication with email verification, password reset, and customizable validation rules.

Email & password is the most common authentication method. Banata Auth provides a complete implementation with email verification, password reset, configurable validation, and automatic audit logging.


Configuration

Enable email/password 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: {
      emailPassword: true,  // Enable email & password sign-up/sign-in
    },
 
    emailOptions: {
      fromAddress: "MyApp <noreply@myapp.com>",
    },
  };
}

Default Delivery Model

Banata is dashboard-first here:

  1. Configure an email provider in Emails > Providers
  2. Keep the auth email types enabled in Emails > Configuration
  3. Customize the built-in templates in Email Templates
  4. Call the auth client from your frontend

Verification and password-reset emails are then sent automatically by Banata. Developers should not need to write Resend, SendGrid, or provider-specific code for the normal setup.

Optional Code Overrides

If you want to bypass the dashboard-managed delivery path for a specific project, add callbacks in config.email. These callbacks override Banata's automatic sending for that email type.

typescript
email: {
  sendVerificationEmail: async ({ user, url, token }) => {
    await fetch("https://api.resend.com/emails", {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        Authorization: `Bearer ${process.env.RESEND_API_KEY}`,
      },
      body: JSON.stringify({
        from: "MyApp <noreply@myapp.com>",
        to: user.email,
        subject: "Verify your email address",
        html: `<a href="${url}">Verify Email</a>`,
      }),
    });
  },
 
  sendResetPassword: async ({ user, url, token }) => {
    await fetch("https://api.resend.com/emails", {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        Authorization: `Bearer ${process.env.RESEND_API_KEY}`,
      },
      body: JSON.stringify({
        from: "MyApp <noreply@myapp.com>",
        to: user.email,
        subject: "Reset your password",
        html: `<a href="${url}">Reset Password</a>`,
      }),
    });
  },
},

Override Callback Parameters

CallbackParametersDescription
sendVerificationEmail{ user, url, token }Called after sign-up only when you override verification delivery in code. url is the full verification link.
sendResetPassword{ user, url, token }Called when user requests password reset only when you override reset delivery in code.

Development tip: In development, you can log to console instead of sending real emails if you intentionally want a local override:

typescript
sendVerificationEmail: async ({ user, url }) => {
  console.log(`[DEV] Verify ${user.email}: ${url}`);
},

Client-Side API

Sign Up

Use the Better Auth client to sign up with email and password:

typescript
import { authClient } from "@/lib/auth-client";
 
// Sign up a new user
const { data, error } = await authClient.signUp.email({
  email: "user@example.com",
  password: "securePassword123",
  name: "Jane Doe",              // Optional but recommended
});
 
if (error) {
  // Handle errors:
  // - "User already exists" (409)
  // - "Invalid email" (422)
  // - "Password too short" (422)
  console.error(error.message);
} else {
  // data contains the created user and session
  console.log(data.user);     // { id, email, name, ... }
  console.log(data.session);  // { id, token, expiresAt, ... }
}

Sign In

typescript
const { data, error } = await authClient.signIn.email({
  email: "user@example.com",
  password: "securePassword123",
});
 
if (error) {
  // Handle errors:
  // - "Invalid email or password" (401)
  // - "Email not verified" (403)
  // - "Account is banned" (403)
  // - "Too many attempts" (429)
  console.error(error.message);
} else {
  // Redirect to app
  window.location.href = "/dashboard";
}

Sign Out

typescript
await authClient.signOut();
// Session cookie is cleared, redirect to sign-in
window.location.href = "/sign-in";

Request Password Reset

typescript
const { error } = await authClient.forgetPassword({
  email: "user@example.com",
  redirectTo: "/reset-password",  // Where to redirect after clicking reset link
});
 
if (error) {
  console.error(error.message);
} else {
  // Email sent — show confirmation message
}

Reset Password (with token from email)

typescript
// On the /reset-password page, extract the token from URL params
const token = new URLSearchParams(window.location.search).get("token");
 
const { error } = await authClient.resetPassword({
  newPassword: "newSecurePassword456",
  token: token!,
});
 
if (error) {
  // "Invalid or expired token" (400)
  // "Password too short" (422)
  console.error(error.message);
} else {
  // Password updated — redirect to sign-in
  window.location.href = "/sign-in";
}

Verify Email

Email verification typically happens automatically when the user clicks the link in the verification email. The link hits the /api/auth/verify-email endpoint with the token, and Better Auth handles the verification.

If you need to resend the verification email:

typescript
const { error } = await authClient.sendVerificationEmail({
  email: "user@example.com",
  callbackURL: "/dashboard",  // Where to redirect after verification
});

Password Validation

Banata Auth validates passwords using the passwordSchema from @banata-auth/shared:

RuleDefault
Minimum length8 characters
Maximum length128 characters
TypeString

The validation is applied on both sign-up and password reset. If a password doesn't meet the requirements, a ValidationError (422) is returned.

Custom Password Rules

To enforce additional rules (uppercase, numbers, special characters), you can add custom validation in your sign-up form before calling the API:

typescript
function validatePassword(password: string): string | null {
  if (password.length < 8) return "Password must be at least 8 characters";
  if (password.length > 128) return "Password must be at most 128 characters";
  if (!/[A-Z]/.test(password)) return "Password must contain an uppercase letter";
  if (!/[0-9]/.test(password)) return "Password must contain a number";
  if (!/[^A-Za-z0-9]/.test(password)) return "Password must contain a special character";
  return null; // Valid
}

Pre-Built UI Components

Banata Auth ships a SignUpForm and SignInForm in @banata-auth/react that handle the entire email/password flow:

Sign Up Form

tsx
import { SignUpForm } from "@banata-auth/react";
import { authClient } from "@/lib/auth-client";
 
export default function SignUpPage() {
  return (
    <SignUpForm
      authClient={authClient}
      onSuccess={() => {
        window.location.href = "/dashboard";
      }}
      onError={(error) => {
        console.error("Sign up failed:", error.message);
      }}
    />
  );
}

Sign In Form

tsx
import { SignInForm } from "@banata-auth/react";
import { authClient } from "@/lib/auth-client";
 
export default function SignInPage() {
  return (
    <SignInForm
      authClient={authClient}
      onSuccess={() => {
        window.location.href = "/dashboard";
      }}
      onError={(error) => {
        console.error("Sign in failed:", error.message);
      }}
    />
  );
}

Note: The pre-built forms use window.location.href for navigation after success (hard navigation, not client-side routing). This ensures the session cookie is properly read on the next page load.


Server-Side User Management

Use the admin SDK to manage users from your backend:

typescript
import { BanataAuth } from "@banata-auth/sdk";
 
const banata = new BanataAuth({
  apiKey: "your-api-key",
  baseUrl: "https://your-deployment.convex.site",
});
 
// Create a user programmatically
const user = await banata.users.createUser({
  projectId: "proj_01HXYZ...",
  email: "admin@example.com",
  password: "securePassword123",
  name: "Admin User",
  role: "admin",
});
 
// List all users (paginated)
const { data, listMetadata } = await banata.users.listUsers({
  limit: 20,
});
 
// Get a specific user
const user = await banata.users.getUser({ userId: "usr_01HXYZ..." });
 
// Update a user
await banata.users.updateUser({
  userId: "usr_01HXYZ...",
  name: "Updated Name",
  metadata: { plan: "pro" },
});
 
// Ban a user
await banata.users.banUser({ userId: "usr_01HXYZ..." });
 
// Delete a user
await banata.users.deleteUser({ userId: "usr_01HXYZ..." });

Override Delivery With Your Own Provider Code

Most teams should configure providers in the Banata dashboard and let Banata send auth emails automatically. Use code like this only if you deliberately want your app to own delivery.

Resend

typescript
sendVerificationEmail: async ({ user, url }) => {
  await fetch("https://api.resend.com/emails", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      Authorization: `Bearer ${process.env.RESEND_API_KEY}`,
    },
    body: JSON.stringify({
      from: "MyApp <noreply@myapp.com>",
      to: user.email,
      subject: "Verify your email",
      html: `<a href="${url}">Verify your email</a>`,
    }),
  });
},

SendGrid

typescript
sendVerificationEmail: async ({ user, url }) => {
  await fetch("https://api.sendgrid.com/v3/mail/send", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      Authorization: `Bearer ${process.env.SENDGRID_API_KEY}`,
    },
    body: JSON.stringify({
      personalizations: [{ to: [{ email: user.email }] }],
      from: { email: "noreply@myapp.com", name: "MyApp" },
      subject: "Verify your email",
      content: [{ type: "text/html", value: `<a href="${url}">Verify</a>` }],
    }),
  });
},

AWS SES (via fetch)

typescript
// Note: AWS SES requires signing requests with SigV4.
// Consider using the AWS SDK or a wrapper service.

Important: Email override callbacks run inside Convex functions, which means you can only use fetch() — you cannot use Node.js-specific libraries like nodemailer. If you do not need a custom delivery path, leave these callbacks out and use the dashboard-managed provider instead.


Audit Events

When email/password auth is enabled, the following events are automatically logged to the audit log:

EventWhen
user.createdA new user signs up
user.updatedUser profile is updated
user.deletedUser account is deleted
session.createdUser signs in (new session)
session.revokedUser signs out (session ended)
email.verifiedUser verifies their email
password.resetUser resets their password

See the Audit Logs guide for more details.


Error Handling

All auth endpoints return typed errors from @banata-auth/shared:

ErrorHTTP StatusWhen
AuthenticationError401Invalid email or password
ForbiddenError403Email not verified, account banned
NotFoundError404User not found
ConflictError409Email already registered
ValidationError422Invalid email format, password too short/long
RateLimitError429Too many sign-in attempts (>30/min) or sign-up attempts (>10/min)

Rate Limits

EndpointLimit
Sign In30 requests per minute
Sign Up10 requests per minute
General (all other endpoints)600 requests per minute

Security Best Practices

  1. Always enable email verification — Without it, anyone can sign up with someone else's email.
  2. Use HTTPS in production — Session cookies are Secure-flagged and won't work over HTTP.
  3. Set a strong BETTER_AUTH_SECRET — Use openssl rand -base64 32 to generate one.
  4. Rate limiting is built-in — Don't disable it. The defaults (30 sign-in/min, 10 sign-up/min) prevent brute force attacks.
  5. Don't expose error details — The default error messages are intentionally vague ("Invalid email or password") to prevent email enumeration.

What's Next