This project has been sunsetted and is no longer active. Follow what the developer is working on now at tylerberggren.com

Magic Link Auth

Switch your site's login flow to passwordless magic links. Users enter their email, receive a one-time link, and click it to log in — no password required.

Configure login mode

Set loginMode in settings/auth.md:

---
provider: custom

# Login mode: password, magic-link
loginMode: magic-link

loginPage: /login
afterLogin: /account
afterLogout: /
---
Value Behavior
password Email + password form (default)
magic-link Email-only form, one-time login link sent by email

How it works

In magic-link mode, the built-in auth pages call window.__sitemdAuth.requestMagicLink(email). Each provider handles the flow differently using its native passwordless API — no custom backend required for third-party providers.

The user experience is the same across all providers: enter email, receive a link, click to log in.

What changes in your pages

When loginMode: magic-link is set, the built-in auth pages adapt automatically at runtime:

/login — shows an email field and "Send Login Link" button instead of the email+password form. After submission, replaces the form with a "check your email" confirmation.

/upgrade (purchase page) — shows email only, opens Stripe checkout in a new tab. After payment, the Stripe webhook creates the user account and sends a welcome email containing the magic link.

/forgot-password — not linked from the login page in magic link mode (there is no password to reset).

/account/settings — the change password section is not shown.

Email template

Magic link emails use the magic-link template from emails/auth/magic-link.md. Customize it like any other email template:

---
id: magic-link
subject: Your login link
# Variables available: {{loginUrl}}
---

Click to log in — this link expires in 30 minutes:

[Log in]({{loginUrl}})

See Email Templates for full template format and provider setup.

JavaScript API

The auth runtime exposes requestMagicLink on window.__sitemdAuth:

window.__sitemdAuth.requestMagicLink(email)
  .then(() => { /* show "check your email" */ })
  .catch(err => { /* show error */ })

The current loginMode value is available at runtime via window.__sitemdAuthCfg.loginMode. Use it in your own page scripts to conditionally show or hide password-related UI:

var cfg = window.__sitemdAuthCfg || {};
if (cfg.loginMode === 'magic-link') {
  // hide password fields
}

Provider support

Magic link mode works with all auth providers. Each uses its native passwordless API:

Provider How it works Dashboard setup
custom Your API implements /auth/magic-link/request and /auth/magic-link/verify endpoints (see below) —
supabase Calls signInWithOtp — Supabase sends the magic link email Enable "Email OTP" in Authentication → Providers → Email
firebase Calls sendSignInLinkToEmail — Firebase sends the email link Enable "Email/Password" → toggle on "Email link (passwordless sign-in)" in Authentication → Sign-in method
clerk Creates a sign-in with email_link strategy — Clerk sends the link Enabled by default
auth0 Redirects to Auth0 Universal Login with passwordless email Enable the "Email" passwordless connection in Authentication → Passwordless

For third-party providers, the redirect URL / callback URL for your site must be allowed in the provider's dashboard settings.

Custom provider API endpoints

When using provider: custom, your API must implement two endpoints:

POST /auth/magic-link/request

Body: { email: string }

Generates a one-time token, stores it, and sends the email. Always returns { ok: true } regardless of whether the email exists (prevents enumeration).

GET /auth/magic-link/verify?token=<tokenId>

Validates the token, creates a session, deletes the token, and redirects to:

{SITE_URL}/account#magic=<sessionToken>

The auth runtime handles the rest client-side.

The dev server includes a built-in auth stub for the custom provider. Navigate to your login redirect URL with any magic code to log in instantly:

http://localhost:4747/account#magic=anytoken

The stub accepts any exchange code — no real API or email required. See Local Auth Testing for details.