Connect email delivery, the Better Auth client plugin, and BTST auth overrides; verify redirects, registration policy, and the resulting session.

Magic-link sign-in needs three connected pieces: Better Auth must issue and verify the link, its browser client must expose the matching method, and BTST must show the email form. Turning on a UI option alone does not configure email delivery.
This guide extends a working BTST auth integration using @btst/better-auth-ui@2.0.0 and Better Auth 1.6.16. Start with the Better Auth UI integration guide if routes, sessions, and the provider are not connected yet. Keep the compatible dependency group together.
Create a small plugin configuration beside your existing server auth configuration:
import { magicLink } from "better-auth/plugins/magic-link";
type DeliverEmail = (message: {
to: string;
subject: string;
text: string;
}) => Promise<void>;
export function emailSignIn(deliverEmail: DeliverEmail) {
return magicLink({
expiresIn: 300,
disableSignUp: true,
sendMagicLink: async ({ email, url }) => {
await deliverEmail({
to: email,
subject: "Your sign-in link",
text: `Sign in using this link: ${url}`,
});
},
});
}
Add emailSignIn(deliverEmail) to the existing betterAuth({ plugins: [...] }) array. Preserve your database, other plugins, secret, trusted origins, and auth handler. deliverEmail is your server email integration; make it reject when the provider reports failure, including providers that return an error object without throwing. The example uses a plain-text email so a URL does not need HTML-attribute escaping.
This configuration deliberately allows existing accounts only. Set disableSignUp: false if email-link registration is part of your product. Hiding a sign-up link in BTST does not enforce that policy on the server. Better Auth's magic-link documentation describes the delivery callback, expiration, and registration setting.
Use the verification URL that Better Auth supplies. It contains the token and points at the auth API; replacing it with the UI form URL discards the verification step. Keep token-bearing URLs out of application logs and analytics. Retain the configured origin checks and rate limits, and verify production email delivery with a test account before making this the only login method.
Add magicLinkClient() to the plugins of the same auth client that your BTST provider uses:
import { createAuthClient } from "better-auth/react";
import { magicLinkClient } from "better-auth/client/plugins";
export const authClient = createAuthClient({
plugins: [magicLinkClient()],
});
This is the minimal client example. In an existing application, preserve other client plugins and any configured API origin or base path. Creating an unrelated second client can leave the rendered form using a client without signIn.magicLink.
The server plugin and client plugin have different jobs. One owns token creation and verification; the other makes the browser method available. The released BTST form calls authClient.signIn.magicLink and handles pending, success, and error states.
Merge these fields into your existing StackProvider's overrides.auth object:
{
authClient,
magicLink: true,
redirectTo: "/p/account/settings",
onSessionChange: () => router.refresh(),
}
This is a configuration fragment inside an existing Next.js client layout, where router comes from useRouter. Keep the provider's authorization adapter, identity handling, and other overrides. Use an existing route in your application for redirectTo.
With a site mount of /p, the default email form is /p/auth/magic-link. The released route definition supplies the route; its magicLink override enables the UI feature. A different site mount changes the public URL.
Keep the companion's callback route and navigation configuration intact. The form can route verification through its callback page when client persistence is enabled. Avoid constructing a competing callback flow until the built-in flow works. For copy and layout changes, use the per-page customization guide.
| Step | Expected result | Where to investigate a failure |
|---|---|---|
| Submit the email form | The auth API accepts the request | Client plugin, endpoint mount, origin checks, rate limit |
| Deliver the message | A usable message reaches the test inbox | Server callback and email-provider result |
| Open the supplied link | Better Auth verifies the token and establishes a session | Expiry, prior token use, configured API/site origins |
| Return to the app | The intended page shows the signed-in state | Callback route, provider session refresh, server identity |
| Request protected data | The backend applies the account's permissions | Authorization contract and request credentials |
A successful delivery toast does not establish a session. A session does not grant access to every plugin operation. Test an existing account, an unknown email under your chosen registration policy, an expired link, reuse of a redeemed link, and a protected page after sign-in. Use controlled test accounts and inspect provider results without recording tokens.
The snippets here were checked against the stated package versions. They do not constitute an end-to-end email-provider or browser-session test. Your deployment's email delivery, redirects, and authorization need that final exercise.
Continue with the Better Auth UI documentation for the complete provider setup and BTST installation for the framework routes.