Moving from Firebase or API v1

API v2 replaces Firebase Authentication, Remote Config, Cloud Messaging registration, Storage and Firestore with one HTTP API, and it replaces API v1 of this service. Both moves can happen app release by app release: v1 keeps working next to v2, and the same accounts work in both.

From API v1

For apps that use /api/v1 today (such as Royal Circle). Users keep their accounts, passwords, points and member IDs.

Keep users signed in

When the updated app starts and still holds a v1 token, trade it for v2 tokens once with POST /auth/token/from-v1 and {"v1_token": "<v1 token>"}. The answer is a normal sign-in (tokens and user). The v1 token stops working at once, so delete it from the app. 401 auth/invalid-token means the v1 token had already expired or was signed out: show the sign-in screen. Sensitive actions still ask for the password once, because the sign-in time stays the v1 one.

// Flutter
final v1Token = await storage.read(key: 'v1_token');
if (v1Token != null) {
  try {
    final json = await call('POST', '/auth/token/from-v1', body: {'v1_token': v1Token});
    await session.keep(json['data']); // as after a sign-in
  } on ApiError {
    // expired or revoked: the user signs in again
  } finally {
    await storage.delete(key: 'v1_token');
  }
}
// React Native
const v1Token = await SecureStore.getItemAsync('v1_token');
if (v1Token) {
  try {
    const { data } = await call('POST', '/auth/token/from-v1', { v1_token: v1Token });
    await keep(data); // as after a sign-in
  } catch {
    // expired or revoked: the user signs in again
  } finally {
    await SecureStore.deleteItemAsync('v1_token');
  }
}
// iOS
if let v1Token = Keychain.read("v1_token") {
    defer { Keychain.delete("v1_token") }
    if let json = try? await call("POST", "/auth/token/from-v1", body: ["v1_token": v1Token]) {
        await session.keep(json["data"] as! [String: Any]) // as after a sign-in
    }
}
// Android
legacyStore.read()?.let { v1Token ->
    try {
        session.keep(call("POST", "/auth/token/from-v1", JSONObject().put("v1_token", v1Token)).getJSONObject("data"))
    } catch (e: ApiError) {
        // expired or revoked: the user signs in again
    } finally {
        legacyStore.write(null)
    }
}
// Web
const v1Token = localStorage.getItem('v1_token');
if (v1Token) {
  localStorage.removeItem('v1_token');
  try {
    keep((await call('POST', '/auth/token/from-v1', { v1_token: v1Token })).data);
  } catch {
    // expired or revoked: the user signs in again
  }
}

Endpoints

v1 v2 Differences
POST /register POST /auth/sign-up Signs in at once (tokens in the answer); names are optional; no password_confirmation
POST /login POST /auth/sign-in Access token plus refresh token instead of one token; see keeping users signed in
GET /me GET /me, GET /me/loyalty, GET /config The profile, the loyalty status and the app content are three calls
POST /logout POST /auth/sign-out
POST /password/forgot, POST /password/reset POST /auth/password/forgot, POST /auth/password/reset No password_confirmation; the reset ends every session
DELETE /account/delete DELETE /me Needs a recent sign-in

Also new in v2: Google and Apple sign-in, email codes, email verification, sessions, push, files and data.

Answers

  • v1 answers {"success": true, "app": …, "data": {…}} and errors {"message": "…"}; v2 answers {"data": …} and errors {"error": {"code", "message", "fields"}}. Handle errors by code (overview).
  • Users are identified by uid (a ULID string), not the numeric id.
  • The content v1 /login and /me return next to the user (telegram_url, tiers, exclusive_offers, events, services, cards, payment_infrastructure, concierge, meta_ads) are Remote Config parameters with the same names and values: fetch them with GET /config. Objects and lists come as JSON strings; parse them. The app's team edits them on the dashboard's Remote Config page, without a release.

Points, tier and member ID

GET /me/loyalty answers what v1 put in user:

{ "data": { "member_id": "PC-000123", "points": 3250, "tier": "gold", "next_tier": { "tier": "platinum", "points_needed": 6750 } } }

next_tier is null at the top tier. GET /me/loyalty/history lists the points added and taken away, newest first, in pages (points, description, created_at). Both need a signed-in user.

When v1 goes

v1 keeps working until the service owner turns it off. When a date is set, every v1 answer carries a Sunset header with it (and a Link to this page), so you see it coming in your HTTP logs. Move before then; the owner checks that no users are left on v1 first.

From Firebase

The calls map one to one; the Firebase SDK is only kept for getting push tokens.

Firebase This API
createUserWithEmailAndPassword POST /auth/sign-up
signInWithEmailAndPassword POST /auth/sign-in
signInWithCredential (Google, Apple) POST /auth/google, POST /auth/apple with the provider's ID token
sendSignInLinkToEmail / signInWithEmailLink POST /auth/email-code/send / POST /auth/email-code/verify (a 6-digit code, and a magic link to your deep link)
sendPasswordResetEmail / confirmPasswordReset POST /auth/password/forgot / POST /auth/password/reset (a code instead of a link)
sendEmailVerification POST /auth/email/verification/send, then …/confirm with the code
getIdToken(true) POST /auth/token/refresh
onAuthStateChanged, currentUser Your session helper (example) and GET /me
signOut POST /auth/sign-out
updateProfile, updatePassword, verifyBeforeUpdateEmail PATCH /me, PUT /me/password, POST /me/email
reauthenticateWithCredential POST /auth/reauthenticate
linkWithCredential, unlink POST / DELETE /me/identities/{provider}
user.delete() DELETE /me
Remote Config fetchAndActivate, getValue GET /config (cache the values and the ETag yourself)
Messaging getToken, subscribeToTopic Firebase's getToken, then POST /push/devices, PUT /push/devices/{id}/topics/{topic}
Storage putFile, getDownloadURL, delete POST /files, GET /files/{id}/url (or public_url), DELETE /files/{id}
Firestore add, set, update, get, delete POST, PUT, PATCH, GET, DELETE under /data/{collection}
Firestore where, orderBy, limit, startAfter POST /data/{collection}/query
Admin verifyIdToken Offline with the JWKS, or POST /admin/tokens/verify
Admin getUser, createUser, updateUser, deleteUser /admin/users
Admin setCustomUserClaims, revokeRefreshTokens PUT /admin/users/{uid}/claims, POST /admin/users/{uid}/revoke-sessions
Admin messaging().send POST /admin/push/send
Auth triggers (onCreate, onDelete) Webhooks

What works differently:

  • No anonymous or phone sign-in. Offer email codes instead of phone numbers.
  • Security rules become a collection's access mode; anything finer runs in your backend through the Admin API.
  • Firestore: no subcollections, transactions or realtime listeners (onSnapshot); re-read or query when the screen opens, or send a push when data changes. Merges (PATCH) merge nested objects; there are no dot-path updates or field transforms.
  • Accounts can't be imported with their Firebase passwords. Existing users sign in with an email code (their email is verified by it) or with Google or Apple (linked by their verified email) and can set a new password in the app.