Sign-in limits, billing and codes

Sign-in limits on a phone, sign out everywhere, billing, and the codes a mobile build meets.

Sign-in limits

Sign-in on a phone uses the same two calls as on the web: sendEmailCode, then verifyEmailCode.

Once a sender domain is verified on the Domains page (the same setup as for a web app), the code comes from that domain's account address. Until then, it comes from Gemmein's shared address as <App name> (via Gemmein). No other email leaves from Gemmein's shared address.

How many sign-in emails your app sends in a day is never capped. Two abuse limits apply: 3 per address and 20 per IP per app (an IPv6 address counts by its /64), each per 15 minutes, answered 429 with resetAt. The Usage & billing page shows the day's and the month's sign-in emails, and Gemmein sends you a signin_surge alert when a day runs far past your app's own normal.

Sign out everywhere

Sign out everywhere is the fix for a stolen phone. You run it from the dashboard on that person's page (Manage → Sign out everywhere), behind a confirm and an emailed step-up code; the app cannot run it. The phone's next request then returns 401 auth_expired.

g.auth.logout() ends this device only. It is idempotent: a session you have already ended resolves without throwing, because the session being gone is what logout asked for.

Billing

A person counts once a month, on their first signed-in request, on web and mobile alike. Anonymous use never counts, and Development never bills. A phone that polls all day is the same one person as a browser tab that loads once.

Codes

Everything on the error table reaches a phone unchanged. These are the ones a mobile build meets that a browser build does not.

CodeStatusMeaningWhat to do
network_unreachable0 (client-side)The request never reached Gemmein: no connection, an offline device, a host that does not resolve, or an apiUrl pointing at nothing. The message names the host, and err.cause carries the transport's own errorHandle it apart from a refusal. "The server said no" and "the server was never reached" are different problems, and the second must leave the app usable. A call cancelled with { signal } stays an AbortError and is not this code
upload_input_unsupported0 (client-side)@gemmein/sdk/expo: upload() was given a picker's { uri } and expo-file-system is not installed, so there is nothing to read the bytes withRun npx expo install expo-file-system, or pass a Blob or an expo-file-system File
token_too_large0 (client-side)A value over 2,048 bytes was given to the device secure store, which iOS refuses with a native throw the app cannot readStore the token, not the session payload. A Gemmein session token is two orders of magnitude smaller
secure_store_unavailable0 (client-side)The token store could not keep the session, so sign-in throws instead of handing back a session the app will lose. All three of the SDK's stores raise it:
  • Expo: expo-secure-store is not installed or the device refused the write. The device's own error is on err.cause.
  • Swift: the Keychain refused. The OSStatus is in the message.
  • Browser: BrowserTokenStore raises it when the localStorage write (localStorage.setItem) is refused.
Reads and clears stay lenient: an unreadable store means signed out, never a crash
In Swift, -34018 is an unsigned build (a simulator build, for example) with no keychain access group: sign the app (ad-hoc is enough). The session itself is valid, so an app that would rather keep running catches this one code and rebuilds its client with a MemoryTokenStore, signed in until the process ends
denied401A private collection read or write with no signed-in personSign in first. The refusal is the server's, and the app is never the check
ai_not_configured409No provider key on this app and environmentPaste one on the AI page, and the same tool runs unchanged

Uploads are sniffed from the bytes when they are confirmed, against the allow-list on Data (images, PDF, ZIP, EPUB, audio and video), so a text/plain blob is refused 415 unsupported_file_type whatever its name says.

Was this page helpful?

Last updated