React SDK reference
@moth/react is a framework-free client core plus a thin React layer. It
is served from your own instance’s npm registry, so the SDK version always
tracks the server version and nothing is published to npmjs. One line in
.npmrc routes only @moth-scoped packages to your instance; everything
else stays on npmjs:
@moth:registry=https://auth.example.com/npmnpm install @moth/reactThe package is project-agnostic — endpoint and publishable key are passed at runtime. The project’s Setup tab in the admin console renders every snippet below with your real values already filled in.
There are two ways to use it: the React layer (MothProvider / hooks
/ MothLoginScreen) for a batteries-included flow, and the client
core (MothClient) for full control or non-React code. The React layer
is built on the client core; you can mix them.
React layer
Section titled “React layer”MothProvider
Section titled “MothProvider”The top-level wrapper. It owns a MothClient, restores any persisted
session on page load, and gates children behind authentication:
import { MothProvider, MothLoginScreen } from '@moth/react'
createRoot(document.getElementById('root')!).render( <MothProvider config={{ endpoint: 'https://auth.example.com', publishableKey: 'pk_...' }} signedOut={<MothLoginScreen />} > <App /> </MothProvider>,)config— endpoint + publishable key;http://URLs work for local development. Optional fields:locale(overrides the browser language),projectSlug(required for the Google/Apple buttons — the web-redirect OAuth flow needs it, along with the app’s origin — e.g.https://app.example.com— registered in the admin under Providers → “Redirect origins (web)”; exact origin match,http://localhostis accepted for development), andstorage.signedOut— rendered while signed out;<MothLoginScreen />is the built-in flow, or pass your own.
While the session is being restored the provider shows a neutral loading state; it never flashes the login screen for a user who is actually signed in.
useMoth()— the auth state (loading | signedOut | signedIn), the underlyingMothClient, and actions (signOut(),refreshUser(),deleteAccount()). Components re-render on state change.useMothUser()— the currentMothUser, ornullwhen not signed in.MothUsercarriesid,email,emailVerified,displayName, andclaims— the project-assigned custom claims, readable for client-side gating (the server remains the authority).useMothCustomerInfo()/useMothEntitlement('pro')— subscription state; see entitlements & paywall.useMothPush()— Web Push subscription state and actions; see web push.
MothLoginScreen
Section titled “MothLoginScreen”The default sign-in surface: email/password sign-in and sign-up with validation, forgot-password, and — when the project enables them — Google/Apple buttons (Google via Google Identity Services, Apple via the web-redirect flow). It reads the project’s public config to show only the providers that are turned on, and renders the project’s theme and localized copy — see theming & copy.
Entitlements & paywall (Stripe)
Section titled “Entitlements & paywall (Stripe)”The web counterpart of the subscriptions & paywall guide, with Stripe Checkout playing the role the native stores play on mobile. Everything is optional: a project with no Stripe credentials or no products runs the whole auth story with gates never blocking.
// Gate a page behind an entitlement; free users see the paywall.<MothGate entitlement="pro" fallback={<MothPaywallScreen />}> <ProPage /></MothGate>
// Or check imperatively:const { active } = useMothEntitlement('pro')MothPaywallScreen renders the offering from the same admin-configured
paywall as the Flutter one — headline, benefits, tier cards, highlighted
tier — themed and localized by the project config, so Design → Paywall
governs web and mobile alike. Tiers without a Stripe price render as
unavailable on the web.
The purchase flow is a redirect, never a card field:
purchase(product)creates a Stripe Checkout session and navigates to it. On return the SDK re-reads the entitlements (polling briefly to absorb webhook latency) and gates unlock without a manual refresh. The result is a typed value, never an exception:redirect,purchased,pending,alreadyOwned,cancelled, orerror.manageBilling()redirects to the Stripe Billing Portal — cancel, payment methods, invoices.
Entitlement state is cached locally, so gating is instant on load and refreshes in the background; the server-derived state is always the authority.
Web Push
Section titled “Web Push”useMothPush() returns { status, permission, subscribe, unsubscribe }.
subscribe() asks for browser permission, subscribes your service
worker’s PushManager with the project’s VAPID public key (configured in
the admin Push tab), and registers the subscription in the project’s
device registry; sign-out unregisters automatically. Your backend reads
the registry over moth.server.v1.PushService and sends with a Web Push
library — moth never sends. Environments that can’t do push are states,
not errors: no VAPID key → unavailable, no PushManager →
unsupported. The app owns its service worker; the
push notifications guide walks the whole loop,
including a minimal sw.js.
Client core
Section titled “Client core”MothClient is an ergonomic wrapper over the connect-web clients
generated from moth.auth.v1 and moth.billing.v1 — the same protos the
Flutter SDK and the admin console are generated from. Use it directly in
non-React code, tests, or a custom UI.
import { MothClient } from '@moth/react'
const moth = new MothClient({ endpoint: 'https://auth.example.com', publishableKey: 'pk_...',})
await moth.restore() // resume a persisted session, if any
const { user } = await moth.signIn({ email: 'jane@example.com', password: '…' })Methods map one-to-one to the auth API:
| Area | Methods |
|---|---|
| Session | restore(), signIn({email, password}), signUp({email, password, displayName?}), signOut({allDevices?}) |
| Guest | signInAnonymously(), linkPassword({email, password}), linkWithOAuth(...) — see Anonymous users |
| Current user | getMe() / refresh(), updateMe({displayName}), changePassword({current, next}) |
requestEmailVerification(email), requestEmailChange(newEmail) |
|
| Password reset | requestPasswordReset(email) |
| Social | signInWithOAuth(...), exchangeOAuthCode(...), unlinkIdentity(provider) |
| Account | deleteAccount({password}) (fresh re-auth required) |
| Config | getProjectConfig() |
| Billing | getCustomerInfo(), getOfferings(), purchase(product), manageBilling(), createCheckoutSession(...), createBillingPortalSession(...) |
| Push | registerPushDevice(...), unregisterPushDevice(...) (used by useMothPush) |
| Tokens | accessToken() |
The confirmation half of email verification, password reset, and email change is completed from the hosted pages moth emails the user — the app requests them, the link finishes them.
Auth state outside React
Section titled “Auth state outside React”onAuthStateChanged(listener) and onEntitlementsChanged(listener)
subscribe non-React code to the same state the hooks expose; the current
value is replayed to every new listener, and both return an unsubscribe
function. currentState reads it synchronously.
Errors
Section titled “Errors”Every failure is a typed subclass of MothError, mapped from the
server’s gRPC status and stable ErrorInfo reason
(error model):
try { await moth.signIn({ email, password })} catch (e) { if (e instanceof MothInvalidCredentialsError) { // wrong email or password (uniform — never reveals which) } else if (e instanceof MothEmailNotVerifiedError) { // project requires verification before sign-in } else if (e instanceof MothRateLimitedError) { // too many attempts; back off } else if (e instanceof MothNetworkError) { // transport failure, not an auth decision }}Others include MothWeakPasswordError, MothEmailAlreadyExistsError,
MothSignUpClosedError, MothBillingNotConfiguredError, and the
guest-conversion errors MothAnonymousDisabledError /
MothNotAnonymousError / MothOAuthAccountExistsError.
Anonymous users
Section titled “Anonymous users”When the project enables Allow anonymous sign-in
(getProjectConfig().anonymousEnabled), the app can start a guest
session — a real, refresh-backed account with no email or password — and
convert it to a permanent account later, keeping the same user id and all
its data. See the API contract for the server
side and backend gating.
// Reuse a stored guest session instead of calling this every load: each// call mints a NEW guest and abandons the old one.if (moth.currentUser === null) { await moth.signInAnonymously()}
// Later, convert in place — the user id (and everything keyed on it) is kept:await moth.linkPassword({ email: 'jane@example.com', password: '…' })// or with a provider ID token, exactly like signInWithOAuth:await moth.linkWithOAuth({ provider: 'google', idToken, rawNonce })MothUser.isAnonymous reports the state, and the access token carries an
anonymous: true claim so your backend can gate privileged actions on a
permanent account. A guest is unrecoverable once its session is lost
(there is no credential to sign back in with), and conversion never merges
accounts — linkPassword / linkWithOAuth throw
MothEmailAlreadyExistsError or MothOAuthAccountExistsError when the
target already belongs to someone, leaving the guest untouched.
Calling your own backend
Section titled “Calling your own backend”The reason auth exists: your API trusts the app’s requests.
moth.accessToken() always returns a valid, auto-refreshed JWT, and
createMothFetch is a drop-in fetch that attaches it:
const apiFetch = createMothFetch(moth)const resp = await apiFetch('https://api.example.com/todos')Your backend verifies that token offline against the project JWKS — see verifying tokens on your backend.
Sessions & tokens
Section titled “Sessions & tokens”- Persistence — the access token lives in memory; the rotating
refresh token persists via
config.storage:'local'(localStorage, the default — survives restarts),'session'(per-tab),'memory'(nothing survives a reload), or a customTokenStore. - The XSS trade-off is real — any script running on your origin can read web storage; there is no way around this for a pure SPA, and moth documents it rather than papering over it. Server-side rotation-reuse detection limits the blast radius of a stolen refresh token — a replayed token revokes the session — but a strict Content-Security- Policy is your first line of defense.
- Automatic refresh — access tokens refresh proactively before expiry, implemented as a connect-web interceptor with single-flight de-duplication, so concurrent callers share one refresh RPC. A refresh rejected as revoked or reused clears the stored session and emits the signed-out state.
- Version coupling — the SDK version matches the server version by construction: the tarball is built into the binary that serves it.
Theming & copy
Section titled “Theming & copy”Both MothLoginScreen and MothPaywallScreen are driven entirely by the
project config — no hardcoded styles or strings:
- The theme renders as CSS custom properties
(
--moth-*) scoped under the moth components, with light and dark resolved perprefers-color-scheme. - Copy is negotiated per the browser language (override with
config.locale), with the SDK’s bundled locales as offline / first-paint fallback. - Both are cached locally and revalidated in the background, keyed by the server-side revision — an admin edit reaches running apps without a deploy.