@fullstacktechnyc/passwordless-gateway
v1.3.0
Published
Passwordless forward-auth gateway for Caddy / nginx / Traefik. Protect any self-hosted service with passkeys + magic links — zero per-service code.
Maintainers
Readme
@fullstacktechnyc/passwordless-gateway
Passwordless forward-auth for self-hosted services. Runs as a plain Node service; put it behind Caddy / nginx / Traefik and every protected site requires a passkey or magic link — with zero code in the services themselves. A simpler, passwordless-first alternative to Authelia / Authentik.
Sovereign end to end: node:sqlite (or Postgres/D1) for storage, WebCrypto for
auth, jose for the forwarded identity token. No external database, no hosted
auth provider.
How it works
┌─ authenticated ─→ 200 + Remote-User / Remote-Email / X-Auth-Token
Browser → Caddy ─forward_auth→ gateway /verify
└─ not ──────────→ 302 → auth.lab.example.com login → back to where you wereThe session cookie is scoped to the parent domain (.lab.example.com), so
grafana.lab.example.com's forward-auth subrequest carries the cookie set when
you signed in at auth.lab.example.com. One sign-in, every service.
Run it
import { createGateway, defaultLoginScript, serve } from "@fullstacktechnyc/passwordless-gateway";
import { createSqlStorage, sqliteDriver } from "@fullstacktechnyc/passwordless-storage-sql";
import { resendEmail } from "@fullstacktechnyc/passwordless-email";
import { DatabaseSync } from "node:sqlite";
const storage = createSqlStorage(sqliteDriver(new DatabaseSync("./gateway.db")));
await storage.migrate();
const gateway = createGateway({
storage,
email: resendEmail({ apiKey: process.env.RESEND_API_KEY, from: "[email protected]" }),
secret: process.env.AUTH_SECRET,
rpID: "lab.example.com",
rpName: "Homelab",
origin: "https://auth.lab.example.com",
cookieDomain: ".lab.example.com", // shared across protected subdomains
allowedEmails: ["@lab.example.com"], // who may pass
magicLink: { from: "[email protected]" },
jwt: { ttlSeconds: 300 }, // signed identity for backends
loginScript: defaultLoginScript,
});
serve(gateway, { port: 9099, host: "0.0.0.0" });Caddy
auth.lab.example.com { reverse_proxy localhost:9099 }
(passwordless) {
forward_auth localhost:9099 {
uri /verify
copy_headers Remote-User Remote-Email Remote-Name X-Auth-Token
}
}
grafana.lab.example.com {
import passwordless
reverse_proxy localhost:3000
}See examples/homelab-gateway for a complete,
runnable setup.
