Repository navigation
[Epic] React Native mobile template #40
Description
Tracking issue for shipping a React Native mobile template, and the work it needs across
the other repositories.
What the spike established
A working Expo spike drove the local auth API directly and answered the two questions
that were blocking this. Both came back yes, and neither needed a change to
seamless-auth-api.
- A native client can authenticate with bearer tokens only. Registration returns a
typ=ephemeralJWT that carries the multi step flow; completing it returns an access
JWT plus an opaque refresh token. No endpoint issued aSet-Cookieat any point.
POST /refreshtakes the opaque token and rotates both, and replaying a spent token
returns401 refresh_token_reusedand revokes the whole session chain. - Native passkeys pass the API's WebAuthn verification. Registration and login both
completed on the iOS 26.5 simulator and on an API 36 Google Play emulator, and were
accepted byverifyRegistrationResponseandverifyAuthenticationResponse. The
ORIGINSlist is not URL validated, so theandroid:apk-key-hash:<base64url>origin
format passes through unchanged.
The cookie machinery in @seamless-auth/core turns out to be a browser adapter wrapped
around tokens the API was already issuing, not part of the protocol.
Effect on the estimate
The pre spike estimate assumed a bearer transport had to be built in the server SDK, at
two to four weeks of security critical work. That work already exists. Revised total is
roughly 7 to 11 engineer weeks, of which only about 1 to 1.5 land in this repository.
The auth view rebuild is now the critical path and the least reducible part.
The prerequisite that is not code
Browser WebAuthn treats localhost as a secure context, which is why the web templates
work the moment seamless init finishes. Native passkey APIs have no such exemption on
either platform. Before a passkey works at all, including on a simulator, an adopter must
publish an association file over HTTPS on a domain they control.
- iOS additionally needs an Apple Developer team (a free Personal Team is enough), a
bundle identifier, and anassociatedDomainsentitlement. - Android additionally needs a Google account signed in on the device and a device screen
lock.
The template can shrink this to a checklist. It cannot remove it. Tracked in
#39.
Child issues
Template and packaging, this repository:
- feat(mobile): add an Expo mobile template #36 Add an Expo mobile template
- feat(registry): support a mobile template kind #37 Support a mobile template kind in the registry and validator
- ci: build the mobile template without native toolchains #38 Build the mobile template in CI without native toolchains
- docs(mobile): association file tooling and per platform setup #39 Association file tooling and per platform setup docs
Client SDK, seamless-auth-react. Packaging as a second package in that repository was
chosen so the framework agnostic client core can be shared rather than duplicated, which
is what fells-code/seamless-auth-js#64 already proposes:
- feat(client): extract passkey ceremonies behind a platform port seamless-auth-js#124 Extract passkey ceremonies behind a platform port
- feat(session): async token storage port for native keystores seamless-auth-js#125 Async token storage port for native keystores
- feat(mobile): rebuild the auth views on React Native primitives seamless-auth-js#126 Rebuild the auth views on React Native primitives (critical path, 3 to 5 weeks)
- fix(session): single flight refresh to avoid reuse detection logout seamless-auth-js#127 Single flight refresh to avoid reuse detection logout
- feat(oauth): start the provider redirect through a port seamless-auth-js#128 Start the OAuth provider redirect through a port
Server SDK:
- feat(guards): accept bearer tokens in requireAuth and getSeamlessUser seamless-auth-server#147 Accept bearer tokens in
requireAuthandgetSeamlessUser
Auth API:
- Magic link redirect target is tenant wide, so web and mobile cannot both receive links seamless-auth-api#141 Magic link redirect target is tenant wide
- Dev compose hardcodes RPID and system config re-seeds on boot seamless-auth-api#142 Dev compose hardcodes RPID and system config re-seeds on boot
CLI:
- feat(init): handle the mobile template kind in init and verify seamless-cli#161 Handle the mobile template kind in init and verify
Suggested order
- feat(guards): accept bearer tokens in requireAuth and getSeamlessUser seamless-auth-server#147 and Dev compose hardcodes RPID and system config re-seeds on boot seamless-auth-api#142 first. Both
are small, and the second removes a persistent source of confusion for anyone else
working on passkeys locally. - Stand up a durable relying party domain before further passkey work. The spike used an
ephemeral tunnel, which changes hostname on restart and invalidates every passkey
registered against the old one. - Start feat(mobile): rebuild the auth views on React Native primitives seamless-auth-js#126 early, since it is the critical path.
- Registry, CLI, and template work can proceed in parallel once the kind is agreed.
Still uncertain
- PRF was not exercised.
react-native-passkeysexposes the extension, so
webauthnPrf.tsmay port rather than degrade, but that is unconfirmed. - Magic link deep linking was not exercised, so Magic link redirect target is tenant wide, so web and mobile cannot both receive links seamless-auth-api#141 is the
least certain estimate in the set. - The 7 to 11 week range assumes the view rebuild is like for like. A redesign of the
mobile auth screens moves it.
Activity
Bccorb commentedon Sep 13, 2026
Status as of 2026-09-13: every piece of the plan is open as a PR, and the pilot app exists end to end. The design changed in one respect from the spike: the mobile client reaches auth through the server adapter in a new bearer transport mode (header
x-seamless-auth-transport: bearer, plusPOST /auth/refresh) rather than the auth API directly, because a real deployment (RoxTarget) keeps its auth server private and owns email delivery in the adapter.Template and packaging, this repository:
@seamless-auth/react-native0.1.0 and@seamless-auth/express0.16.0 are published). Expo 57 + expo-router starter,mobilekind in the registry and validator,expo exportas the toolchain-free build,tools/associations/generate.mjsand the README checklist for the association files.Client SDK,
seamless-auth-react:@seamless-auth/client(the framework-free core, per feat(kit): mark the generated sign-in screen with both products #64).authorizedFetchfor the application's own API, which the epic had not listed.@seamless-auth/react-native: provider, hooks, and the native ports (expo-secure-store, react-native-passkeys, expo-web-browser). Headless; fix(api): keep dev container writes out of the bind-mounted source #126 (prebuilt views) is deferred and stays open.Server SDK:
requireAuthandgetSeamlessUser, Express and Fastify)./auth/refresh(#163).Auth API and types:
flow_rate_limitsand pins the Androidapk-key-hashorigin format with a test.docs/api-contract.md.CLI:
--mobile, the optional prompt, config, check, output). fix(verify): pack the client core alongside react for a workspace checkout seamless-cli#203 fixesverifypacking the client workspace. ASEAMLESS_TEMPLATES_REFbump follows the templates release.Docs: fells-code/seamless-auth-docs#43 (React Native SDK and Mobile Passkeys pages, plus the server SDK, secure routes, React SDK, ecosystem, system config and CLI updates).
Pilot:
fells-code/roxtarget-mobile#1 to #6 (stacked drafts) carry every screen of the web app on the new SDK, verified on the iOS simulator against the local stack for sign-in, sign-up, magic links via the URL scheme, onboarding, logging, and every tab. Native passkeys remain to be exercised on the staging domain once fells-code/roxtarget-web#1 (association files) is deployed.Release order once merged: server, react monorepo, types, api, templates (tag), cli (REF bump), then the RoxTarget repos switch their lockfiles to the published versions and leave draft.
Still uncertain, unchanged: PRF on
react-native-passkeysis shipped flagged experimental and unexercised.Bccorb commentedon Sep 13, 2026
Closing status as of 2026-09-13: every PR in the plan is merged, and every package it produced is published except the last CLI release, which is one Version Packages merge away.
Released:
seamless-auth-server:@seamless-auth/core0.16.0,@seamless-auth/express0.16.0,@seamless-auth/fastify0.7.0. feat(guards): accept the auth API's access token as a bearer credential seamless-auth-server#162 landed #147 (bearer access tokens inrequireAuthandgetSeamlessUser), feat(transport): bearer transport on the auth routes and POST /refresh seamless-auth-server#164 landed #163 (bearer transport mode on the auth routes andPOST /auth/refresh).seamless-auth-react:@seamless-auth/client0.1.0,@seamless-auth/react0.13.0,@seamless-auth/react-native0.1.0. chore: split the client core into @seamless-auth/client as an npm workspace seamless-auth-js#149 landed the workspace split and the framework-free client (feat(kit): mark the generated sign-in screen with both products #64); feat(client): platform ports and a bearer transport with token custody seamless-auth-js#150 landed feat(nextjs): confirm cross-device magic links and move to @seamless-auth/nextjs 0.5 #124, chore: move every template to the current Seamless SDKs #125, chore: version packages #127 and chore: move the security contact to the fellscode.com domain #128 plus the bearer transport andauthorizedFetch; feat(react-native): add the headless React Native binding seamless-auth-js#151 landed the headless React Native package. fix(api): keep dev container writes out of the bind-mounted source #126 (prebuilt native views) stays open by design.seamless-auth-types0.22.0 (feat(system-config): add flow_rate_limits and pin the Android origin form seamless-auth-types#68:flow_rate_limits, and the Androidapk-key-hashorigin form pinned by a test).seamless-auth-api0.14.0 (feat(rate-limit): read the per-flow limits from system config seamless-auth-api#310: the six flow limiters readflow_rate_limitsfrom system config;docs/api-contract.mdgains the native client section).seamless-templatesv0.15.0 (feat(mobile): add the Expo starter and the mobile template kind #90: themobilekind, the Expo starter asexpo/ aliasmobile/ beta,expo exportas the toolchain-free CI build,tools/associations/generate.mjsand the README checklist). That is feat(mobile): add an Expo mobile template #36, feat(registry): support a mobile template kind #37, ci: build the mobile template without native toolchains #38 and docs(mobile): association file tooling and per platform setup #39; feat(mobile): add an Expo mobile template #36 is closed, the other three can be closed against feat(mobile): add the Expo starter and the mobile template kind #90.seamless-cli0.16.0 (feat(init): learn the mobile template kind seamless-cli#205 landed #161, fix(verify): pack the client core alongside react for a workspace checkout seamless-cli#203 fixedverifyfor the client workspace). chore: scaffold from seamless-templates v0.15.0 seamless-cli#206 movesSEAMLESS_TEMPLATES_REFto v0.15.0 and is merged; 0.17.0 publishes when chore: version packages seamless-cli#207 merges. Until then the published CLI scaffolds from v0.14.0, which has no mobile kind, so--mobile=expois refused as an unknown option.Pilot: fells-code/roxtarget-api#2 (bearer guards and
/authin bearer mode), fells-code/roxtarget-web#1 (association files under/.well-known/), and fells-code/roxtarget-mobile#1 to #6 are merged. The app carries every screen of the web app on the new SDK and was driven on the iOS simulator against the local stack: sign-up and sign-in with an email code, the magic link (polled, and completed through the app's URL scheme standing in for the universal link), onboarding, logging, every tab, and the sessions list.Verified since the last status:
seamless init --yes --mobile=expoagainst templates v0.15.0 scaffoldsmobile/on@seamless-auth/react-native0.1.0 next to an API starter on@seamless-auth/express0.16.0, withEXPO_PUBLIC_API_URLset andservices.mobilerecorded. The bearer contract through the adapter holds end to end: login, OTP verify,users/me, refresh rotation, and a replayed refresh token answered with401 refresh_token_reusedand the chain revoked. The published@seamless-auth/clientbundle carries the two fixes found on the simulator (aHeadersinstance is flattened before it reaches Expo's fetch, andauthorizedFetchsends the transport header for URLs under the adapter mount).What remains, none of it a code change in this repository:
RPIDandORIGINSset for that domain, and then the register / sign-in / step-up delete checks from the plan.react-native-passkeysis shipped flagged experimental and still unexercised.With fells-code/seamless-cli#207 merged and the open children closed against their PRs (#37, #38, #39; fells-code/seamless-auth-js#64, #124, #125, #127, #128), what is left of this epic is #126 and the device check above.
Bccorb commentedon Oct 6, 2026
Closing. Every child in this repository and the server, API, CLI, and client repositories is merged and released, and the Expo starter shipped in v0.15.0.
The two items left open in the last status are settled:
Still open and unchanged from the last status: PRF on
react-native-passkeysis flagged experimental and unexercised, and session lifetime stays tenant-wide.