Upload one small PHP app. Keep your UI, mail, data and limits.
MagicLink handles the security machinery underneath.
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.
The included sign-in screen, captured locally with a development configuration.
- Download the ready-to-upload ZIP and upload it.
- Run
php bin/install.php, or copy.env.exampleto.envand set the three values below. - Open the domain; MagicLink creates SQLite, its schema and the application key.
Without shell access, set:
APP_URL=https://login.example.com
MAIL_FROM_ADDRESS=no-reply@example.com
MAGICLINK_ALLOWED_EMAILS=you@example.comPoint 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 foreign link silently changes the account | A different browser or account switch must explicitly confirm the actual identity before authentication. Polling alone never signs in. |
| 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.
Independent security review wanted. No external audit has been completed. Review scope and private reporting.
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.
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.
MIT licensed. See LICENSE.
