Repository navigation
Adapter framework strategy: extract framework-agnostic client core (deferred) #64
Description
Summary
Extract a framework-agnostic client core so new SDKs (Vue, Angular, and similar) are thin bindings over one shared implementation instead of parallel reimplementations.
This issue has been re-scoped after review. The actionable near-term pieces were split into #76, #77, and #78. What remains here is the deferred extraction itself.
Key correction: the client is not the hard part
The obvious assumption is that the shared core is the HTTP client. It is not. These modules already have zero React imports and are effectively extraction-ready today:
src/client/createSeamlessAuthClient.tssrc/client/webauthnPrf.tssrc/client/webauthnSupport.tssrc/fetchWithAuth.tssrc/scopedRoles.tssrc/types.ts
The part that will actually be duplicated is the session state machine in src/AuthProvider.tsx: loading state, user, credentials, organizations, active organization, step-up status, validateToken, logout-on-401, markSignedIn, and refresh-after-mutation. That logic is React-only today. If a Vue composable and an Angular service each reimplement it, they will drift, and they will drift on auth state, which is the worst place for divergence.
So the core should expose a framework-agnostic session store (getState, subscribe, and actions), not just a fetch client. Each adapter then becomes a thin binding:
- React:
useSyncExternalStore - Vue: a composable or Pinia store
- Angular: an injectable service exposing an observable
That reframes this work from moving files to extracting a state machine, which is the part that needs real design.
Naming constraints
@seamless-auth/coreis already taken by the server monorepo (seamless-auth-server/packages/core), so it is not available for a client core.@seamless-auth/typesalready exists as a separate package that this repo does not currently consume. Decide how it relates before introducing a third naming lane.- Suggested name:
@seamless-auth/client.
Prerequisites
- Enforce framework-agnostic boundary in the client layer #76: enforce the framework-agnostic boundary by lint, so the extraction is a mechanical move.
- [Feature]: Improve headless client ergonomics for custom UI consumers #24: settle the return-shape convention (some client methods return raw
Response, others return parsed result objects). With one SDK this is a wart. With four, each adapter bakes in its own error-handling convention and they will disagree. Settle this before the extraction, not after.
Timing
Do this as step one of the first non-React adapter (Vue or Angular), not as a speculative refactor. Extracting earlier costs a repo restructure, dual publishing, changesets reconfiguration, and a consumer migration, in exchange for benefits that cannot be collected until a second SDK exists.
Next.js (#78) explicitly does not depend on this work, because Next.js is React and needs SSR safety rather than a new binding.
Cross-repo note
The conformance harness in seamless-cli takes per-repo refs (react-ref, templates-ref, and similar). Each new adapter will need a ref and a lane there, so the harness needs generalizing as adapters are added. That work lives in seamless-cli, not this repo.
Related
- Enforce framework-agnostic boundary in the client layer #76 boundary enforcement (prerequisite, do now)
- isPlatformAuthenticatorAvailable throws during server-side rendering #77 SSR guard fix
- Add Next.js support #78 Next.js support (independent of this work)
- [Feature]: Improve headless client ergonomics for custom UI consumers #24 headless client ergonomics (prerequisite)
- [Feature]: Expand custom UI docs and examples #25 custom UI docs and examples
Activity
[-]Adapter framework strategy: extract framework-agnostic client-core[/-][+]Adapter framework strategy: extract framework-agnostic client core (deferred)[/+]Bccorb commentedon Jul 19, 2026
Re-scoped after a design review. The near-term actionable pieces are split out so this issue is not perpetually blocked on the large version:
windowaccess that blocks SSRThe substantive change to this issue: the shared core is not primarily the HTTP client, which is already extraction-ready. It is the session state machine currently living in
AuthProvider. The core should expose a framework-agnostic session store so Vue and Angular bindings cannot drift on auth state. Also noting that@seamless-auth/coreis already taken by the server monorepo, so a client core needs a different name.Bccorb commentedon Jul 20, 2026
Scan update, current as of the 0.5.0 release candidate. Two of the three prerequisites this issue named are now met:
src/client,src/fetchWithAuth.ts,src/scopedRoles.ts, orsrc/types.tsimports React or the router.SeamlessAuthResult, so adapters cannot each invent their own error handling.What still blocks the extraction is unchanged and is the part that actually needs design: the session state machine in
AuthProvideris still React-only, and that is what Vue and Angular bindings would otherwise each reimplement and drift on.One newly surfaced dependency: #92 asks whether provider helpers should throw or return results. That answer propagates directly into whatever the extracted bindings expose, so it is worth settling before this work starts rather than during it.
Bccorb commentedon Jul 29, 2026
Decisions recorded, and phase 1 is open as #114.
What is actually being extracted
Confirming the re-scoped framing in the comment above: the HTTP client is not the hard part.
src/client/*,src/fetchWithAuth.ts,src/scopedRoles.ts, andsrc/types.tsare already React-free and lint-enforced. The part a Vue composable and an Angular service would each reimplement, and drift on, is the session state machine inAuthProvider. That is what phase 1 moves.Both prerequisites named earlier are settled and this design inherits them: #24 fixed the client return shape, and #92 is closed with every provider helper returning a result. Nothing in the store throws.
Decisions
src/session/, unexported.@seamless-auth/clientgets published only once a second binding exists to validate the API. An API extracted against one consumer is a guess, and the second binding is what proves it. This also keeps Add Next.js support #78 unblocked.validateTokenused to calllogout()on any error, so a plain anonymous page load 401d on/users/meand then firedDELETE /logoutfor a session that does not exist. The store separatesclearSession()(local) fromlogout()(network) and uses the former on a failed validation.@seamless-auth/types, which is the consolidation effort already underway across the other repos. No dependency is added here until that lands, so this issue does not block on it.hasSignedInBeforegoes behind aSessionStoragePort, replacing theusePreviousSignInhook. Browser, memory, and default implementations. The memory fallback is what makes the store safe to create during server-side rendering, which matters for Add Next.js support #78.Behaviors the extraction had to carry over
Listing these because they are the parts that are easy to lose in a port, and they are now pinned by tests:
finally, so a server-side failure still leaves the UI signed outupdateCredentialreturns the credential itself, not the response wrapperrefreshStepUpStatusclears the status tonull, while a failed verification leaves it untouchedmarkSignedInmoved from auseEffectonuser && isAuthenticatedinto the authenticated transition, so no binding has to remember to run itTwo latent problems surfaced once the state left React and are fixed in #114: a slow session read could overwrite a newer one (
switchOrganization,finishOAuthLogin, andhandlePasskeyLoginall trigger a refresh, and React's batching hid the race), and snapshot references now stay stable souseSyncExternalStorecannot loop.Phasing
tests/authProvider.test.tsxis the contract.tests/authSession.test.ts,tests/sessionStorage.test.ts, and a node-environment SSR test).@seamless-auth/client, when a second binding starts. That is packaging and CI work, not design.This issue stays open for phase 3.
refactor(session): move session state into a framework-agnostic store
chore: split the client core into @seamless-auth/client as an npm wor…
Bccorb commentedon Sep 13, 2026
Shipped in #149, with the second binding in #151.
@seamless-auth/client0.1.0 holds the framework-free client and the session store (createAuthSession:getState,subscribe, actions),@seamless-auth/react0.13.0 is auseSyncExternalStorebinding over it, and@seamless-auth/react-native0.1.0 is the second adapter, built the way this issue describes. A Vue or Angular adapter is now a binding over the same store.