Expo, Swift and local development

Keys and sessions on a phone, Expo and React Native, Swift, the store account token, and gemmein dev.

Keys, sessions and domains

A phone app works the same way as a browser app, with one difference: verified domains.

  • The app key identifies the app, and the session authorises the person.
  • Verified domains are a browser control. An app sends no Origin, so they are not checked for one.
  • A mobile-only app needs no domain to go live. Answer Where your app lives with mobile, on the Domains page's first question or on the Get started page. An app that is both web and mobile still needs a domain, for the browser half.
  • Ordinary API authentication does not establish app integrity. The app key is inside the binary, so build as though any request could come from anywhere, and let the session and the server's rules decide, as on the web.

Expo and React Native

@gemmein/sdk/expo is the same package as @gemmein/sdk, with a second entry point that sets four things a phone needs: where the session lives, how the client knows the app is in front of the person, which fetch carries the bytes, and what the client tag says. Nothing is imported from Expo or React Native at module load, so the entry resolves in Node too.

Install the peers in the app itself, not at the root of a monorepo: npx expo install expo-secure-store expo-file-system.

The second argument is the modules, and every key in it is optional: { SecureStore, AppState, Platform, FileSystem, fetch }.

  • If you pass none, the entry loads expo-secure-store, react-native, expo-file-system and expo/fetch on the first call that needs each one.
  • If you pass your own (an app's static imports, or a test's fakes), the entry uses them and loads nothing itself.
  • With Platform, the x-client-info header reads expo-ios, expo-android or expo-web; without it, it reads expo. Either way it is a report the app makes about itself, never a proof, and the usage ledger stores it as a report.

Your own tokenStore, visibility, fetch or platform in the first argument overrides the entry's defaults.

The entry gives the client expo/fetch, because React Native's own fetch is XHR-backed and cannot return a streaming body, which g.ai.chat({ stream: true }) and g.ai.run(…, { stream: true }) return.

This affects upload() too. Expo's FormData refuses a bare picker part, so @gemmein/sdk/expo turns a picker's { uri, name, type, size } into an expo-file-system File before the bytes are sent. Install expo-file-system for that shape; without it, that one call is refused upload_input_unsupported. A Blob or a File needs neither.

Swift

GemmeinSwift has the same HTTP API and the same method names, in Swift idiom, for iOS 17+ and macOS 14+, with no dependencies. In Xcode, choose File → Add Package Dependencies, or add it to a Package.swift: .package(url: "https://github.com/gemmeinhq/gemmein-swift.git", from: "0.12.0").

The session lives in the Keychain by default, so a relaunch keeps the person signed in. If the Keychain refuses the write, sign-in throws secure_store_unavailable (see Codes). A sign-in adds a session, so the same person stays signed in on the phone and in the browser at once, up to 10 devices. g.auth.devices() lists them (see Devices).

import GemmeinSwift

let g = try Gemmein(appKey: "pk_live_...")   // KeychainTokenStore by default

try await g.auth.sendEmailCode(email)
_ = try await g.auth.verifyEmailCode(email: email, code: code)
let page = try await g.collection("notes").list(ListOptions(limit: 20))

if let token = try await g.auth.currentUser().storeAccountToken {
    Purchases.configure(withAPIKey: rcKey, appUserID: token)
}

GemmeinServer is included in the package for Swift that runs on a server. It takes a secret key, and a secret key never belongs in an app bundle. Everything the package throws is a GemmeinError carrying the server's own code and sentence. A rate limit adds resetAt, and a 403 entitlement_required adds requires, so you branch the same way as in web code.

The store account token

currentUser() returns storeAccountToken: an opaque name for this person. It is created on the first call, stays the same for the life of the person, is linked to nothing else, and is gone when the person is erased.

Give a store that value (RevenueCat's app user id, Apple's appAccountToken, Google's obfuscatedExternalAccountId), never the person id or their address. A value inside a third party's ledger is outside the erasure cascade and outside your control.

A Gemmein version that does not carry the token leaves the field absent. Read that as "not carried", never as "no account".

Local development

npx gemmein dev serves a phone the same way it serves a browser. Point the app at it with apiUrl, an option on createExpoGemmein's first argument beside appKey, or with apiURL: on Swift's Gemmein(appKey:apiURL:):

export const g = createExpoGemmein(
  { appKey: "pk_test_...", apiUrl: "http://127.0.0.1:4545" },
  { AppState, Platform },
)

The address depends on the device. An iOS simulator shares the Mac's network stack and reaches gemmein dev at http://127.0.0.1:<port>. An Android emulator reaches it at http://10.0.2.2:<port>, because 127.0.0.1 inside the emulator is the emulator. The presigned upload URL and the file link use the host the request came in on, so uploads and downloads follow the address the app used.

gemmein dev binds 127.0.0.1. A simulator and a current Android emulator both reach it there. A real phone on the same wifi does not, and neither does a second computer. Serve those with one flag:

npx gemmein dev --host 0.0.0.0

The flag is opt-in, one run at a time, and it prints its own warning: reachable from every device on your network, and the engine holds your app's data, its local server key and any AI provider key in your shell. The boot card also prints both phone addresses so you can copy the right one. Without the flag, gemmein dev stays on 127.0.0.1, this machine only.

Licences: the SDK, the MCP server and the gemmein launcher are MIT. The engine the launcher downloads is proprietary: run and cache it freely for building against Gemmein, with no redistribution. Full text at downloads.gemmein.com/engine/LICENSE.

Was this page helpful?

Last updated