Magic Link
Passwordless email sign-in with one-time links.
The magic link plugin emails the user a signed, short-lived URL. When they open it, Kernia consumes the token, creates or links the account, and issues a session — no password required.
Installation
Add the plugin to your server
Pass magic_link() to your Kernia config and supply a send_magic_link
callback. The callback receives the recipient, the verification URL, and the
raw token — send the email however you like (Resend, SES, Postmark, SMTP).
from kernia import KerniaOptions
from kernia.auth import init
from kernia.plugins.magic_link import magic_link
async def send_magic_link(email: str, url: str, token: str) -> None:
await email_client.send(
to=email,
subject="Sign in to Acme",
html=f'<a href="{url}">Click to sign in</a>',
)
auth = init(KerniaOptions(
database=adapter,
secret=os.environ["KERNIA_SECRET"],
base_url=os.environ["KERNIA_BASE_URL"],
plugins=[magic_link()],
advanced={"magic-link": {"send_magic_link": send_magic_link}},
))Add the client plugin
Kernia speaks the Better Auth wire protocol, so the official JavaScript client
works unchanged. Add the magicLink client plugin:
import { createAuthClient } from "better-auth/client";
import { magicLinkClient } from "better-auth/client/plugins";
export const authClient = createAuthClient({
baseURL: "/api/auth",
plugins: [magicLinkClient()],
});Usage
Send a magic link
await authClient.signIn.magicLink({
email: "user@example.com",
callbackURL: "/dashboard", // where to land after the link is verified
});The user receives the email. When they click the link, the browser hits
/api/auth/magic-link/verify, Kernia sets the session cookie, and redirects to
callbackURL.
Verify manually
If you handle the redirect yourself (e.g. a native-app deep link), verify the token explicitly:
await authClient.magicLink.verify({ query: { token } });Options
Configure under advanced["magic-link"]:
| Option | Type | Default | Description |
|---|---|---|---|
send_magic_link | async (email, url, token) -> None | — | Required. Delivers the link. |
expires_in | int | 300 | Token lifetime in seconds. |
disable_sign_up | bool | False | Reject links for emails with no existing account. |
Schema
The plugin reuses the core verification table — no migration beyond the core
schema is required.
Tokens are single-use and consumed atomically on verify, so a leaked link can't be replayed after the first click.