Krizaka UI
Krizaka UI

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; onChange fires 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_URL wins 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 screens useAuth(): loading, then signedIn or signedOut, followed when a session starts or ends.

Decision: ADR-004.