@cometalabs/better-auth-passwordless
v0.1.8
Published
Passwordless authentication plugin for Better Auth — OTP codes and magic links via email
Maintainers
Readme
🔐 better-auth-passwordless
better-auth-passwordless is a passwordless authentication plugin for Better Auth. It enables login without passwords by sending users a one-time code (OTP) and/or a magic link via email — just like Slack or Linear do it.
Features
- 🔑 Passwordless login via OTP code or magic link
- 📧 Single email with both an OTP and a clickable deep link
- 🔒 Atomic OTP verification to prevent concurrent reuse
- 🚦 Built-in rate limiting per endpoint
- 🛡️ Configurable max attempts for both OTP and magic link
- 🚫 Optional sign-up disable (existing users only)
- 📦 Full TypeScript support with exported types
- ⚡️ Works seamlessly as a Better Auth plugin
Installation
npm
npm install @cometalabs/better-auth-passwordlesspnpm
pnpm add @cometalabs/better-auth-passwordlessbun
bun add @cometalabs/better-auth-passwordlessRequires
better-auth >= 1.0.0andzod >= 4.0.0
How it works
- User enters their email address
- Plugin creates a signed token and an OTP, stores them in the verification table
- Your
sendEmailfunction is called with both the OTP and a magic link URL - User either:
- Clicks the magic link → instantly authenticated via
GET /passwordless-bundle/verify - Enters the OTP code → authenticated via
POST /passwordless-bundle/verify-otp
- Clicks the magic link → instantly authenticated via
- On success, a session is created and the user is redirected or receives a session token
Usage
Server setup
import { betterAuth } from "better-auth";
import { passwordlessBundle } from "@cometalabs/better-auth-passwordless";
// or from the server subpath:
// import { passwordlessBundle } from "@cometalabs/better-auth-passwordless/server";
export const auth = betterAuth({
plugins: [
passwordlessBundle({
sendEmail: async ({ to, otp, magicLinkUrl, expiresInSeconds, appName }) => {
// Send your email here using Resend, Nodemailer, etc.
await resend.emails.send({
from: "[email protected]",
to,
subject: `Your ${appName} login code`,
html: `
<p>Your login code is: <strong>${otp}</strong></p>
<p>Or <a href="${magicLinkUrl}">click here</a> to log in directly.</p>
<p>Expires in ${expiresInSeconds / 60} minutes.</p>
`,
});
},
}),
],
});Client setup
import { createAuthClient } from "better-auth/client";
import { passwordlessBundleClient } from "@cometalabs/better-auth-passwordless";
// or from the client subpath:
// import { passwordlessBundleClient } from "@cometalabs/better-auth-passwordless/client";
export const authClient = createAuthClient({
plugins: [passwordlessBundleClient()],
});Requesting a login email
await authClient.passwordlessBundle.request({
email: "[email protected]",
callbackURL: "/dashboard", // redirect after magic link login
newUserCallbackURL: "/onboarding", // redirect for first-time users (optional)
errorCallbackURL: "/login?error", // redirect on error (optional)
});Verifying with OTP
await authClient.passwordlessBundle.verifyOtp({
email: "[email protected]",
otp: "123456",
});Configuration options
passwordlessBundle({
sendEmail: async (payload, ctx) => { /* required */ },
expiresInSeconds: 300, // Token/OTP expiry in seconds (default: 300 = 5 min)
otpLength: 6, // Length of the OTP code (default: 6)
allowedAttemptsOtp: 3, // Max OTP verification attempts (default: 3)
allowedAttemptsMagicLink: 1, // Max magic link uses (default: 1)
disableSignUp: false, // Block new user creation (default: false)
rateLimit: {
windowSeconds: 60, // Rate limit window (default: 60s)
max: 5, // Max requests per window (default: 5)
},
})sendEmail payload
| Field | Type | Description |
|-------|------|-------------|
| to | string | Recipient email address |
| otp | string | The one-time code to display |
| magicLinkUrl | string | The full URL for the magic link |
| expiresInSeconds | number | Seconds until both expire |
| appName | string | Value from betterAuth({ appName }) |
| requestMetadata | Record<string, unknown> \| undefined | Extra data passed in the request body |
Endpoints
| Method | Path | Description |
|--------|------|-------------|
| POST | /passwordless-bundle/request | Send OTP + magic link email |
| GET | /passwordless-bundle/verify | Verify magic link token (redirects) |
| POST | /passwordless-bundle/verify-otp | Verify OTP code |
Cometa Labs
better-auth-passwordless is built with ☄️ by Cometa Labs.
Source code: github.com/CometaLabs/better-auth-passwordless
Authors
License
MIT - © 2025 Cometa Labs
