Examples
Run Better Auth email verification, magic-link, and OTP flows through InboxTap with Playwright.Better Auth + Next.js + InboxTap
A minimal Next.js app using Better Auth for email verification, magic-link sign-in, and email OTP — with a Playwright suite that drives every flow through real emails captured by InboxTap.
Prerequisites
- Node.js 20+
npx playwright install chromium(one-time browser download)better-sqlite3compiles a native module on install; if that fails you need a working C++ toolchain (Xcode Command Line Tools on macOS,build-essentialon Debian/Ubuntu)
Setup
npm install
npx playwright install chromiumRun the tests
npm testThe tests start InboxTap and the app themselves — no other terminal needed. Playwright's
webServer config launches npx inboxtap (SMTP on :1025, HTTP API on :8025) and
next dev (:3000), waits for both health URLs, and tears everything down afterwards.
Run it interactively
npx inboxtapThen in a second terminal:
npm run db:migrate
npm run devOpen http://localhost:3000, sign up with any address, and read the captured email:
curl http://localhost:8025/api/emails/latestHow it works
lib/mailer.tsis the single SMTP wiring point: Nodemailer pointed atlocalhost:1025(secure: false,ignoreTLS: true— InboxTap speaks plain SMTP, no auth).- Every Better Auth email callback (
sendVerificationEmail,sendMagicLink,sendVerificationOTP) funnels through that onesendMailhelper. Each email is plain text with exactly one link or one 6-digit code, so extraction is deterministic. - Tests call
inboxTap.createInbox()inside each test. Every inbox gets a unique address, so parallel Playwright workers share one InboxTap server without reading each other's mail — andauth.dbnever needs cleanup between runs. inbox.waitForLink()/inbox.waitForCode()poll the InboxTap API until the email lands, filtered by subject so the assertion targets exactly one message.
To develop against a local InboxTap checkout instead of the npm release, build a tarball from
the repo root (bun run build && bun pm pack) and npm install the generated .tgz here.
Troubleshooting
waitForLink/waitForCodetimes out — check the app logs for SMTP errors and confirm InboxTap is listening:curl http://localhost:8025/health.- Port already in use — something else owns
:1025,:8025, or:3000. Locally the config reuses existing servers, so a stalenext devfrom another project also counts. - Schema errors after upgrading Better Auth — delete
auth.dband rerunnpm run db:migrate; the database is disposable test state.