This is the narrowest of the framework guides, on purpose. "Plain JS" means a browser with no framework; this page means no browser at all. Read the boundary section before writing any server-side code against this SDK — it will save you from architecting around something that isn't there.
You cannot construct a Threshold1 instance in your backend and hand it an already-logged-in browser user's JWT to "continue" or "check" their session. There is no constructor option for an existing token, no setter, no way in. Every session-bound method (getUser(), getSession(), addPasskey(), enrollTotp(), and the rest) requires a JWT that was set by that exact instance completing a real auth flow — a login or register call it ran itself, in its own process lifetime. A fresh instance, no matter how it's configured, starts with no session and has no way to be given one from outside.
This is an architectural boundary, not a missing feature. If you need to verify a user's session from your own backend — confirm a request really came from a logged-in user, look up who they are — do that by verifying their JWT directly against threshold1's API. Don't try to reconstruct an SDK instance around a token you got from somewhere else; that path doesn't exist, and nothing about this SDK's design is trying to grow one.
Node usage of this SDK is for processes that complete a full auth flow themselves, for their own purposes, within one process lifetime — not for relaying or continuing someone else's browser session. Real examples: a CLI tool that verifies a user by email before running a privileged command, a backend job that sends a verification code as part of some other workflow, a support bot that needs to confirm the person messaging it owns the email they claim.
Confirmed directly, no browser shims of any kind (typeof window, typeof document, typeof localStorage all undefined; only fetch present):
import { Threshold1 } from "@threshold1/auth";
const auth = new Threshold1({ apiKey: "th_test_..." });
await auth.sendOtp("user@example.com");
// Real network call — no window/document involved at all. Resolved cleanly.
// ... your process gets the code back some other way (however this
// specific tool collects it) ...
const user = await auth.verifyOtp("user@example.com", "123456");
// Real UserProfile: { id, email, externalUserId }
const fresh = await auth.getUser();
// Works — THIS SAME instance just authenticated itself via verifyOtp()
// above. This is the one and only way getUser() works in Node: call it
// on the instance that did the logging in, in the same process run.
This exact round trip was live-verified end to end against the real hosted API: real email sent, real code retrieved, real session established, getUser() on that same instance returning the real, matching profile.
loginWithMagic()/registerWithMagic()'s send half works the same way — a plain network call, nothing browser-dependent:
await auth.loginWithMagic("user@example.com");
// Resolves OK — queues a real email. auth.isAuthenticated stays false;
// this call was never going to establish a session (see Magic Link),
// so there's nothing further this Node process can do with it. The
// link itself has to be opened in a real browser to complete.
A fresh instance, in a process that hasn't logged in yet, really does fail the way described above. Captured the actual error, not assumed it:
const auth = new Threshold1({ apiKey: "th_test_..." });
// isAuthenticated: false
await auth.getUser();
// throws: "Threshold1 SDK: not authenticated. Call login() or
// register() first, or call resumeSession() on page load to restore
// a magic link session."
That message's own wording — "call login() or register() first" — is the boundary in one sentence: there is no other door in.