## What users see
Setting up a new project with hosted components, clicking **Sign in**
sometimes throws the browser into a redirect ping-pong between the app
and the hosted components site — anywhere from 5 to 9+ cross-domain
redirects — before the sign-in page finally renders. Reproduced live
against production:

Captured redirect chain from that recording (one line per navigation, ~1
per second):
```
localhost:3000/ ← click "Sign in"
HOSTED /handler/sign-in?...nested_refresh_token_id ← start session handoff
localhost:3000/?redirect_uri=...&state=S1 ← bounce: "prove the session"
HOSTED /handler/sign-in?...&code=... ← code delivered... then RESTART ↩
localhost:3000/?redirect_uri=...&state=S2 ← bounce again (fresh state!)
HOSTED /handler/sign-in?...&code=... ← code delivered... RESTART ↩
localhost:3000/?redirect_uri=...&state=S3 ← again
HOSTED /handler/sign-in?...&code=... ← again
localhost:3000/?redirect_uri=...&state=S4 ← again
HOSTED /handler/sign-in?...&code=... ← exchange finally wins the race
HOSTED /handler/sign-in (clean URL) ← sign-in form renders
```
The designed handshake is only 3 cross-domain redirects. Everything past
that is one bug restarting the chain over and over.
## The bug
When a page on the hosted domain loads with a one-time `code`, the
`StackClientApp` constructor schedules **two** async startup flows
back-to-back:
1. `callOAuthCallback` — which **synchronously strips `code` + `state`
from the URL** (`history.replaceState`) before starting its network
token exchange, and
2. `_maybeHandleNestedCrossDomainAuth` — which has a guard for exactly
this situation ("a real OAuth callback wins"), implemented as *"is
`code`+`state` in the URL?"*
Flow 1 runs first. By the time flow 2 reads `window.location`, the
params it's guarding on are already gone — so it concludes no OAuth
callback is happening, sees the (un-stripped) nested handoff marker, and
bounces back to the app domain to request a *new* code, cancelling the
in-flight exchange:
```mermaid
sequenceDiagram
participant A as Your app (a.com)
participant B as Hosted sign-in (b.com)
A->>B: 1. go to sign-in ("I have session X")
B->>A: 2. "prove it" (state, code_challenge)
A->>B: 3. one-time code for session X
Note over B: callOAuthCallback strips code+state from URL,<br/>starts token exchange (network)
Note over B: nested handler runs next, checks URL for code+state…<br/>already gone → guard defeated ❌
B->>A: 2'. "prove it" AGAIN (fresh state) — exchange cancelled
A->>B: 3'. another one-time code
Note over B: …loop repeats until the exchange happens to<br/>finish before the re-bounce navigation commits
```
Whether each cycle escapes is a coin flip between two competing
navigations (the exchange's success redirect vs. the handler's
re-bounce), which is why the loop count varies run to run and the issue
reproduces so inconsistently.
## The fix
Capture the URL once, at construction time — before anything can mutate
it — and let the nested handler consult that snapshot in addition to the
live URL:
- The constructor now captures `new URL(window.location.href)` when
scheduling the nested-auth resolution and passes it in.
- `_maybeHandleNestedCrossDomainAuth(urlAtConstructionTime?)` stands
down if **either** the live URL **or** the construction-time URL carries
`code` + `state`.
A stripped callback still counts as a callback, so the handler no longer
re-bounces while the exchange is in flight. Every other path is
unchanged: the handler still reads all of its working params from the
live URL (the strip never touches the nested params), hop-1/hop-2 pages
have no `code` in either snapshot, and ordinary social-login callbacks
never had this race (the component-driven flow strips long after the
handler has run).
Note this fix removes the *restarts*. The remaining 3-redirect baseline
for signed-out users is a separate design issue (the analytics-created
anonymous session triggering the handoff at all) and is intentionally
out of scope here.
## Tests
- New: `does not re-bounce nested cross-domain auth after the OAuth
callback consumed code+state from the URL` — pins both guards
(mutation-tested: reverting either fix line fails it).
- New: `passes the construction-time URL to the nested cross-domain auth
handler` — pins the eager capture; fails if the URL is read lazily at
handler run time.
- Full cross-domain suite passes (the `signOut` timeout in that file is
a pre-existing flake on `dev`, reproducible without this change).
<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Fixes a race in nested cross-domain auth that caused repeated redirects
between the app and the hosted sign-in. We now snapshot the URL at
construction so OAuth callbacks are respected even after `code` and
`state` are stripped.
- **Bug Fixes**
- Capture `window.location` at construction and pass it to
`_maybeHandleNestedCrossDomainAuth`.
- Handler stands down if `code` and `state` exist in the live or
captured URL.
- Stops the redirect ping‑pong; the 3‑redirect baseline remains
unchanged.
- Keeps reading nested params from the live URL; no other paths changed.
- Adds tests to pin the race and the construction‑time URL behavior.
<sup>Written for commit
|
||
|---|---|---|
| .agents/skills | ||
| .changeset | ||
| .claude | ||
| .cursor | ||
| .devcontainer | ||
| .github | ||
| .vscode | ||
| apps | ||
| configs | ||
| docker | ||
| docs | ||
| docs-mintlify | ||
| examples | ||
| packages | ||
| patches | ||
| scripts | ||
| sdks | ||
| skills/hexclave | ||
| .dockerignore | ||
| .gitignore | ||
| .gitmodules | ||
| AGENTS.md | ||
| CHANGELOG.md | ||
| CLAUDE.md | ||
| CONTRIBUTING.md | ||
| hexclave.config.ts | ||
| LICENSE | ||
| package.json | ||
| pnpm-lock.yaml | ||
| pnpm-workspace.yaml | ||
| README.md | ||
| skills-lock.json | ||
| turbo.json | ||
| vitest.shared.ts | ||
| vitest.workspace.ts | ||
Hexclave
The user infrastructure platform.
Hexclave handles everything around your users: authentication, teams, payments, emails, analytics, and much more. Start in minutes on the hosted cloud. Your data is always yours to export and self-host.
Get started
Setting up Hexclave is one prompt. Paste this into your coding agent of choice:
Read skill.hexclave.com and help me setup hexclave in this project
For AI agents
If you're an AI coding agent, use your web fetch tool or curl the skill documentation for up-to-date integration instructions:
curl -sSL "https://skill.hexclave.com?question=<your-question>&context=<your-context>"
What's included
Hexclave ships as a catalog of apps you switch on as your product needs them. Each one is built on the same user model, and new apps land regularly.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Contributing
Hexclave is open source, and contributions are welcome. Read
CONTRIBUTING.md to get started, and say hello in
Discord before picking up anything large.
Found a security issue? Email security@hexclave.com.










