Better Auth integration
Test Better Auth emails with InboxTap
Better Auth gives the application callbacks for producing authentication email; it does not choose an SMTP transport for you. Point the sender used by those callbacks at InboxTap, then drive the real signup, sign-in, verification, and reset endpoints from a browser or integration test.Map each email callback
Email verification uses emailVerification.sendVerificationEmail, password reset uses emailAndPassword.sendResetPassword, and the magic-link and email-OTP plugins expose sendMagicLink and sendVerificationOTP. Route each callback through the same local sender so the test observes the template your application actually constructs.
Keep the generated URL or OTP unchanged. Rebuilding an authentication URL inside the test can hide configuration errors in callback URLs, redirect allowlists, token encoding, and templates.
Send through the local transport
Implement sendLocalEmail with a Nodemailer transport configured for InboxTap: localhost, the selected SMTP port, secure: false, ignoreTLS: true, and no auth field. In a fixture-based test, inject inboxTap.smtp rather than copying a dynamic port.
The snippet returns the delivery promise to make failures deterministic in a local example. Better Auth recommends background dispatch to reduce timing side channels in production. If you follow that production pattern, use the platform's supported background-task mechanism and let InboxTap's bounded wait observe completion.
const auth = betterAuth({
emailVerification: {
sendVerificationEmail: ({ user, url }) =>
sendLocalEmail(user.email, "Verify your email", url),
},
emailAndPassword: {
enabled: true,
sendResetPassword: ({ user, url }) =>
sendLocalEmail(user.email, "Reset your password", url),
},
plugins: [
magicLink({
sendMagicLink: ({ email, url }) =>
sendLocalEmail(email, "Your sign-in link", url),
}),
emailOTP({
sendVerificationOTP: ({ email, otp }) =>
sendLocalEmail(email, "Your verification code", otp),
}),
],
});Test links as secrets
Create a new InboxTap recipient for the test, trigger the Better Auth operation, and wait for a link using a stable subject or path filter. Before navigating, parse the URL and assert the expected origin and callback path without printing its token.
A magic link may create a user by default. Set disableSignUp when the product must allow sign-in only for existing users, and cover both the permitted and rejected cases explicitly.
Match the OTP configuration
Better Auth's email-OTP plugin defaults to six digits, which matches InboxTap's default waitForCode() pattern. If otpLength or generateOTP changes the format, pass a project-specific pattern instead of assuming every provider emits six digits.
For resend tests, wait for the second complete message with afterId set to the first message ID, then extract the new code from that returned message. This distinguishes deliveries and lets the application test prove whether the earlier code was invalidated.
Cover the product contract
Capturing the email is only the midpoint. Continue the flow and assert verified state, session creation, redirect destination, invalid or expired token handling, attempt limits, and password replacement according to the application's configuration.
Avoid asserting secrets in test titles, logs, snapshots, or screenshots. InboxTap's matchers suppress token-bearing values, and its report redaction is best-effort rather than permission to publish unreviewed authentication artifacts.
Run the authentication flows end to end
Follow the complete Next.js and Playwright setup for verification, magic-link sign-in, OTP delivery, and resend behavior.
Read the Better Auth guide