client.getCookie() fails on expo #1833

Closed
opened 2026-03-13 09:06:47 -05:00 by GiteaMirror · 3 comments
Owner

Originally created by @ByteBlitz on GitHub (Sep 3, 2025).

Is this suited for github?

  • Yes, this is suited for github

To Reproduce

  1. Create an Expo Project: npx create-expo-app@latest
  2. Follow the BetterAuth integration tutorial for Expo
  3. Drop this into app/index.tsx
// index.tsx
import { authClient } from "@/lib/auth-client";

export default function Index() {
  return {authClient.getCookie()};
}
  1. Run with expo start --web and get an error:
_ExpoSecureStore.default.getValueWithKeySync is not a function. (In '_ExpoSecureStore.default.getValueWithKeySync(key, options)', '_ExpoSecureStore.default.getValueWithKeySync' is undefined)

Current vs. Expected behavior

I expect the cookie to be displayed. Instead, the app crashes.

What version of Better Auth are you using?

1.3.7

System info

System:
    OS: macOS 15.6.1
    CPU: (8) arm64 Apple M1
    Memory: 140.53 MB / 16.00 GB
    Shell: 5.9 - /bin/zsh
  Browsers:
    Chrome: 139.0.7258.155
    Safari: 18.6

Which area(s) are affected? (Select all that apply)

Client

Auth config (if applicable)

Unrelated

Additional context

It appears to be an issue when accessing the secure storage.

Originally created by @ByteBlitz on GitHub (Sep 3, 2025). ### Is this suited for github? - [x] Yes, this is suited for github ### To Reproduce 1. Create an Expo Project: `npx create-expo-app@latest` 2. Follow the BetterAuth integration tutorial for Expo 3. Drop this into app/index.tsx ``` // index.tsx import { authClient } from "@/lib/auth-client"; export default function Index() { return {authClient.getCookie()}; } ``` 3. Run with `expo start --web` and get an error: ``` _ExpoSecureStore.default.getValueWithKeySync is not a function. (In '_ExpoSecureStore.default.getValueWithKeySync(key, options)', '_ExpoSecureStore.default.getValueWithKeySync' is undefined) ``` ### Current vs. Expected behavior I expect the cookie to be displayed. Instead, the app crashes. ### What version of Better Auth are you using? 1.3.7 ### System info ```bash System: OS: macOS 15.6.1 CPU: (8) arm64 Apple M1 Memory: 140.53 MB / 16.00 GB Shell: 5.9 - /bin/zsh Browsers: Chrome: 139.0.7258.155 Safari: 18.6 ``` ### Which area(s) are affected? (Select all that apply) Client ### Auth config (if applicable) ```typescript Unrelated ``` ### Additional context It appears to be an issue when accessing the secure storage.
GiteaMirror added the expo label 2026-03-13 09:06:47 -05:00
Author
Owner

@dosubot[bot] commented on GitHub (Sep 3, 2025):

Hi @ByteBlitz! I'm Dosu and I’m helping the better-auth team.

The error happens because Expo SecureStore's synchronous methods (like getValueWithKeySync) aren't available on web, only on native platforms. The Better Auth Expo client plugin uses SecureStore for storage, but doesn't check for platform or provide a web fallback, so calling authClient.getCookie() on Expo web will crash with this error source.

To work around this, you'll need to use a browser-compatible storage (like localStorage or sessionStorage) when running on web, and only use SecureStore on native. You can implement a platform check in your client setup to select the right storage backend. There isn't an official Better Auth workaround documented yet, but this is a common Expo pattern.

Recent updates to cookie parsing and SecureStore dependencies don't resolve this web compatibility issue source. If you need to retrieve cookies on Expo web, consider writing a custom storage adapter or polyfill for the Expo client plugin.

To reply, just mention @dosu.


How did I do? Good | Irrelevant | Incorrect | Verbose | Hallucination | Report 🐛 | Other  Chat with Dosu Join Discord Share on X

@dosubot[bot] commented on GitHub (Sep 3, 2025): <!-- Greeting --> Hi @ByteBlitz! I'm [Dosu](https://go.dosu.dev/dosubot) and I’m helping the better-auth team. <!-- Answer --> The error happens because Expo SecureStore's synchronous methods (like `getValueWithKeySync`) aren't available on web, only on native platforms. The Better Auth Expo client plugin uses SecureStore for storage, but doesn't check for platform or provide a web fallback, so calling `authClient.getCookie()` on Expo web will crash with this error [source](https://github.com/better-auth/better-auth/issues/2944). To work around this, you'll need to use a browser-compatible storage (like `localStorage` or `sessionStorage`) when running on web, and only use SecureStore on native. You can implement a platform check in your client setup to select the right storage backend. There isn't an official Better Auth workaround documented yet, but this is a common Expo pattern. Recent updates to cookie parsing and SecureStore dependencies don't resolve this web compatibility issue [source](https://github.com/better-auth/better-auth/pull/3909). If you need to retrieve cookies on Expo web, consider writing a custom storage adapter or polyfill for the Expo client plugin. <!-- Dosu Comment Footer --> *To reply, just mention [@dosu](https://go.dosu.dev/dosubot).* --- <sup>How did I do? [Good](https://app.dosu.dev/response-feedback/10d12341-0c22-4fa4-87bd-26d0d184c10b?feedback_type=great_response) | [Irrelevant](https://app.dosu.dev/response-feedback/10d12341-0c22-4fa4-87bd-26d0d184c10b?feedback_type=irrelevant_answer) | [Incorrect](https://app.dosu.dev/response-feedback/10d12341-0c22-4fa4-87bd-26d0d184c10b?feedback_type=incorrect_sources) | [Verbose](https://app.dosu.dev/response-feedback/10d12341-0c22-4fa4-87bd-26d0d184c10b?feedback_type=too_verbose) | [Hallucination](https://app.dosu.dev/response-feedback/10d12341-0c22-4fa4-87bd-26d0d184c10b?feedback_type=hallucination) | [Report 🐛](https://app.dosu.dev/response-feedback/10d12341-0c22-4fa4-87bd-26d0d184c10b?feedback_type=bug_report) | [Other](https://app.dosu.dev/response-feedback/10d12341-0c22-4fa4-87bd-26d0d184c10b?feedback_type=other)</sup>&nbsp;&nbsp;[![Chat with Dosu](https://dosu.dev/dosu-chat-badge.svg)](https://app.dosu.dev/cdda13d9-dd27-4d31-b09a-5d8bec92de21/ask?utm_source=github)&nbsp;[![Join Discord](https://img.shields.io/badge/join-5865F2?logo=discord&logoColor=white&label=)](https://go.dosu.dev/discord-bot)&nbsp;[![Share on X](https://img.shields.io/badge/X-share-black)](https://twitter.com/intent/tweet?text=%40dosu_ai%20helped%20me%20solve%20this%20issue!&url=https%3A//github.com/better-auth/better-auth/issues/4392)
Author
Owner

@ByteBlitz commented on GitHub (Sep 3, 2025):

Thanks @dosu.

I found the workaround for web. Since my goal is just to call the JWT plugin with the correct authentication, the following seems to be sufficient. I do not need to get the token myself, instead I can let fetch take care of it:

const res = await fetch(EXPO_PUBLIC_BETTER_AUTH_URL + "/api/auth/token", {
    credentials: 'include'
  });
@ByteBlitz commented on GitHub (Sep 3, 2025): Thanks @dosu. I found the workaround for web. Since my goal is just to call the JWT plugin with the correct authentication, the following seems to be sufficient. I do not need to get the token myself, instead I can let `fetch` take care of it: ``` const res = await fetch(EXPO_PUBLIC_BETTER_AUTH_URL + "/api/auth/token", { credentials: 'include' }); ```
Author
Owner

@jancbeck commented on GitHub (Sep 9, 2025):

Thanks for sharing, this helped me solve my issue too.

I thought I had solved it with a compatible storage

const createWebCompatibleStorage = () => {
  if (Platform.OS === 'web') {
    return {
      setItem: (key: string, value: string) => {
        try {
          localStorage.setItem(key, value);
        } catch (e) {
          console.error('Local storage is unavailable:', e);
        }
      },
      getItem: (key: string): string | null => {
        try {
          return typeof localStorage !== 'undefined'
            ? localStorage.getItem(key)
            : null;
        } catch (e) {
          console.error('Local storage is unavailable:', e);
          return null;
        }
      },
    };
  } else {
    return SecureStore;
  }
};

export const authClient = createAuthClient({
  baseURL: process.env.EXPO_PUBLIC_BETTER_AUTH_URL,
  plugins: [
    expoClient({
      storage: createWebCompatibleStorage(),
    }),
  ],
});

until I realized expoClient never actually uses this code on web:
abfc48d2aa/packages/expo/src/client.ts (L235C10-L240)

In my solution I pass the value of getCookie() to a sever action and if it's an empty string I dynamically include and try read the actual headers

const { data: session } = await betterFetch<Session>(
  '/api/auth/get-session',
  {
    baseURL: String(process.env.EXPO_PUBLIC_BETTER_AUTH_URL),
    headers: headerArgs.get('Cookie')
      ? // on native we use the manually passed cookie auth header
        headers: headerArgs
      : // on web headers (including cookies) are automatically included but must be accessed via `unstable_headers()`
        await (
          await import('expo-router/build/rsc/server')
        ).unstable_headers(),
  },
);
@jancbeck commented on GitHub (Sep 9, 2025): Thanks for sharing, this helped me solve my issue too. I thought I had solved it with a compatible storage ```ts const createWebCompatibleStorage = () => { if (Platform.OS === 'web') { return { setItem: (key: string, value: string) => { try { localStorage.setItem(key, value); } catch (e) { console.error('Local storage is unavailable:', e); } }, getItem: (key: string): string | null => { try { return typeof localStorage !== 'undefined' ? localStorage.getItem(key) : null; } catch (e) { console.error('Local storage is unavailable:', e); return null; } }, }; } else { return SecureStore; } }; export const authClient = createAuthClient({ baseURL: process.env.EXPO_PUBLIC_BETTER_AUTH_URL, plugins: [ expoClient({ storage: createWebCompatibleStorage(), }), ], }); ``` until I realized expoClient never actually uses this code on web: https://github.com/better-auth/better-auth/blob/abfc48d2aaf1f186621c33f07bc37add1fd10e86/packages/expo/src/client.ts#L235C10-L240 In my solution I pass the value of `getCookie()` to a sever action and if it's an empty string I dynamically include and try read the actual headers ```ts const { data: session } = await betterFetch<Session>( '/api/auth/get-session', { baseURL: String(process.env.EXPO_PUBLIC_BETTER_AUTH_URL), headers: headerArgs.get('Cookie') ? // on native we use the manually passed cookie auth header headers: headerArgs : // on web headers (including cookies) are automatically included but must be accessed via `unstable_headers()` await ( await import('expo-router/build/rsc/server') ).unstable_headers(), }, ); ```
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/better-auth#1833