Something always felt a bit off when trying to add auth to my apps. You'd try something PassportJS and find yourself with a mush of magical, (overly) stateful code, while still having to worry about all'em little tidbits.

Then you'd try rolling your own auth, and that's, well... Almost always a problem. Auth is something you don't want to mess up, and if you don't have the time to properly write, test, fix, test, realize you're still far from being done and live in fear for the rest of your life - you need a battle-tested solution.

Solutions like Clerk, Supabase etc. are nice, though they have some issues - above all, the price. I've seen people actually make product-level compromises in order to stay on their current Clerk tier. You NEVER want to be a prisoner of your own device. (eagle and guitar)

Enters Better Auth. Man, I'd marry this library if I could. It gives you so much freedom but keeps you in line, you almost never get into a guessing game, it makes complicated things look easy without the over-abstraction, and mainly - it just works. You plug it in, write a bit of nice-looking code, and it works.

OK, enough with the poetry! Let's get our hands dirty and see why I think you should give Better Auth a try.

1. Universal Framework Support

Better Auth supports virtually every full-stack and backend framework in the JavaScript ecosystem. Looking at their docs, you'll find support for Astro, Remix, Next.js, Nuxt, SvelteKit, SolidStart, TanStack Start, and backend-only frameworks like Hono, Fastify, Express, Nitro, Nest.js—even Expo for React Native.

The setup experience is remarkably consistent across all frameworks. The only variation is how you configure the API endpoints for each framework. Learn Better Auth once, and you just need to check the framework-specific page for minor integration differences.

For example, with Next.js you create a catch-all handler that exports to the Next.js handler. Nuxt and Hono follow the same pattern with their respective wildcard handlers.

Client-side integration is equally straightforward. There's a framework-specific createAuthClient for React, Vue, Svelte, and Solid, along with hooks like useSession for each. If you've used any other auth framework before, you'll feel right at home.

On top of that, Better Auth supports virtually every database: MySQL, SQLite, PostgreSQL, Microsoft SQL, MongoDB, plus adapters for Drizzle and Prisma. Community adapters exist for Convex and Payload, and you can always create your own database adapter if needed.

2. Email and Password Authentication Done Right

Better Auth supports email and password authentication out of the box, along with any number of social providers. While many DIY auth frameworks discourage email/password, sometimes that's simply what you need.

Things like email verification are simple and straight-forward. In your Better Auth config, you set up sendVerificationEmail with whatever logic you want—talk to an SMTP server, use a service like Resend or AWS SES, or just log the URL during development.

emailVerification: {
  sendVerificationEmail: async ({ user, url, token }) => {
    // Send email however you want
    // The token/URL is what Better Auth uses to verify the user
  }
}

Setting up password reset follows the same pattern:

sendResetPassword: async ({ user, url, token }) => {
  // Send reset password email
}

Tip:

For local development, using Ethereal Email is a great approach. It provides a temporary inbox where you can preview verification emails without setting up a real mail server.

Just as simply, you can use native plugins for stuff like One-Time Passwords and Magic Links:

plugins: [
    magicLink({
     sendMagicLink: async ({ email, token, url }, ctx) => {
      // send email to user
     }
    }),
    emailOTP({
     async sendVerificationOTP({ email, otp, type }) {
       if (type === "sign-in") {
         // Send the OTP for sign in
       } else if (type === "email-verification") {
         // Send the OTP for email verification
       } else {
         // Send the OTP for password reset
       }
      }
    })
  ]

3. Bearer Token Authentication

This might scare some developers, but Better Auth can enable bearer token authentication. Many JavaScript auth frameworks force you into session cookies, but there are legitimate scenarios where cookies won't work:

  • Separate backend hosted on a different domain (cross-domain cookie issues)
  • Building an API that any client needs to access
  • Mobile applications

Better Auth's bearer plugin handles this elegantly:

// Store the token on successful auth
onSuccess: (ctx) => {
  localStorage.setItem('token', ctx.data.token);
}

// Automatically include it in requests
fetchOptions: {
  headers: {
    Authorization: `Bearer ${localStorage.getItem('token')}`
  }
}

Yes, there are security considerations with storing tokens in localStorage or sessionStorage. Better Auth includes appropriate disclaimers, but if you understand the tradeoffs, this plugin makes implementation straightforward.

4. Powerful Hook System

Better Auth's customizability shines through its hook system. Before and after hooks let you modify behavior at any point in the auth flow—sign up, login, password reset, session creation, and more.

Simple Example: Domain-Restricted Signups

before: async (ctx) => {
  if (ctx.body.email && !ctx.body.email.endsWith('@yourcompany.com')) {
    throw new Error('Only company emails allowed');
  }
}

Real-World Example: Auto-Login After Password Reset

By default, after resetting a password, users have to go back to the login page. I wanted users to be logged in automatically after a successful reset. With hooks, this was simple:

// Before hook: attach the email to the response
before: async (ctx) => {
  if (ctx.path === '/reset-password') {
    const verification = await getVerificationFromDB(ctx.body.token);
    ctx.body.email = verification.email;
  }
}

// After hook: include email in successful response
after: async (ctx) => {
  if (ctx.path === '/reset-password' && ctx.response.success) {
    ctx.response.email = savedEmail;
  }
}

The frontend then has access to both the email and the password (the user just typed it), enabling an automatic sign-in redirect.

5. Database Hooks

Beyond request/response hooks, Better Auth provides database hooks that fire when users, sessions, or accounts are created, updated, or deleted.

Example: First User Becomes Admin

A common pattern for self-hosted apps is making the first user an administrator:

user: {
  create: {
    before: async (user) => {
      const userCount = await db.users.count();
      if (userCount === 0) {
        user.role = 'admin';
      }
      return user;
    }
  }
}

No need for custom database adapters or hacky workarounds—just clean, declarative hooks.

6. Admin Plugin

The admin plugin provides everything you need to manage users in your system:

  • Create, list, update, delete users
  • Set roles and permissions
  • Ban/unban users
  • List and revoke sessions
  • Impersonate users (invaluable for debugging and support)

User impersonation deserves special mention. Admins can temporarily become any user to see exactly what they're experiencing—without needing their password.

Role-Based Access Control

Out of the box, you get user and admin roles with sensible defaults. But you can create custom resources and permissions:

ac: {
  project: ['create', 'read', 'update', 'delete'],
},
roles: {
  user: {
    project: ['create', 'read']
  },
  admin: {
    project: ['create', 'read', 'update', 'delete']
  },
  projectManager: {
    project: ['create', 'read', 'update', 'delete'],
    user: ['ban']
  }
}

Then check permissions anywhere in your code:

const canCreate = await auth.api.hasPermission({
  userId: user.id,
  permission: { project: ['create'] }
});

7. Organization Plugin

For multi-tenant applications or team-based features, the organization plugin is a game-changer. It provides:

  • Organizations with teams and members
  • Built-in roles: owner, admin, member
  • Invitation system with email support
  • Fine-grained access control per organization

Invitation Flow

organization: {
  sendInvitationEmail: async ({ email, inviteLink, organization, inviter }) => {
    // Send invitation email
  }
}

Users can be invited as members, admins, or owners. Pending invitations are tracked, and the entire accept/reject flow is handled for you.

Custom Permissions Per Organization

Just like the admin plugin, you can define custom resources:

ac: {
  buzzwordList: ['create', 'read', 'update', 'delete']
},
roles: {
  member: {
    buzzwordList: ['read']
  },
  owner: {
    buzzwordList: ['create', 'read', 'update', 'delete']
  }
}

Each organization gets a unique ID that you can attach to your own resources, enabling proper data isolation between tenants.

8. Extensive Plugin Ecosystem

Beyond the core plugins, Better Auth offers solutions for virtually every auth-related need:

Monetization

  • Stripe Plugin: Customer management, subscription handling, webhook integration
  • Polar Plugin: Alternative payment provider support

Lock features behind subscription tiers:

organization.allowUserToCreateOrganization({
  userId: user.id,
  plans: ['pro', 'enterprise']
});

Security & Access

  • Two-Factor Authentication: TOTP support for auth apps
  • API Key Plugin: Generate keys with rate limiting, expiration, and request quotas
  • Rate Limiting: Built-in protection for auth endpoints
  • CAPTCHA: Bot protection for your forms

Developer Experience

  • OpenAPI Documentation: Auto-generated API docs for all auth endpoints
  • MCP Auth Provider: Use Better Auth as an auth provider for MCP clients
  • Multi-Session: Allow users to be logged in with multiple accounts simultaneously

Conclusion

Better Auth delivers everything you'd expect from a premium hosted auth service, but it's free, open source, and keeps you in complete control of your data. The hook system makes customization painless, the plugin ecosystem covers virtually every use case, and the consistent API across frameworks means you only have to learn it once.

After years of wrestling with auth implementations, Better Auth is the solution I wish I'd had from the start. No monthly fees, no user limits, no vendor lock-in—just a robust, flexible auth system that truly lives up to its name.

If you're starting a new JavaScript or TypeScript project, give Better Auth a try. You might never go back to anything else.