Skip to content

Hosted portal redirects

When a user completes sign-in or sign-up on the hosted portal, the portal hands the browser back to your app. Four inputs decide where, in this order.

Priority Source Where you set it
1 Force redirect Project → Shrines → Redirects
2 redirect_url query parameter the link your app sends the user through
3 Fallback redirect Project → Shrines → Redirects
4 / on the portal itself

The first one present wins. Sign-in and sign-up have separate force and fallback values, so you can land new users on an onboarding flow and returning users on their dashboard.

Which pair applies is decided by the flow the user completed, not by the URL they arrived on. The card switches between sign-in and sign-up in place, so someone who opens /sign-in and follows the Sign up footer link gets the sign-up destination.

  • Force ignores whatever the request asked for. Use it when every user should end up in the same place regardless of where they started.
  • Fallback applies only when there is no usable redirect_url. This is the common choice: deep links keep working, and a bare visit to the portal still goes somewhere useful.
  • With neither set and no redirect_url, the user stays on the portal and sees the account page.

Append it to the portal link, URL-encoded:

const portal = 'https://shrines.yourdomain.com';
const back = 'https://app.yourdomain.com/projects/42';
location.href = `${portal}/sign-in?redirect_url=${encodeURIComponent(back)}`;

It survives an OAuth round-trip: the provider callback lands on a fresh URL that no longer carries the parameter, so the portal re-attaches it across the bounce.

The parameter rides in the query string, which means anyone can put anything in it. Honoring it unchecked would make your sign-in page an open redirect. So the portal honors it only when its origin is one of your environment’s allowed origins, managed under Settings → Allowed origins.

A redirect_url that fails the check is not an error — the portal silently falls through to the fallback, then to /. If your users are landing somewhere unexpected, an origin missing from that list is the first thing to check.

Wildcards work, and match a single label:

Allowed origin https://app.example.com https://preview.example.com https://a.b.example.com
https://app.example.com matches no no
https://*.example.com matches matches no

* never spans a dot, so one wildcard entry cannot quietly cover a whole tree of nested subdomains. Add deeper levels explicitly if you need them.

Sandbox: the session travels in the URL fragment

Section titled “Sandbox: the session travels in the URL fragment”

In production the portal and your app share a registrable domain (shrines.example.com and app.example.com), so the session cookie set by the portal is readable by your app. Nothing needs to be carried.

In sandbox your app is usually on localhost:3000 while the portal is on a Torii-owned host. That cookie is cross-site, and Safari and Firefox block it unconditionally — so without help, a user would sign in successfully and arrive back at your app signed out, bouncing straight to the portal again.

To close that loop, the sandbox portal appends the session to the redirect target in the URL fragment. <ToriiProvider> consumes and clears it on mount, so you do not handle it yourself. Three things to know:

  • It is sandbox-only, by construction. The mechanism keys off a token the server issues for sandbox environments only, so it cannot fire in production.
  • The target must be on your allowed origins. A destination that fails the check still gets the redirect, just without the session — a misconfigured URL degrades to signed-out rather than handing a session to a third party.
  • Fragment-bearing targets are not supported. The provider clears the whole hash on mount, so a redirect target that relies on its own fragment — an anchor, or a hash router (/#/dashboard) — will lose it. Use a path-based route as the sandbox redirect target.

Production is unaffected by all three: the cookie does the work.

Section titled “Removing the cross-site cookie problem entirely”

The sandbox fragment handoff exists because the cookie cannot reach your app. If you would rather it could — in sandbox too — put Torii on your own origin with a proxy. See first-party cookies, which you need before production regardless.