Mobile

Expo authentication architecture

How the Expo starter stores a session token in SecureStore and sends it as a bearer token to your API.

2 min read

The Mobile App Starter (Expo) does not implement a login server. It implements the client: a sign-in screen, a token in SecureStore, and a fetch wrapper. The app is Expo SDK 52, React Native 0.76.3, and React Navigation 7. Your API is whatever EXPO_PUBLIC_API_URL points at.

Session storage

import * as SecureStore from "expo-secure-store";

const KEY = "session";

export async function saveToken(token: string) {
  await SecureStore.setItemAsync(KEY, token);
}
export async function readToken() {
  return SecureStore.getItemAsync(KEY);
}
export async function clearToken() {
  await SecureStore.deleteItemAsync(KEY);
}

expo-secure-store is ~14.0.0 in the starter. The value is a string. The kit does not store a refresh token, an expiry, or the user profile. On launch, App.tsx calls readToken() and treats any non-empty string as signed in:

readToken().then((token) => {
  setAuthed(Boolean(token));
  setReady(true);
});

There is no call to the API to validate that token at startup. A stale token still opens the tabs until a later request fails. If you need a hard check, call a /me endpoint before setAuthed(true) and clearToken() on 401.

Sign-in

const result = await api<{ token: string }>("/auth/login", undefined, {
  method: "POST",
  body: JSON.stringify({ email, password })
});
await saveToken(result.token);
onDone();

The client expects JSON { token: string }. Any non-OK response throws, and the screen sets the error “Could not sign in. Check the API URL.” The default base URL is https://example.com when EXPO_PUBLIC_API_URL is unset, so that error is the normal result until you set the variable. EXPO_PUBLIC_ is exposed in the app binary. It is the right prefix for a base URL. It is the wrong prefix for a secret.

Attaching the token

export async function api<T>(path: string, token?: string, init?: RequestInit): Promise<T> {
  const response = await fetch(base + path, {
    ...init,
    headers: {
      "Content-Type": "application/json",
      ...(token ? { Authorization: "Bearer " + token } : {}),
      ...init?.headers
    }
  });
  if (!response.ok) throw new Error("Request failed");
  return response.json() as Promise<T>;
}

Pass readToken() as the second argument on calls that need the user. The login call passes undefined because there is no token yet. The helper does not refresh a session. A 401 is a thrown Error with the message “Request failed”.

Navigation is a stack: onboarding, then sign-in, then a bottom tab navigator (Home, Profile, Settings). headerShown is false. Onboarding completion is React state (seen), not SecureStore, so it shows again on the next cold start.

Want to skip the setup? The Mobile App Starter (Expo) already includes SecureStore, the sign-in screen, and the bearer-token client.

Product updates for developers shipping with Designyff kits.

Purchased source may be used in personal, commercial, and client projects. Do not redistribute or resell the original source. Terms of Service

Designyff © 2026 | Starter kits for developers | Powered by  Tacko