Upload one small PHP app. Keep your UI, mail, data and limits.
MagicLink handles the security machinery underneath.
Public CI on a separate account, same maintainer. The badge proves the named commit passed the published jobs; it is not a third-party audit.
Quick start · Why MagicLink? · Your UI · Your mail · Scale · Security · Docs
MagicLink is a small self-hosted authentication service for ordinary PHP shared hosting and VPS deployments. It has no framework or Composer requirement, starts with SQLite and exposes the same versioned API to its built-in screen and yours.
- Download the ready-to-upload ZIP and upload it.
- With shell access, run
php bin/install.phpand answer three questions. - Without a shell, copy
.env.exampleto.envand set:
APP_URL=https://login.example.com
MAIL_FROM_ADDRESS=no-reply@example.com
MAGICLINK_ALLOWED_EMAILS=you@example.com- Open the domain. MagicLink creates SQLite, its schema and the application key.
Point the document root at public/ when possible. The included root fallback
also works on traditional Apache hosting. Read the shared-hosting guide
or VPS guide when the three-minute path is not enough.
The email is the easy part. The surrounding failure modes are the product:
| The problem | What MagicLink does |
|---|---|
| Tokens leak through server logs | The secret stays in the URL fragment and is consumed only by a protected POST. |
| A link signs in the wrong browser | Request state is bound to the initiating browser; only the browser holding the secret receives a session. |
| Responses reveal whether an account exists | Allowed and denied identities follow the same public path, timing budget and state model. |
| Mail fails halfway through a request | An encrypted outbox adds bounded retries, leases, stable message IDs and explicit terminal states. |
| A burst becomes an outage | Global, IP, identity and selector budgets combine with queue backpressure and bounded automatic cleanup. |
| A custom frontend must understand internals | Stable state and batch endpoints return one response envelope: { ok, code, data, error, meta }. |
const api = "https://login.example.com/api/v1";
const config = await fetch(`${api}/config`, { credentials: "include" })
.then(response => response.json());
const request = await fetch(config.data.endpoints.request, {
method: "POST",
credentials: "include",
headers: {
"Content-Type": "application/json",
"X-CSRF-Token": config.data.csrf_token
},
body: JSON.stringify({ email: "you@example.com" })
}).then(response => response.json());Poll GET /api/v1/state?id=… using meta.poll_after_ms, or group owned
requests through POST /api/v1/states. A separate backend can use the optional
confidential-client handoff: exact redirect URI, short-lived code, state, PKCE
and one-time exchange, without sharing either application's session cookie.
API reference · Integration guide
Copy resources/mail-templates to storage/mail-templates, edit the raw
subject.txt, plain.txt and html.html, then set
MAIL_TEMPLATE_DIR=storage/mail-templates.
php bin/mail-preview.php --locale=en --format=html > preview.htmlThe preview creates no token and sends no mail. Only documented placeholders
are accepted, inserted HTML values are escaped and the template directory must
remain local and outside public/.
Shared hosting needs no resident worker: MagicLink attempts one queued message after the response, and a cron job can drain more. On a VPS, keep a worker alive:
php bin/worker.php --loop
php bin/maintain.php --all
php bin/status.phpSQLite is the zero-config default. MySQL adds multi-worker capacity; remote
production connections require a CA, verified server identity and mysqlnd.
Automatic queue, worker, state-batch and maintenance sizes stay bounded and can
all be overridden in .env.
The repeatable local baseline records a 0.515 ms request p50, 0.008 ms state lookup and 0.052 ms batch-of-32 lookup with a 2 MiB peak. Real SMTP, storage and network latency are intentionally measured separately. Method and results.
Before exposing an installation, run:
php bin/doctor.php
php bin/check.phpMagicLink protects passwordless sign-in. It does not replace application authorization, protect a compromised mailbox or make an unsafe hosting account safe. Read the security policy, threat model and operations guide before production use.
The product and language badges come from IamAngusU/Badges.
The clickable stack marquee below always opens IamAngusU/icon-marquee.
MIT licensed. See LICENSE. The current link mark is a placeholder until the final logo is supplied.