One account for every Archway Point product. This service knows about accounts and nothing else — products verify tokens locally with the public key, so any number of them share one Archway ID with zero coupling and no shared secrets. The universal sign-in page lives at /; send users to /?redirect=<your-page> and read #archway_token off the fragment when they come back.
https://sso.archwaypoint.com (public, TLS)
http://192.168.3.31 (rack LAN — VM 102 "archway-sso")
Client ── email+password ──────────▶ Archway ID ── issues Ed25519 JWT (7d)
│ │
└── Authorization: Bearer <jwt> ──▶ Your product's server
verifies LOCALLY via /auth/jwks.json
Node 22, zero-framework node:http, jose, Postgres (2 tables), one container behind Caddy. The Ed25519 keypair is generated on first boot and persisted in Postgres — tokens survive restarts. Public key at /auth/jwks.json (kid: archway-1). Passwords: scrypt (N=16384, r=8, p=1, 16-byte salt), constant-time compare, stored as s1$salt$hash.
| Endpoint | Body / header | Returns |
|---|---|---|
POST /auth/register | {email, password} (pw ≥ 8) | {token, account} · 409 if email taken |
POST /auth/login | {email, password} | {token, account} · 401 on mismatch |
GET /auth/me | Authorization: Bearer <jwt> | {account: {id, email}} |
POST /auth/change-password | {old, new} + Bearer | {ok: true} |
GET /auth/jwks.json | — | {keys: [publicJwk]} |
GET /auth/health | — | {ok, service: "archway-id"} |
Rate limit: 10 register/login attempts per IP per minute (429). Emails lower-cased, unique. Bodies capped at 10 KB.
JWT, alg: EdDSA, kid: archway-1. Claims: sub = account UUID (key your product's data on this, never on email), email, iss: "archway-id", iat, exp (7 days). No refresh tokens in v1 — clients re-login on expiry.
Every product can skin sign-in however it likes: the auth endpoints are plain JSON with permissive CORS, so your page calls them directly — no redirect to the hosted page required. Easiest path is the drop-in SDK served right from this host:
<script src="https://sso.archwaypoint.com/archway-id.js"></script>
<script>
// your own form, your own branding — the SDK is just the wire calls
ArchwayID.login(email, password) // or .register(email, password)
.then(({ account }) => start(account)) // token auto-stored in localStorage
.catch(err => showError(err.error)); // { status, error } — 401/409/429 pre-worded
ArchwayID.me().then(a => a && skipLogin(a)); // already signed in?
fetch("/api/thing", { headers: ArchwayID.authHeader() }); // call YOUR backend
</script>
Also on the SDK: changePassword(oldPw, newPw), token(), signOut(), configure({baseUrl}) for LAN installs, and hostedLoginUrl(returnTo) / consumeRedirectToken() if you'd rather bounce through the hosted page. Prefer raw HTTP? Skip the SDK — the table above is the whole API.
1 · Client: POST /auth/login (or register), keep token, hand it to your backend however you like.
3 · Config: one env var — AUTH_JWKS_URL=. Retry the JWKS fetch at boot so container ordering never matters.
2 · Server — verify locally, never call Archway ID per request:
import { createRemoteJWKSet, jwtVerify } from "jose";
const jwks = createRemoteJWKSet(
new URL(process.env.AUTH_JWKS_URL),
{ cooldownDuration: 60_000 });
const { payload } = await jwtVerify(token, jwks,
{ issuer: "archway-id" });
// payload.sub = the account id
Known v1 gaps, accepted for LAN/beta: no refresh/revocation (7-day blast radius on a leaked token), no email verification or password reset, in-memory per-instance rate limiting, * CORS (safe because auth is header-based — no cookies). Before wide public use: password reset via email, per-account lockout, and key rotation (issue archway-2, serve both keys in the JWKS until old tokens expire).
Reference implementation: src/index.ts (245 lines) in the archway-sso repo · consumers: Prometheus (server/src/verify.ts).