You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
I have not already reached out to Clerk support via email or Discord (if you have, no need to open an issue here)
This issue is not a question, general help request, or anything other than a bug report directly related to Clerk. Please ask questions in our Discord community: https://clerk.com/discord.
We implemented Clerk's documented native→web session handoff (mint a sign-in token on the
backend, open a URL in the app's in-app browser, redeem with signIn.ticket()). It cannot be
made to work as documented. Two distinct defects compound, and the fix for the second one is a
PR from a Clerk engineer that was closed without merging.
We have a working workaround. We are filing this because the workaround depends on undocumented
behaviour that could change under us, and because the documented path is broken for anyone who
tries it.
Environment
@clerk/nextjs
7.5.1 (also reported by others on 7.6.3 and 7.7.4)
clerk-js
6.x, served from our custom FAPI domain
@clerk/expo
4.2.3
Next.js
16, App Router, deployed on Cloudflare Workers via OpenNext
Instance
Production, custom FAPI domain, single session mode
The mobile app signs the user in natively with @clerk/expo. Several screens are web-only
(reports, household sharing/invites, policy documents, account deletion), and the app opens them
in an in-app browser. We want those to open already signed in.
Per the docs, we:
POST /v1/sign_in_tokens with { user_id, expires_in_seconds: 60 }
Open https://app.example.com/sign-in-with-token?token=<TOKEN>&redirect_url=/settings/privacy
On that page, call signIn.ticket({ ticket }) then signIn.finalize({ navigate })
Account deletion in particular is an App Store Review Guideline 5.1.1(v) requirement, so this
is not a nice-to-have — a reviewer meeting a sign-in wall is a rejection.
Defect 1 — session_exists makes the documented example a silent no-op
The in-app browser usually already holds a Clerk session (on iOS, one our own app created during
a previous handoff — SFSafariViewController has a persistent per-app store). In single-session
mode, signIn.ticket() then fails:
session_exists — "Session already exists"
"You're currently in single session mode. You can only be signed into one account at a time."
The official example does not handle this. From docs/guides/development/custom-flows/authentication/embedded-email-links.mdx:
if(!signInToken||user||loading){return// <-- silently does nothing when a session exists}
So the documented flow's behaviour in our scenario is "quietly do nothing," which is
indistinguishable from a bug in the caller.
setActive({ session: null }) does not clear it. Reading clerk-js (core/clerk.ts), #touchCurrentSession early-returns when the session is null and client.sessions is never
mutated — it issues no FAPI request at all. The Frontend API still sees the session. This is not
obvious from the API surface, and "set the active session to nothing" is exactly what a reader
would reach for.
Only client.removeSessions() / signOut() actually issue DELETE /v1/client/sessions.
Prior report:clerk/javascript#8044 —
same defect, filed in March of this year, closed by a staleness bot with zero maintainer
replies, and re-reported by two more users on 2026-07-30 against @clerk/nextjs@7.6.3. It is
not fixed in any released version.
Defect 2 — signOut() triggers a Safari hard reload that respends the single-use token
Working around Defect 1 by signing out first produces the next failure:
sign_in_token_already_used_code — "Sign in token has already been used."
This reproduces on iOS and not on Android, with identical code.
The mechanism is documented in Clerk's own PR #7873 by manovotny:
clerk-js calls onBeforeSetActive() during sign-out deliberately without the 'sign-out' intent (there is a comment explaining why).
@clerk/nextjs skips its invalidateCacheAction() server action only when that intent is 'sign-out' — so for signOut() the action fires.
invalidateCacheAction() calls cookies().delete() inside a server action, which in
Next.js 15+ re-renders the current page's RSC tree as part of the response.
In Safari that RSC delivery fails (TypeError: Load failed), and Next.js falls back to a
hard browser navigation.
The reload restores the URL from Next's internal router state — token included — and the
page redeems the already-consumed token.
That PR proposes the fix (drop the && intent === 'sign-out' condition). It is closed and
never merged; the condition is still present in main today.
Note this also means history.replaceState is not a sufficient mitigation, because Next's
router state is not updated by it — as the PR itself points out.
Possibly related and still open: clerk/javascript#9405 (invalidateCacheAction
firing on Next 16 + @clerk/nextjs 7.5.x–7.7.x, different symptom, same machinery).
What we shipped, in case it's useful to others
// 1. Guard must survive a full page RELOAD — not a re-render, not a remount.// A useRef or module-scope flag is reborn in the new document.constattemptKey=`ticket_attempt:${token.slice(-24)}`;constalreadyAttempted=sessionStorage.getItem(attemptKey)!==null;sessionStorage.setItem(attemptKey,"1");// 2. If a previous document already spent it, don't redeem — recover.if(alreadyAttempted){constlive=clerk.client?.signedInSessions?.[0];if(live){awaitclerk.setActive({session: live.id,navigate: ... });return;}}// 3. Skip redemption entirely when the browser already holds the right user.constexisting=clerk.client?.signedInSessions??[];constmine=existing.find(s=>s.user?.id===expectedUserId);if(mine){awaitclerk.setActive({session: mine.id,navigate: ... });return;}// 4. Otherwise clear server-side. removeSessions(), NOT signOut() — signOut()// routes through onBeforeSetActive and triggers Defect 2.if(existing.length>0)awaitclerk.client.removeSessions();const{ error }=awaitsignIn.ticket({ticket: token});
Two things worth calling out for the docs:
clerk.client.signedInSessions is the only reliable signal here. useAuth().isSignedIn is seeded from server-rendered state and reads false until clerk-js
has fetched /v1/client, so guarding on it means the clear never runs.
After a successful POST /v1/client/sign_ins the session is attached to clerk.client.signedInSessions, but clerk.session stays null until setActive runs — so if (clerk.session) is the wrong recovery check.
Fix the accept-token docs example. As written it silently no-ops when a session already
exists. It should show the existing-session path — that is the common case in any embedded
browser, not an edge case.
Is there a supported way to do native→webview session handoff? We're aware of clerk-docs#2483 ("Clerk does not support
Webviews environments") and clerk/javascript#3880. We also found a
closed, unmerged clerk-ios PR
(#357) adding prepareAuthenticatedWebURL()
backed by POST /v1/client/prepare_webview, which is exactly this use case.
Is /v1/client/prepare_webview live on production FAPI? If so we would use it and delete
all of the above. If not, is a supported handoff on the roadmap?
Would you consider a signIn.ticket() option that replaces an existing session rather
than erroring? Every consumer of this API in an embedded browser has to hand-roll the
sign-out dance, and getting it wrong burns a single-use token.
Reproduction
Any production Clerk instance in single-session mode, Next.js 15/16 App Router:
Sign in normally in Safari so a session exists.
Mint a sign-in token via POST /v1/sign_in_tokens.
Visit /your-accept-page?token=<TOKEN> in the same browser.
Call signIn.ticket({ ticket }) → session_exists.
Call await signOut() first, then signIn.ticket({ ticket }) → in Safari, sign_in_token_already_used_code, because the page reloaded and redeemed twice.
Android/Chrome does not reproduce step 5.
Environment
||||---|---||`@clerk/nextjs`| 7.5.1 (also reported by others on 7.6.3 and 7.7.4) ||`clerk-js`| 6.x, served from our custom FAPI domain ||`@clerk/expo`| 4.2.3 || Next.js | 16, App Router, deployed on Cloudflare Workers via OpenNext || Instance |**Production**, custom FAPI domain, **single session mode**|| Native | Expo SDK 57, `expo-web-browser` → SFSafariViewController (iOS) / Custom Tabs (Android) |
Preliminary Checks
I have reviewed the documentation: https://clerk.com/docs
I have searched for existing issues: https://github.com/clerk/javascript/issues
I have not already reached out to Clerk support via email or Discord (if you have, no need to open an issue here)
This issue is not a question, general help request, or anything other than a bug report directly related to Clerk. Please ask questions in our Discord community: https://clerk.com/discord.
Reproduction
https://github.com/scoobydrew83/worthsync
Publishable key
pk_live_Y2xlcmsud29ydGhzeW5jLmNvbSQ
Description
Summary
We implemented Clerk's documented native→web session handoff (mint a sign-in token on the
backend, open a URL in the app's in-app browser, redeem with
signIn.ticket()). It cannot bemade to work as documented. Two distinct defects compound, and the fix for the second one is a
PR from a Clerk engineer that was closed without merging.
We have a working workaround. We are filing this because the workaround depends on undocumented
behaviour that could change under us, and because the documented path is broken for anyone who
tries it.
Environment
@clerk/nextjsclerk-js@clerk/expoexpo-web-browser→ SFSafariViewController (iOS) / Custom Tabs (Android)What we're trying to do
The mobile app signs the user in natively with
@clerk/expo. Several screens are web-only(reports, household sharing/invites, policy documents, account deletion), and the app opens them
in an in-app browser. We want those to open already signed in.
Per the docs, we:
POST /v1/sign_in_tokenswith{ user_id, expires_in_seconds: 60 }https://app.example.com/sign-in-with-token?token=<TOKEN>&redirect_url=/settings/privacysignIn.ticket({ ticket })thensignIn.finalize({ navigate })Account deletion in particular is an App Store Review Guideline 5.1.1(v) requirement, so this
is not a nice-to-have — a reviewer meeting a sign-in wall is a rejection.
Defect 1 —
session_existsmakes the documented example a silent no-opThe in-app browser usually already holds a Clerk session (on iOS, one our own app created during
a previous handoff — SFSafariViewController has a persistent per-app store). In single-session
mode,
signIn.ticket()then fails:The official example does not handle this. From
docs/guides/development/custom-flows/authentication/embedded-email-links.mdx:So the documented flow's behaviour in our scenario is "quietly do nothing," which is
indistinguishable from a bug in the caller.
setActive({ session: null })does not clear it. Readingclerk-js(core/clerk.ts),#touchCurrentSessionearly-returns when the session is null andclient.sessionsis nevermutated — it issues no FAPI request at all. The Frontend API still sees the session. This is not
obvious from the API surface, and "set the active session to nothing" is exactly what a reader
would reach for.
Only
client.removeSessions()/signOut()actually issueDELETE /v1/client/sessions.Prior report: clerk/javascript#8044 —
same defect, filed in March of this year, closed by a staleness bot with zero maintainer
replies, and re-reported by two more users on 2026-07-30 against
@clerk/nextjs@7.6.3. It isnot fixed in any released version.
Defect 2 —
signOut()triggers a Safari hard reload that respends the single-use tokenWorking around Defect 1 by signing out first produces the next failure:
This reproduces on iOS and not on Android, with identical code.
The mechanism is documented in Clerk's own
PR #7873 by
manovotny:clerk-jscallsonBeforeSetActive()during sign-out deliberately without the'sign-out'intent (there is a comment explaining why).@clerk/nextjsskips itsinvalidateCacheAction()server action only when that intent is'sign-out'— so forsignOut()the action fires.invalidateCacheAction()callscookies().delete()inside a server action, which inNext.js 15+ re-renders the current page's RSC tree as part of the response.
TypeError: Load failed), and Next.js falls back to ahard browser navigation.
page redeems the already-consumed token.
That PR proposes the fix (drop the
&& intent === 'sign-out'condition). It is closed andnever merged; the condition is still present in
maintoday.Note this also means
history.replaceStateis not a sufficient mitigation, because Next'srouter state is not updated by it — as the PR itself points out.
Possibly related and still open:
clerk/javascript#9405 (
invalidateCacheActionfiring on Next 16 +
@clerk/nextjs7.5.x–7.7.x, different symptom, same machinery).What we shipped, in case it's useful to others
Two things worth calling out for the docs:
clerk.client.signedInSessionsis the only reliable signal here.useAuth().isSignedInis seeded from server-rendered state and readsfalseuntilclerk-jshas fetched
/v1/client, so guarding on it means the clear never runs.POST /v1/client/sign_insthe session is attached toclerk.client.signedInSessions, butclerk.sessionstaysnulluntilsetActiveruns — soif (clerk.session)is the wrong recovery check.What we're asking for
Ship PR fix(nextjs): skip invalidateCacheAction on Next.js 15+ for all auth transitions #7873, or an equivalent fix. Right now
signOut()on Safari + Next.js 15/16 cantrigger a hard reload that replays the current URL. That is a general hazard, not specific to
ticket flows — any page holding one-time state in its query string is exposed.
Reopen or comment on signIn.create({ strategy: 'ticket' }) silently fails when user has an active session #8044. It is a real, reproducible defect that was closed by a bot
without a maintainer ever looking at it, and it has since been re-reported against 7.6.3.
Fix the
accept-tokendocs example. As written it silently no-ops when a session alreadyexists. It should show the existing-session path — that is the common case in any embedded
browser, not an edge case.
Is there a supported way to do native→webview session handoff? We're aware of
clerk-docs#2483 ("Clerk does not support
Webviews environments") and
clerk/javascript#3880. We also found a
closed, unmerged
clerk-iosPR(#357) adding
prepareAuthenticatedWebURL()backed by
POST /v1/client/prepare_webview, which is exactly this use case.Is
/v1/client/prepare_webviewlive on production FAPI? If so we would use it and deleteall of the above. If not, is a supported handoff on the roadmap?
Would you consider a
signIn.ticket()option that replaces an existing session ratherthan erroring? Every consumer of this API in an embedded browser has to hand-roll the
sign-out dance, and getting it wrong burns a single-use token.
Reproduction
Any production Clerk instance in single-session mode, Next.js 15/16 App Router:
POST /v1/sign_in_tokens./your-accept-page?token=<TOKEN>in the same browser.signIn.ticket({ ticket })→session_exists.await signOut()first, thensignIn.ticket({ ticket })→ in Safari,sign_in_token_already_used_code, because the page reloaded and redeemed twice.Android/Chrome does not reproduce step 5.
Environment