Skip to content

React Quickstart

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.

Start with a prompt

Hand this to your AI agent to scaffold the Torii setup, then follow the steps below to fill in the details.

Set up Torii authentication in my React app using the @torii-js/torii-react SDK.

1. Install it: npm install @torii-js/torii-react
2. Add my publishable key to .env.local as VITE_TORII_PUBLISHABLE_KEY (I will paste the pk_test_… value from app.toriiauth.eu → Settings → API keys).
3. Wrap the app root in <ToriiProvider publishableKey={import.meta.env.VITE_TORII_PUBLISHABLE_KEY}>.
4. In the main view, render the control components: <AuthLoading> for the loading state, <SignedOut> containing <SignIn />, and <SignedIn> containing <UserDashboard />.
5. Remind me to add my dev origin (e.g. http://localhost:5173) to Allowed origins in the Torii dashboard so browser requests pass CORS.

Follow the reference guide: https://docs.toriiauth.eu/guides/getting-started/quick-start
  1. 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.

  2. Terminal window
    npm install @torii-js/torii-react
  3. Create or open .env.local at the project root and add your publishable key:

    .env.local
    VITE_TORII_PUBLISHABLE_KEY=pk_test_your-publishable-key

    Vite exposes any variable prefixed with VITE_ to client code. Other frameworks have similar conventions: NEXT_PUBLIC_… for Next.js, PUBLIC_… for Astro, etc.

  4. Section titled “Proxy /_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.

    vite.config.ts
    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.

  5. Wrap 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.

    src/main.tsx
    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>,
    );
  6. <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.
    src/App.tsx
    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>
    );
    }
  7. Run the dev server:

    Terminal window
    npm run dev

    Open 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 →

“Failed to fetch” or CORS errors in the console

Section titled ““Failed to fetch” or CORS errors in the console”

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.

The card renders but submit returns 401 Unauthorized

Section titled “The card renders but submit returns 401 Unauthorized”

Almost always one of:

  1. The publishable key doesn’t match the application you set up in the dashboard. Re-copy it from Settings → API keys.
  2. You’re using a pk_live_… key against a sandbox environment, or vice-versa. Use the pk_test_… key during local development.
  3. The user exists but hasn’t verified their email yet. Check Users in the dashboard.

OAuth sign-in succeeds at the provider, then the app loads signed out

Section titled “OAuth sign-in succeeds at the provider, then the app loads signed out”

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:

Terminal window
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 runtime

Section titled “useAuth must be used inside <ToriiProvider> thrown at runtime”

A 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.