Skip to content

feat(expo): simplify native client sync engine - #9956

Draft
mikepitre wants to merge 2 commits into
mike/expo-native-sync-behavior-testsfrom
mike/expo-native-sync-engine
Draft

mikepitre wants to merge 2 commits into
mike/expo-native-sync-behavior-testsfrom
mike/expo-native-sync-engine

Conversation

@mikepitre

Copy link
Copy Markdown
Contributor

Description

Replaces the @clerk/expo JS <-> native client sync engine (nativeClientSync.tsx, nativeClientSyncCoordinator.ts, useNativeClientEvents) with a much smaller one in provider/nativeClientSync.ts. Stacked on #9954, whose behavior suite now runs against this engine.

The server is the source of truth. The two SDKs share only:

  1. Identity: one device token, owned by native storage. clerk-js reads the client JWT from getDeviceToken(), and a rotated token from a response is written with setDeviceToken(token, requestToken), a compare-and-set against the token that request was sent with (recorded per request in createClerkInstance). The effective token is mirrored into the app's tokenCache so it stays usable without native.
  2. A payload-free "client changed" signal: when the JS client fingerprint (client, sessions, active user; nothing token related) changes, JS calls refreshClient(); when native emits clerkNativeClientInvalidated, JS refetches its own client and follows native's active session. Both directions are single-flight with one trailing rerun, and a native pull records the fingerprint it applied so it is not echoed back.

No state is pushed across the bridge, and the source-id tags, suppression counters, generation counters and token rollback are gone. configureNative is local setup only and runs once per publishable key before clerk-js's first request; native keeps its own stored token and only adopts the seed from tokenCache when it has none. If it fails or takes longer than 3s, sync stays off for the session and clerk-js keeps using tokenCache directly, which is also what happens without the native module or with __experimental_disableNativeClientSync.

Other pieces:

  • A handleUnauthenticated guard refetches the client first and only signs out when no signed-in session remains. The updateClient interception still hides clerk-js's transient signed-out emission while another session is activated.
  • Biometric flows use the new idle() and pullFromNative() barriers. idle() rejects with the latest native refresh failure until a later refresh succeeds, and with environment_unavailable after 5s.
  • A behavior change: when native moves to a different client on its own, JS now follows it instead of rejecting a foreign sessionless client and restoring its previous token. Native owns the identity, and the startup case that check guarded against is covered by configureNative keeping native's own token.

New ClerkExpo contract: configureNative(publishableKey, seedDeviceToken), getDeviceToken(), setDeviceToken(token, expected): Promise<boolean>, refreshClient(), and the clerkNativeClientInvalidated event. This PR is JS only. It depends on the native bridge PR (link to follow), which is backed by clerk/clerk-ios#583 and clerk/clerk-android#964.

Checklist

  • pnpm test runs as expected.
  • pnpm build runs as expected.
  • (If applicable) JSDoc comments have been added or updated for any package exports
  • (If applicable) Documentation has been updated

Type of change

  • 🐛 Bug fix
  • 🌟 New feature
  • 🔨 Breaking change
  • 📖 Refactoring / dependency upgrade / documentation
  • other:

🤖 Generated with Claude Code

@changeset-bot

changeset-bot Bot commented Sep 27, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 1b59ac3

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@clerk/expo Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@vercel

vercel Bot commented Sep 27, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
clerk-js-sandbox Ready Ready Preview Sep 28, 2026 8:26pm UTC
swingset Ready Ready Preview Sep 28, 2026 8:26pm UTC

Request Review

@coderabbitai

coderabbitai Bot commented Sep 27, 2026

Copy link
Copy Markdown
Contributor

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
  auto_review:
    drafts: true

Comment @coderabbitai help to get the list of available commands.

@mikepitre

mikepitre commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor Author

Superseded by #9956 (comment) (re-recorded so sheet and modal animations are captured).

mikepitre and others added 2 commits September 28, 2026 16:09
Replace the JS <-> native client sync engine with one where native storage
owns the single shared device token and each side refetches its own client
on a payload-free "client changed" signal. Codes against the new ClerkExpo
module contract (configureNative, getDeviceToken, setDeviceToken CAS,
refreshClient, clerkNativeClientInvalidated).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Session updatedAt can move on session activity and token refresh, which
would refresh the native client on every token refresh. Profile edits
still reach native through the active user's updatedAt.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@mikepitre

Copy link
Copy Markdown
Contributor Author

Native ↔ JS sync round trip, re-recorded with full-frame-rate simulator capture (simctl io recordVideo) so the sheet and modal transitions are visible. iPhone Air simulator (iOS 27), Expo SDK 57 app built from the current top of the stack (@clerk/expo + @clerk/expo-native-components). JS signs in with a test-mode email code.

  1. JS sign-in → the native UserButton appears, and its account sheet shows the same user (JS → native)
  2. Sign out from the native account sheet → JS signs out (native → JS)
  3. JS sign-in, then JS sign-out → AuthView asks for sign-in again (JS → native)
roundtrip-v3-final.mp4

This branch was successfully deployed

2 active deployments
Preview – swingset — 1b59ac37 Deployed Sep 28, 2026 by vercel[bot]
Preview – clerk-js-sandbox — 1b59ac37 Deployed Sep 28, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant