Sessions on a device
Sign-in, token storage, refresh under rotation, one-time codes and deep links — the whole session lifecycle on a phone.
Auth gives your application its own users — held in your project, separate from every other project, and nothing to do with how anybody signs in to Flares itself. This page is the session lifecycle from the device's point of view: getting a token, keeping it somewhere safe, and refreshing it without signing your user out by accident.
What a sign-in returns
POST /v1/projects/prj_…/auth/signin
Authorization: Bearer basket_public_…
Content-Type: application/json
{ "email": "aisha@example.com", "password": "a-strong-passphrase" }
{
"user": { "id": "usr_…", "email": "aisha@example.com",
"email_verified": false, "status": "active" },
"access_token": "eyJ…",
"expires_in": 3600,
"refresh_token": "frt_…"
}
| Token | Lives for | What it is for |
|---|---|---|
access_token | 60 minutes by default | Sent with every request. Short-lived on purpose: if it leaks, it expires. |
refresh_token | 30 days by default | Exchanged for a new access token. This is the one that keeps somebody signed in between app launches. |
Both lifetimes are per project and can be changed in the console under Auth settings. A banking app and a game should not have the same session length.
Where to keep them
| Platform | Use | Not |
|---|---|---|
| iOS | Keychain, with kSecAttrAccessibleAfterFirstUnlock |
UserDefaults — it is a plist, readable from a backup |
| Android | EncryptedSharedPreferences, or the Keystore | Plain SharedPreferences, or a file in app storage |
| Flutter | flutter_secure_storage, which wraps both |
Anything that writes to the documents directory |
| React Native | Keychain / Keystore through a native module | AsyncStorage — unencrypted on both platforms |
Use after first unlock rather than when unlocked for the access item if your app does background work; otherwise a background refresh on a locked phone cannot read the token it needs.
Refresh tokens rotate. This is the part that catches people.
Every refresh returns a new refresh token and retires the one you used. If a retired token is presented again, Flares treats it as a replay — the assumption being that two parties now hold the same token, one of whom stole it — and revokes the entire session family. The user is signed out on that device.
POST /v1/projects/prj_…/auth/refresh
Authorization: Bearer basket_public_…
{ "refresh_token": "frt_…" }
→ { "access_token": "eyJ…", "expires_in": 3600, "refresh_token": "frt_NEW…" }
The three rules that avoid it
- One refresh at a time. Serialise it. Every caller that hits a 401 waits on the same in-flight refresh rather than starting its own.
- Store the new token before you use it. Write the successor to secure storage as soon as the response arrives — before retrying whatever was waiting. A process killed after the write still has a usable token; one killed before it does not.
- Never retry a refresh. Retrying any other request is fine. Retrying a refresh is by definition sending a token that may already have been consumed. If a refresh fails, sign the user in again.
A serialised refresh
// Swift — one actor owns the token, so only one refresh can be in flight
actor SessionStore {
private var refreshTask: Task<String, Error>?
func validAccessToken() async throws -> String {
if let token = keychain.accessToken, !isExpired(token) { return token }
// Somebody is already refreshing: wait for that one.
if let existing = refreshTask { return try await existing.value }
let task = Task { () -> String in
defer { refreshTask = nil }
let fresh = try await callRefresh(keychain.refreshToken)
// Persist BOTH tokens before anybody uses either.
keychain.accessToken = fresh.accessToken
keychain.refreshToken = fresh.refreshToken
return fresh.accessToken
}
refreshTask = task
return try await task.value
}
}
The equivalent on Android is a Mutex around the same three steps, or an OkHttp
Authenticator that synchronises on the token store. The shape matters more than the
language: check, wait, refresh once, persist, then proceed.
Signing in without a password
Typing a passphrase on a phone keyboard is miserable, so Auth supports one-time codes by email. Enable it under Auth in the console, alongside or instead of email and password.
POST /v1/projects/prj_…/auth/otp/request { "email": "aisha@example.com" }
POST /v1/projects/prj_…/auth/otp/redeem { "email": "aisha@example.com", "code": "123456" }
Redeeming returns exactly what a password sign-in returns, so the rest of your session handling is unchanged. The request step answers the same way whether or not the address exists — do not build a screen that reveals the difference.
On iOS, mark the code field .textContentType(.oneTimeCode); on Android use
autofillHints="smsOTPCode" for the equivalent. The code arrives by email rather than
SMS, but the autofill affordance still helps.
Verification and password reset, as deep links
Verification and reset emails contain a link. By default that link points at a web page, which on a phone means your user leaves your app. You can point them back into it instead: set verification URL and reset URL in Auth settings, and Flares will build its links from those.
myapp://auth/verify?token=…
https://yourdomain.example/auth/reset?token=… // universal / app link
A universal link (iOS) or app link (Android) is better than a custom scheme: it opens your app when installed and your website when not, so a person reading the email on a laptop is not stuck. The token is appended by Flares; your app hands it back to the reset or verify endpoint.
Sessions the person can see
Each sign-in is its own session, so a user on a phone and a laptop has two. Both can be ended — from your app by calling sign-out, or by an administrator in the console. Ending a session takes effect on the next request; an access token already in flight remains valid until it expires, which is at most its 60 minutes.
POST /v1/projects/prj_…/auth/signout this session
POST /v1/projects/prj_…/auth/signout-all every session this user has
Sign out everywhere is worth putting in your settings screen. It is what somebody reaches for when they lose a phone, and it is one call.
Handling the failures
| What you get | What it means | Do |
|---|---|---|
401 on a normal request | The access token expired | Refresh once, then retry the request |
401 on a refresh | The refresh token is retired, revoked or expired | Clear stored tokens and show sign-in. Do not retry |
403 | Signed in, but a rule refused this record | A permissions problem, not a session problem. Do not sign the user out |
| Network timeout | The phone moved between networks | Retry the request — but never the refresh |
403 as a session failure is a common and confusing bug: the user is signed
out and, on signing back in, hits the same refusal. Only a failed refresh means the
session is over.
Next
- Realtime on a phone — the same token, over a websocket that survives backgrounding.
- Rules — writing the record access your app relies on.
- Auth API reference — every endpoint and field.