Abrova

Sign in guests and upgrade them later

At the end: users who start as guests and keep their data when they sign up.
Abrova AuthFlutter · WebBeginner · 20 min

Before you start

GuestSame user idSigned-up user

A sign-in wall on the first screen loses users. With Abrova Auth the app signs each new user in as an anonymous guest with no screen at all. The guest has a real user id from the start, so everything you store under it is theirs. When they decide to sign up, you link an e-mail and password to that same user: the id doesn't change and nothing has to be moved.

Costs from your wallet. Each user who uses the app in a month, guests included, is a monthly active user. 1,000 are free every month; above that, 10,000 Toman per 1,000 from your wallet. See Monthly active users.

Turn on Abrova Auth and the two methods#

In Abrova Console:

  1. Settings › Products: turn on Abrova Auth.
  2. Abrova Auth › Sign-in methods: turn on Anonymous and E-mail and password. For e-mail, set the shortest password (8 to 128 characters, default 8) and whether users need a verified e-mail before they can sign in.
  3. Copy the public key (ab_live_…) from Project settings → Keys. Abrova Auth needs nothing else: no server, no server secret.

Install the SDK#

Flutter

Add the package with the Abrova registry as its hosted source, then run flutter pub get:

yaml
# pubspec.yaml
dependencies:
  abrova_auth:
    hosted: https://center.abrova.ir/api/packages/abrova/pub
    version: ^0.1.1

Web

Point the @abrova scope at Abrova Center in .npmrc, then install:

bash
# .npmrc
@abrova:registry=https://center.abrova.ir/api/packages/abrova/npm/
bash
npm install @abrova/auth-web

Sign the guest in at first launch#

Sign in anonymously only when nobody is signed in yet. The SDK keeps the session, so the same guest comes back on the next launch; calling signInAnonymously() on every start would create a new user each time.

Flutter

dart
import 'package:flutter/material.dart';
import 'package:abrova_auth/abrova_auth.dart';

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();

  // Restores the saved session from secure storage, without a network call.
  await AbrovaAuth.initialize('ab_live_your_api_key');

  final auth = AbrovaAuth.instance;
  if (auth.currentUser == null) {
    await auth.signInAnonymously();
  }

  runApp(const MyApp());
}

Web

On the web the session is kept in memory by default, so a guest would be lost on every reload. Keep it in localStorage for this flow, and wait for ready() before you check:

typescript
import { createAbrovaAuth, localStorageStorage } from '@abrova/auth-web';

export const auth = createAbrovaAuth({
  apiKey: 'ab_live_your_api_key',
  storage: localStorageStorage(), // the guest survives a reload
});

export async function startSession() {
  await auth.ready(); // the stored session (if any) is loaded
  if (!auth.isSignedIn) {
    await auth.signInAnonymously();
  }
  return auth.currentUserId; // ru_…
}
localStorage and XSS. Any script on your page can read localStorage. Use it only with a strict Content Security Policy and no untrusted scripts. See Staying signed in.

Store the user's data under this id from now on (currentUser.id on Flutter, currentUserId on the web). If you also use Abrova Trace, attach the id to errors: see Attach the signed-in user to errors.

Show who is a guest#

Offer "Create an account" only to guests. The user's anonymous field is true until a method is linked, and your auth listener hears about every change:

Flutter

dart
StreamBuilder<AbrovaAuthUser?>(
  stream: AbrovaAuth.instance.authStateChanges, // current user first, then every change
  builder: (context, snapshot) {
    final user = snapshot.data;
    if (user == null) return const SizedBox.shrink();
    return user.anonymous
        ? TextButton(onPressed: openSignUp, child: const Text('Create an account'))
        : Text('Signed in as ${user.id}');
  },
)

Web

typescript
const me = await auth.getMe(); // { id, anonymous, emailVerified, createdAt, providers }
showSignUpButton(me.anonymous); // your code

When the guest signs up, link the e-mail and password to the signed-in guest. Don't call signUpWithEmail here: that creates a new user and leaves the guest's data behind.

Flutter

dart
Future<void> upgrade(String email, String password) async {
  final auth = AbrovaAuth.instance;
  try {
    await auth.linkPassword(email: email, password: password);
    // Same user id. currentUser.anonymous is now false.

    // Optional: ask them to confirm the address.
    await auth.sendEmailVerification(redirectUrl: 'https://example.com/auth/verify');
  } on AbrovaAuthException catch (e) {
    switch (e.code) {
      case AbrovaAuthErrorCode.emailTaken:
        showError('This e-mail already has an account. Sign in instead?');
      case AbrovaAuthErrorCode.weakPassword:
        showError('Choose a longer, less common password.');
      case AbrovaAuthErrorCode.networkError:
        showError('No connection. Try again.');
      default:
        showError('Could not create the account.');
    }
  }
}

Web

typescript
import { isAbrovaAuthError } from '@abrova/auth-web';

export async function upgrade(email: string, password: string) {
  try {
    await auth.linkPassword({ email, password }); // same user id
    await auth.sendEmailVerification({ redirectUrl: 'https://example.com/auth/verify' });
  } catch (e) {
    if (isAbrovaAuthError(e) && e.code === 'EMAIL_TAKEN') showError('This e-mail already has an account.');
    else if (isAbrovaAuthError(e) && e.code === 'WEAK_PASSWORD') showError('Choose a longer password.');
    else throw e;
  }
}

The verification link opens your redirectUrl with ?token=…; pass the token to verifyEmail. The redirectUrl must start with one of the link addresses under Abrova Auth › Settings. Linking also starts a fresh session, which the SDK takes over by itself.

When the e-mail already has an account#

EMAIL_TAKEN means the person signed up before, on another device or before reinstalling. Nothing is merged on its own: two users' data is never combined behind your back. What most apps do:

  1. Tell the user this account already exists and offer to sign in.
  2. If the guest has data worth keeping, copy it to the other account in your app first (or let the user discard it).
  3. Sign in with signInWithEmail. This replaces the guest on this device: the guest is not deleted, but no device holds its session any more, so it can't be reached again.
dart
await AbrovaAuth.instance.signInWithEmail(email: email, password: password);

Guests that never come back are removed after 90 days without a sign-in (7 to 3,650 days, under Abrova Auth › Settings › Anonymous users). Users who linked a method are never removed this way.

Check that it worked#

  1. Start the app: in Abrova Console → Abrova Auth › Users a new user appears with the method anonymous.
  2. Restart the app (or reload the page): no second user appears.
  3. Sign up in the app: the same user now lists password among its methods, and its id is unchanged.

Troubleshooting#

A new guest on every start

Flutter: call AbrovaAuth.initialize before reading currentUser, and sign in only when it is null. Web: pass storage: localStorageStorage() and await auth.ready() before checking isSignedIn; the default memory storage forgets the session on reload.

METHOD_DISABLED or PRODUCT_DISABLED

Turn on Abrova Auth under Settings › Products, and Anonymous and E-mail and password under Abrova Auth › Sign-in methods.

RATE_LIMITED on signInAnonymously

Too many new guests from one address, or the project's daily limit (200 new anonymous users a day, or your own lower limit) is reached. Wait for retryAfter (Flutter) or retryAfterSec (web). Existing users are not affected.

EMAIL_TAKEN when linking

Another user already has this e-mail. Offer sign-in or a password reset, as in step 6.

Signing in later on another device fails with EMAIL_NOT_VERIFIED

Your project requires a verified e-mail. Send the verification e-mail after linking (step 5) and ask the user to open it.

MAU_LIMIT for new users

This month's 1,000 free monthly active users are used up and the wallet has no balance. Top up the wallet in Abrova Console to let new users in.

Pitfalls
  • If the e-mail already belongs to another user, show the conflict — there are no silent merges.
  • Sign in anonymously only when nobody is signed in, or every start creates a new guest.
  • On the web the session is in memory by default: keep guests with localStorageStorage().

Where next: Auth quick start