Docs
Quickstarts
Sign users in from a single-page app, a React Native app, or verify tokens in a Go backend — the three Latchkey SDKs in a few minutes each.
Three thin SDKs, one per surface: a browser SPA, a React Native app, and the backend that verifies what they send. Each wraps the part that’s easy to get subtly wrong — PKCE, rotating refresh tokens, attestation — and nothing else.
Packages: the JavaScript SDKs are landing on npm; until then they ship from the latchkeyid/ui repo (spa/ and native/, zero-dependency, MIT). The Go module is github.com/latchkeyid/latchkey/auth.
Single-page app — @latchkey/spa
PKCE authorize, the callback exchange, claims, and an authFetch that refreshes and retries once on a 401. Refresh tokens rotate — spending one twice signs the user out everywhere — so refresh is single-flight per page and serialized across tabs with Web Locks. That detail is most of why this SDK exists.
import { createLatchkey } from "@latchkey/spa";
const lk = createLatchkey({
issuer: "https://auth.latchkey.id", // or your org's auth domain
clientId: "",
});
// on your /callback route
const { returnTo } = await lk.handleCallback();
router.navigate(returnTo);
// anywhere
if (!lk.signedIn()) lk.signIn({ returnTo: location.pathname });
const res = await lk.authFetch("/api/things");
const who = lk.claims()?.email;
lk.signOut();
-
redirectUridefaults to{origin}/callback— register it on your client. -
Call
lk.authFetch(...)on the object — it bindsthis; destructuring breaks it. -
claims()decodes locally for display. Authorization belongs to your backend, which verifies.
React Native / Expo — @latchkey/native
Phone and email-code sign-in in the two-call shape you know from Supabase — no browser round trip — plus the device-attestation handshake and secure token storage. The SDK carries no native code: you inject storage and (optionally) the attestation SDK call.
import * as SecureStore from "expo-secure-store";
import { createLatchkeyNative, MfaRequiredError } from "@latchkey/native";
const lk = createLatchkeyNative({
issuer: "https://auth.latchkey.id",
clientId: "",
storage: {
getItem: SecureStore.getItemAsync,
setItem: SecureStore.setItemAsync,
removeItem: SecureStore.deleteItemAsync,
},
});
// sign in by SMS code — two calls, like Supabase's signInWithOtp/verifyOtp
const { ticket } = await lk.startPhone("+61400000123");
const claims = await lk.verifyPhone(ticket, codeFromTheTextMessage);
// then, anywhere
const res = await lk.authFetch("https://api.yourproduct.com/things");
startEmailCode/verifyEmailCode are the same pair over email. Refresh is single-flight and rotation-safe, same as the SPA.
Device attestation
If your client requires attestation (Play Integrity / App Attest), inject the platform SDK call — Latchkey handles the challenge round trip:
attest: async challenge => ({
platform: "android",
token: await requestIntegrityToken(challenge), // Play Integrity SDK
}),
MFA: an account armed with two-factor can’t finish a browserless grant. Catch MfaRequiredError and bounce that user to the browser flow.
Using Supabase for data? Keep supabase-js in accessToken mode — accessToken: () => lk.getAccessToken() — and your migration is auth-only.
Backend — github.com/latchkeyid/latchkey/auth
A zero-dependency Go verifier for your product’s gateway: discovery + JWKS (cached, key rotation handled), RS256 verification, and helpers over the token’s ns claims.
import "github.com/latchkeyid/latchkey/auth"
verifier := auth.New("https://auth.latchkey.id")
mux.Handle("/api/", verifier.Middleware(apiHandler))
// in a handler
claims := auth.FromContext(r.Context())
if claims == nil || !claims.Has("grapevine/home-123", "member") {
http.Error(w, "forbidden", http.StatusForbidden)
return
}
if claims.HasRole("grapevine/home-123", "committee") { /* … */ }
-
Has(ns, min)checks the exact namespace (plus the staff wildcard). Levels:viewer<member<owner/admin<staff. -
FromContextisniloutside the middleware — check it. -
Set
auth.WithAudience(...)if you mint audience-scoped tokens.
The line that keeps this fast: authorize from the token’s claims plus your own synced data — never by calling Latchkey per request. Role changes land at the next token refresh; short access-token TTLs (per-client, configurable) are the staleness knob.
Where the pieces live
-
Clients, tenants, roles, teams, grants and API keys as code: the Terraform provider.
-
Custom claims from your own backend at sign-in: auth hooks.
-
Discovery:
https://auth.latchkey.id/.well-known/openid-configuration.