Better Auth
Better Auth email verification, password reset and magic link emails sent through the Postal API.
Better Auth leaves email sending up to you: just call Postal's POST /emails in its callbacks. There is no connection to create.
#Key and sender
Create a sending-only key under API keys and store it in POSTAL_API_KEY. The sender must belong to a verified domain.
// lib/postal.ts
export async function sendWithPostal(to: string, subject: string, html: string) {
const response = await fetch('https://api.postal.so/emails', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.POSTAL_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
from: 'Acme Store <[email protected]>',
to: [to],
subject,
html,
}),
})
if (!response.ok) {
throw new Error(`Postal responded with ${response.status}: ${await response.text()}`)
}
}
#Verification, password and magic link
import { betterAuth } from 'better-auth'
import { magicLink } from 'better-auth/plugins'
import { sendWithPostal } from './lib/postal'
export const auth = betterAuth({
emailAndPassword: {
enabled: true,
requireEmailVerification: true,
sendResetPassword: async ({ user, url }) => {
await sendWithPostal(user.email, 'Reset your password', `<p><a href="${url}">Create a new password</a></p>`)
},
},
emailVerification: {
sendOnSignUp: true,
sendVerificationEmail: async ({ user, url }) => {
await sendWithPostal(user.email, 'Confirm your email', `<p><a href="${url}">Confirm email</a></p>`)
},
},
plugins: [
magicLink({
sendMagicLink: async ({ email, url }) => {
await sendWithPostal(email, 'Your sign-in link', `<p><a href="${url}">Sign in</a></p>`)
},
}),
],
})
#With Postal templates
To edit the emails in the dashboard instead of in code, create a template with the {{{URL}}} variable and send with template instead of html:
{
from: 'Acme Store <[email protected]>',
to: [user.email],
template: { id: 'confirm-email', variables: { URL: url } },
}
To also bring sign-ups into your audience and trigger automations, call POST /events in Better Auth's databaseHooks.user.create.after, with the user ID in external_id. See the Integrate your app guide.