ToriiProvider prop reference
Every prop on <ToriiProvider> including proxyOrigin, with
default-value behaviour.
Open →
When your React app at app.example.com calls the Torii API at
*.torii.so, the session cookie that backs JWT refresh is a
cross-site cookie. Torii sets it SameSite=None; Secure; Partitioned,
so the CHIPS partitioned-cookie scheme keeps it working
cross-site in Chrome, Firefox, and Safari 18.4+. Safari is still the
weak spot:
Partitioned support, so refresh
fails immediately for those users.*.torii.so host.Making the cookie first-party sidesteps all of this: the cookie stops being cross-site, so no browser’s third-party rules apply. There are two ways to do it, and you should pick one before going live:
/_torii/* paths to the Torii
FAPI host on the server side. The cookie lands on your origin
and is first-party. Available on every plan.example.com) and Torii derives the FAPI host torii.example.com,
which you point at us with a DNS CNAME. Torii serves on it directly.
After you verify the domain, regenerate your publishable key; it now
encodes torii.example.com, so the SDK routes there with no
proxyOrigin. The cookie’s Domain matches your app’s eTLD+1, so
it’s first-party to your domain. Custom domains are free on every
plan, production asks for your application domain when you create it.Both work. Proxy is the default: ~5 min to wire up. CNAME has zero runtime cost on your infrastructure (no proxy hop, no SDK config) and is the recommended production setup.
Torii’s session cookie is HttpOnly; Secure; SameSite=None; Partitioned.
SameSite=None is required or a cross-origin XHR wouldn’t include the
cookie at all; Partitioned opts it into CHIPS so browsers store it in a
jar keyed by your top-level site. How each browser treats it:
| Browser | Behaviour |
|---|---|
| Chrome | Honours Partitioned (CHIPS): the cookie persists per top-level site, so refresh keeps working cross-site. |
| Firefox | Total Cookie Protection partitions cross-site cookies per top-level site; the partitioned cookie works. |
| Safari ≥ 18.4 | Supports Partitioned, so it works, unless ITP has classified the cookie’s domain as a tracker, in which case it’s blocked even when partitioned. |
| Safari < 18.4 | Blocks all third-party cookies outright (since Safari 13.1, 2020); no Partitioned support, so refresh fails. |
| Brave | Blocks cross-site cookies by default; treat like old Safari. |
That doesn’t break the first login (you get a JWT in the response
body). The refresh is what breaks: once the cookie is blocked or gone,
/auth/session/refresh returns 401 no_session and <ToriiProvider>
flips the user to signed-out.
Forward /_torii/* paths from your own origin to the Torii FAPI host.
Pass proxyOrigin to <ToriiProvider> so the SDK posts to your origin
instead of the FAPI host directly.
// Vite dev server only; production needs a real reverse proxy (Caddy / nginx / your CDN).import { defineConfig } from 'vite';
export default defineConfig({ server: { proxy: { '/_torii': { target: 'https://your-app-name.torii.so', changeOrigin: false, // KEEP the original Host header; Torii routes by it secure: true, }, }, },});module.exports = { async rewrites() { return [ { source: '/_torii/:path*', destination: 'https://your-app-name.torii.so/_torii/:path*', }, ]; },};app.example.com { handle_path /_torii/* { reverse_proxy https://your-app-name.torii.so { header_up Host {upstream_hostport} } } reverse_proxy your-app-origin}server { listen 443 ssl; server_name app.example.com;
location /_torii/ { proxy_pass https://your-app-name.torii.so; proxy_set_header Host your-app-name.torii.so; }}The critical bit is forwarding the original FAPI host header to the
upstream: Torii resolves the tenant from Host, so the upstream
needs to see your-app-name.torii.so, not your origin.
<ToriiProvider publishableKey={import.meta.env.VITE_TORII_PUBLISHABLE_KEY} proxyOrigin="https://app.example.com"> <App /></ToriiProvider>proxyOrigin must be the absolute origin (scheme://host[:port], no
path, no trailing slash) where your app runs. The SDK validates it
synchronously at mount; invalid values throw with a clear message.
Open your app, sign in, and inspect the network tab. You should see
requests to /_torii/auth/login, /_torii/auth/session/refresh, etc.
under app.example.com, never your-app-name.torii.so. The session
cookie’s Domain attribute should match your app’s domain.
Enter your application domain (example.com) in the dashboard’s
Custom Domain panel; Torii derives the FAPI host torii.example.com.
Add a DNS CNAME record pointing torii.example.com at your
environment’s FAPI host (the target the panel shows). Once Torii verifies
the domain and serves TLS for it, regenerate your publishable key; it now
encodes torii.example.com, so the SDK routes there directly. No
proxyOrigin, no reverse proxy. Torii resolves the tenant from the
request Host, and the session cookie lands first-party to your domain.
In the Torii dashboard, navigate to your environment’s Settings
and find the Custom Domain card. Enter your application domain
(e.g. example.com). Torii derives the FAPI host torii.example.com
and displays the CNAME target you should point it at.
torii.example.com. CNAME <cname-target-from-dashboard>. 3600Once the record propagates, click Verify in the dashboard. Torii
checks the CNAME resolves correctly; the first HTTPS request to
torii.example.com then provisions a TLS certificate via ACME on demand.
After the domain is verified, regenerate your publishable key from
the dashboard (Settings → Publishable keys). The new key encodes
torii.example.com instead of the FAPI subdomain, so the SDK posts to
your domain automatically. Drop it into publishableKey, and remove
proxyOrigin if you had set it:
<ToriiProvider publishableKey={import.meta.env.VITE_TORII_PUBLISHABLE_KEY}> <App /></ToriiProvider>That’s it: no proxyOrigin, no proxy rules. The publishable key is the
only thing that changes. Your old FAPI-subdomain key keeps working until
you revoke it, so cut over first, then revoke it from the same panel.
With a verified custom domain you can let the session work across every
subdomain of your primary domain: sign in on app.example.com and stay
signed in on dashboard.example.com. Enable Cross-subdomain session
sharing in the Custom Domain card; Torii scopes the session cookie to
your registrable domain (example.com) so it’s sent to every subdomain.
If you only want some subdomains to use the session, turn on Restrict
allowed subdomains in the same card and list the exact hostnames
(app.example.com, dashboard.example.com). By default, restriction
off, any subdomain of your primary domain is allowed. Restriction
requires a verified custom domain (it’s the primary domain the
subdomains belong to).
| Proxy | CNAME | |
|---|---|---|
| Setup time | ~5 min | ~30 min + DNS |
| Operational cost | One extra hop | None |
| TLS management | Your CDN / host | Torii (ACME) |
| Works with serverless edge | Yes | Yes |
| Works offline of your origin | No | Yes |
| Best for | SPAs, monoliths | Marketing sites, multi-app accounts |
A common shape: dev + staging use the proxy (everything’s already behind your reverse proxy), production uses CNAME (one less hop and your CDN doesn’t need to know about Torii).
Local dev usually doesn’t need either pattern: http://localhost:5173
is treated as same-site enough by every browser to keep the cookie
working. Sandbox environments automatically allow localhost and
127.0.0.1 on any port, so credentialed cross-origin calls to your
FAPI host work out of the box, there’s nothing to configure. (Web
origins for production environments are derived from your
verified custom domain instead.)
To exercise the proxy pattern locally, e.g. to mirror production before
shipping, add a /_torii proxy rule to your dev server that points at
your environment’s FAPI host (your-app.torii.so, from the
dashboard) using the Vite snippet above, and set
proxyOrigin="http://localhost:5173".
A handful of “browser-like” runtimes send an Origin that can’t be
inferred from a domain: Capacitor (capacitor://localhost), Electron,
or a browser extension (chrome-extension://<id>). For these, register
the exact origins through the Backend API (secret key auth); there is no
dashboard surface for them:
curl -X PUT https://api.torii.so/api/server/v1/allowed-origins \ -H "Authorization: Bearer $TORII_SECRET_KEY" \ -H "Content-Type: application/json" \ -d '{"origins": ["capacitor://localhost", "chrome-extension://abc123"]}'This list is additive to the origins derived from your domains; regular web origins never need an entry here.
You shipped without configuring either pattern. Chrome and Firefox keep the partitioned cookie, so it looks fine in testing, but older Safari blocks the cross-site cookie outright (and current Safari blocks it if ITP flags our domain), so refresh 401s for those users. Pick proxy or CNAME and wire it up.
Refused to set unsafe header "Host" in browser consoleYou’re trying to override the Host header from the browser fetch
call. Don’t: the Host header is fixed by the browser. Move the
override to your server-side proxy (the snippets above).
Check the cookie’s SameSite and Domain attributes in DevTools.
If SameSite=None and Domain doesn’t match your app’s eTLD+1,
the browser is treating it as third-party. Verify your proxy keeps
the original Host header (so Torii’s Set-Cookie lands on your
domain, not the FAPI host).
ToriiProvider prop reference
Every prop on <ToriiProvider> including proxyOrigin, with
default-value behaviour.
Open →