Skip to content

Magic link sign-in

Included free on every install.

A person asks for a sign-in link, receives it by email, and opens it in the admin console, which redeems it for a session. Each link works once and expires after 10 minutes by default. An account with two-factor authentication is still asked for its code, and a link issued for one tenant never signs anyone into another.

StepWhat happens
RequestThe console posts the address to /api/admin/auth/magic-link/request. The answer is the same whether or not the account exists.
EmailThe link <LYEVE_CONSOLE_URL>/auth/magic-link/verify?token=<token> is mailed, stating how long it works.
RedeemThe person submits the page the link opens, and the console redeems the token for a session. A mail scanner that only opens the link does not use it up.
Second factorAn account with MFA gets a challenge and is asked for its code on the same page.

Magic link sign-in runs on every install. It needs two things to work end to end:

  • A way to send email. With the email feature configured, links go through it as the tenant's magic-link template, whose wording and design are yours to change. See required templates. Otherwise set SMTP_HOST, SMTP_PORT and SMTP_FROM, plus SMTP_USER and SMTP_PASS if your relay needs them.
  • The console's public address. Set LYEVE_CONSOLE_URL to the address people open the admin console at. In production it is required and must use https, and magic link sign-in refuses to start without it. Outside production it defaults to http://localhost:5173.

In the admin console, the sign-in page links to Email me a sign-in link.

Both routes are public and need Content-Type: application/json.

  1. Ask for a link:

    Terminal window
    curl -X POST http://localhost:3001/api/admin/auth/magic-link/request \
    -H "Content-Type: application/json" \
    -d '{"email": "you@example.com"}'
    { "message": "if the email exists, a magic link has been sent" }
  2. Open the email and copy the token from the link.

  3. Redeem it:

    Terminal window
    curl -X POST http://localhost:3001/api/admin/auth/magic-link/verify \
    -H "Content-Type: application/json" \
    -d '{"token": "<token from the link>"}'
    {
    "access_token": "eyJhbGciOi...",
    "is_new_user": false,
    "user_id": "1c5e2a7b-8f3d-4e9a-b6c0-3d2f1a8e7b45",
    "email": "you@example.com"
    }

    Use access_token as a bearer token on both APIs.

  4. Redeem the same token again. The answer is 400 with invalid or expired magic link.

When the account has MFA, the redeem answer is a challenge:

{ "mfa_required": true, "challenge_token": "eyJ...", "mfa_methods": ["totp"] }

Finish with POST /api/admin/auth/mfa-verify and {"challenge_token": "...", "code": "123456"}. If the instance cannot tell whether the account has a second factor, it answers 503 and issues no session. The link is not used up, so the same link works once the check runs again.

Set MAGIC_LINK_AUTO_CREATE=true and list their roles in MAGIC_LINK_AUTO_CREATE_ROLES, such as editor. The first link an unknown address redeems creates the account, and the answer carries "is_new_user": true. If the list names admin or super_admin, the whole list is dropped and new accounts get no roles.

VariableWhat it doesDefault
LYEVE_CONSOLE_URLThe console address the link points to. Required in production, where it must use https.http://localhost:5173 outside production
MAGIC_LINK_TTL_SECONDSHow long a link works, from 30 to 3600 seconds. A value outside the range is clamped to it.600
MAGIC_LINK_AUTO_CREATEtrue creates an account the first time an unknown address redeems a link. Otherwise unknown addresses are refused.false
MAGIC_LINK_AUTO_CREATE_ROLESComma-separated roles for accounts created that way.none
MAGIC_LINK_ALLOWED_HOSTSComma-separated hostnames links may be built on. A request from a listed host that resolves to the same tenant gets a link on that host, which works only there. Any other request gets a link on LYEVE_CONSOLE_URL.none
MAGIC_LINK_REQUIRE_RESOLVED_TENANTtrue refuses to issue or redeem a link unless the request's hostname resolves a tenant. Turn it on only with domain routing set up.false
MAGIC_LINK_RETENTION_DAYSDays the sign-in history is kept. 0 or less keeps everything.365
MAGIC_LINK_TOKEN_RETENTION_MINUTESMinutes after it was created that a link record is removed, checked every 15 minutes.60
TRUSTED_PROXIESCIDRs whose X-Forwarded-For is trusted for the client address in the history and the per-address limits.none

Magic link sign-in runs on every install. If you set LYEVE_PLUGINS, include magic-link in it.

LimitDefault
magic-link.email3 link requests per 15 minutes per address.
magic-link.request-ip60 link requests per 15 minutes per client address.
magic-link.verify-ip120 redemptions per 15 minutes per client address.
The request route5 per second, burst 10, per client address.
The redeem route10 per second, burst 20, per client address.

A super admin can change the first three under rate limiting. PUBLIC_RATE_LIMITS changes the two per-route limits.

StatusMessageCause
400invalid or expired magic linkThe link was used, expired, or opened on another host or tenant.
403authentication failedThe account is disabled or expired, or unknown while MAGIC_LINK_AUTO_CREATE is off.
429too many magic link requests - try again in 15 minutesOver a request limit.
Every other error
StatusMessage
400invalid JSON body, email is required, invalid email format, token is required
415unsupported media type, expected application/json or multipart/form-data for most body types, or content-type must be application/json for multipart/form-data
429too many magic link attempts - try again later
429rate limit exceeded (a per-route limit)
503authentication is temporarily unavailable. The two-factor check could not run. Try the same link again.
503magic link sign-in is not configured. LYEVE_CONSOLE_URL is not set.