App client
What every Expo app shares: the bearer session over a storage port, the discovery client with a product's address, and the API client (bearer, timeout, 401 handling).
Three Expo apps had copied the same session, discovery and API code, and two more were starting.
@krizaka/app-client is that code, once, with ports in and no native module imported: the app passes
expo-secure-store, its fetch and its language.
const storage = secureStorage(SecureStore);
const session = createSessionStore({ storage, key: "yomizaka.session" });
const discovery = createAppDiscovery({ product: "yomizaka", hostSuffix: "yomizaka.com", appVersion, baked, storage, env });
const api = createApiClient({ baseUrl: () => discovery.getService("yomizaka").apiBase, session, fetch: appConfig.fetch, language, productName: "Yomizaka", timeoutMs: 20_000 });-
Session. The bearer token lives in the Keychain or Keystore, never a cookie;
onChangefires when a session starts or ends. -
Discovery. Krizaka's signed manifest, keys and host allow-list plus the product's own domain;
EXPO_PUBLIC_APP_URLwins for local work, and a malformed build input fails at start. -
API client. JSON with the bearer,
credentials: "omit", a timeout that fails like a network error, and a 401 that drops the session. -
Auth context (
@krizaka/app-client/react).createAuth(session, { signIn, signOut })gives the screensuseAuth():loading, thensignedInorsignedOut, followed when a session starts or ends.
Decision: ADR-004.