Skip to content
IamAngusUPublic

About

Drop-in, self-hosted magic-link authentication for PHP shared hosting and VPS deployments.

Topics

Resources

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

MagicLink — self-hosted passwordless authentication

Passwordless sign-in without handing over your stack.

Upload one small PHP app. Keep your UI, mail, data and limits.
MagicLink handles the security machinery underneath.

Diese README auf Deutsch lesen

Download the ready-to-upload ZIP PHP 8.2 or newer Shared-hosting and VPS deployment Versioned headless API MIT licensed

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.

MagicLink sign-in screen

The included sign-in screen, captured locally with a development configuration.

Quick start

  1. Download the ready-to-upload ZIP and upload it.
  2. Run php bin/install.php, or copy .env.example to .env and set the three values below.
  3. 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.com

Guided MagicLink installation and doctor check in a terminal

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.

Why MagicLink?

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 }.

MagicLink keeps request state separate from the secret-bearing token exchange

Bring your own UI

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

Make the mail yours

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.html

The 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/.

From shared hosting to VPS

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.php

SQLite 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.

Live public MagicLink CI proof

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.

Security boundary

Before exposing an installation, run:

php bin/doctor.php
php bin/check.php

MagicLink 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.

PHP, JavaScript, HTML, CSS, SQLite, MySQL, Apache and Nginx

MIT licensed. See LICENSE.

About

Drop-in, self-hosted magic-link authentication for PHP shared hosting and VPS deployments.

Topics

Resources

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages