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-systemandexpo/fetchon 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, thex-client-infoheader readsexpo-ios,expo-androidorexpo-web; without it, it readsexpo. 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.
Thanks for telling us.
Thanks for telling us.
Last updated