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

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.

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.

Quick start

  1. Download the ready-to-upload ZIP and upload it.
  2. With shell access, run php bin/install.php and answer three questions.
  3. Without a shell, copy .env.example to .env and set:
APP_URL=https://login.example.com
MAIL_FROM_ADDRESS=no-reply@example.com
MAGICLINK_ALLOWED_EMAILS=you@example.com
  1. Open the domain. MagicLink creates SQLite, its schema and the application key.

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

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.

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.

The product and language badges come from IamAngusU/Badges. The clickable stack marquee below always opens IamAngusU/icon-marquee.

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

MIT licensed. See LICENSE. The current link mark is a placeholder until the final logo is supplied.

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