Components
Full prop reference for <SignIn>, <SignUp>, <UserButton>,
<UserDashboard>, and <UserProfile>.
Browse →
Add Torii authentication to a React app. This guide uses Vite; the same steps work for any React framework; only the environment-variable plumbing changes.
Sign up at app.toriiauth.eu and click New application.
Name it after your app. From Settings → API keys, copy the publishable
key; it looks like pk_test_… in sandbox and pk_live_… in production.
Under Settings → Allowed origins, add http://localhost:5173 (or whatever
port your dev server uses). Without this the API rejects browser requests
with a CORS error.
@torii-js/torii-reactnpm install @torii-js/torii-reactpnpm add @torii-js/torii-reactyarn add @torii-js/torii-reactCreate or open .env.local at the project root and add your publishable key:
VITE_TORII_PUBLISHABLE_KEY=pk_test_your-publishable-keyVite exposes any variable prefixed with VITE_ to client code. Other
frameworks have similar conventions: NEXT_PUBLIC_… for Next.js,
PUBLIC_… for Astro, etc.
/_torii in your dev server (recommended)Forward the /_torii path from your dev origin to your Frontend API host.
That host is encoded in your publishable key, so decodeFapiUrl derives the
proxy target from the key you already put in .env — nothing extra to look
up, and no second value to keep in sync per environment.
import { decodeFapiUrl } from '@torii-js/torii-react';import react from '@vitejs/plugin-react';import { defineConfig, loadEnv } from 'vite';
export default defineConfig(({ mode }) => { const env = loadEnv(mode, process.cwd(), '');
return { plugins: [react()], server: { proxy: { '/_torii': { target: decodeFapiUrl(env.VITE_TORII_PUBLISHABLE_KEY), // Rewrites Host to the target. Torii resolves your tenant from Host, // so without this every proxied call comes back 403. changeOrigin: true, }, }, }, };});Prefer to pin it by hand? The host is in the dashboard under
Settings → Domains → Primary domain, e.g.
target: 'https://your-app-name.shrines.dev'.
Two things this buys you locally. The session cookie stays first-party
on localhost, so refresh behaves the way it will in production instead of
riding a cross-site cookie. And OAuth sign-in completes: the provider
callback hands the session back through
<your origin>/_torii/auth/session/establish, which only resolves if your
dev server forwards that path.
Skip this and password sign-in still works in Chrome and Firefox. It is ~2 minutes now against a class of bug that is confusing to debug later.
<ToriiProvider> to your appWrap your root with <ToriiProvider> and pass the publishable key. The
provider owns session state, refreshes JWTs in the background, and
exposes the useAuth() hook to the rest of the tree.
proxyOrigin is the other half of step 4: the proxy makes /_torii
resolvable on your origin, proxyOrigin is what makes the SDK use it.
Drop the prop if you skipped the proxy, and set it to a literal origin
string rather than window.location.origin if your app server-renders.
import { ToriiProvider } from '@torii-js/torii-react';import { StrictMode } from 'react';import { createRoot } from 'react-dom/client';import App from './App';
const PUBLISHABLE_KEY = import.meta.env.VITE_TORII_PUBLISHABLE_KEY;
if (!PUBLISHABLE_KEY) { throw new Error('Missing VITE_TORII_PUBLISHABLE_KEY');}
createRoot(document.getElementById('root')!).render( <StrictMode> <ToriiProvider publishableKey={PUBLISHABLE_KEY} proxyOrigin={window.location.origin}> <App /> </ToriiProvider> </StrictMode>,);<AuthLoading>, <SignedOut>, and <SignedIn> are control components;
each renders its children for exactly one phase of the auth lifecycle.
On every page load the provider runs a session probe (the cookie is
checked against the server); these three tags cover all three outcomes:
<AuthLoading>: the probe is still in flight. Render a spinner or
skeleton. Skipping it means signed-in users briefly see the sign-in
card flash on reload before the probe resolves.<SignedOut>: no session. Render <SignIn>
(the prebuilt card). It carries sign-in, sign-up and forgot-password
behind one mount with an internal toggle, so a new reader can register
without a router or any callbacks. It opens on the sign-in form; pass
defaultMode="sign-up" to open on registration instead, or
hideSignUp to keep the surface sign-in-only. If you’d rather give
registration its own route, mount <SignUp>
there separately.<SignedIn>: authenticated. Render <UserDashboard>, the full
account surface. It bundles the <UserButton> avatar menu, an
email-verification banner, and the profile panel: editable profile
fields, connected accounts (account linking), active sessions,
and data-export requests. Drop in <UserButton> on its own instead
if you only want the avatar menu.import { AuthLoading, SignedIn, SignedOut, SignIn, UserDashboard } from '@torii-js/torii-react';
export default function App() { return ( <main> <AuthLoading> <p>Loading…</p> </AuthLoading> <SignedOut> <SignIn /> </SignedOut> <SignedIn> <UserDashboard /> </SignedIn> </main> );}Run the dev server:
npm run devOpen http://localhost:5173; the card renders for signed-out users, on
the sign-in form. Click the Sign up link in its footer to switch the
card to registration, create an account, verify the email link from
Mailpit / your inbox, and you’ll see the <UserDashboard> appear in its place.
Components
Full prop reference for <SignIn>, <SignUp>, <UserButton>,
<UserDashboard>, and <UserProfile>.
Browse →
Customize the theme
Pick a preset (shadcn / mui), override CSS tokens, or attach
per-element classes via appearance.elements.
Theming →
i18n & labels
Ship in en and da out of the box, or override every label.
Labels →
Hooks
useAuth(), useUser(), and useAuthFetch() for reading auth
state and calling your API.
Hooks →
First-party cookies (required)
Before production, wire up a proxy or CNAME so Safari doesn’t silently sign your users out. Set up →
Your dev origin isn’t in the application’s allowed-origins list. Open the
dashboard → Settings → Allowed origins and add the exact URL the
browser sends from (scheme + host + port, no trailing slash).
http://localhost:5173 and http://127.0.0.1:5173 are different; add
the one the browser is actually using.
401 UnauthorizedAlmost always one of:
pk_live_… key against a sandbox environment, or
vice-versa. Use the pk_test_… key during local development.You briefly see a URL like
http://localhost:5173/_torii/auth/session/establish?ticket=… before landing
back on a signed-out page. That means proxyOrigin is set but your dev server
is not actually forwarding /_torii: the callback hands the session back
through that URL, it hits your SPA’s catch-all route instead of Torii, the
one-time ticket is never redeemed, and no session is created. Check the proxy
is live:
curl -i "http://localhost:5173/_torii/client"JSON from Torii means the proxy works. HTML means the request never left your
dev server — recheck server.proxy in vite.config.ts (step 4) and restart
it. A 403 instead means the proxy is forwarding but stripping the Host
header; set changeOrigin: true.
useAuth must be used inside <ToriiProvider> thrown at runtimeA component calling useAuth, useAuthFetch, or any control component
(<SignedIn>, <SignedOut>, <Show>) is rendering outside the
provider. In Next.js this usually means you need a 'use client' wrapper
around <ToriiProvider> imported into the root layout.
If something else is going wrong, mail [email protected] with the error and a code snippet, and we’ll respond inside one business day.