# How to Implement Passwordless Login in FlutterFlow

- Tool: FlutterFlow
- Difficulty: Beginner
- Time required: 30-45 min
- Compatibility: FlutterFlow Free+
- Last updated: March 2026

## TL;DR

FlutterFlow supports two passwordless login methods: magic links via Firebase sendSignInLinkToEmail paired with Firebase Dynamic Links (email-based, no password), and SMS OTP via Firebase Phone Authentication (phone number + 6-digit code). Magic links require configuring Dynamic Links with your app's domain. Phone Auth works out of the box in FlutterFlow's built-in auth actions. Both methods must be enabled in the Firebase Authentication console first.

## Two Paths to Passwordless Login in FlutterFlow

Removing passwords reduces sign-up friction and eliminates the most common support request: 'I forgot my password'. FlutterFlow supports two passwordless approaches. Magic links are ideal for web-focused apps — the user enters their email, clicks a link in their inbox, and is signed in. SMS OTP is better for mobile-first apps where users are on their phone and cannot easily switch to email. This tutorial implements both, including the Firebase Dynamic Links configuration that most magic link tutorials skip and that is the most common point of failure.

## Before you start

- FlutterFlow project with Firebase connected
- Firebase Authentication console access
- For magic links: a custom domain or Firebase Hosting subdomain for Dynamic Links
- For SMS OTP: Firebase Blaze plan (phone auth requires billing enabled)

## Step-by-step guide

### 1. Enable Email Link and Phone Authentication in Firebase

Open the Firebase console and navigate to Authentication → Sign-in method. Click Email/Password and toggle the 'Email link (passwordless sign-in)' option on — this is separate from the standard Email/Password toggle. Also enable Phone as a sign-in provider if you want SMS OTP. Click Save after each change. Both of these must be enabled in the Firebase console before any code in FlutterFlow will work. Back in FlutterFlow, go to your project Settings → Firebase and confirm your Firebase config is up to date by clicking Sync Firebase.

**Expected result:** Email Link and Phone sign-in methods show as enabled (green) in the Firebase Authentication console's Sign-in method tab.

### 2. Configure Firebase Dynamic Links for magic link sign-in

Magic links use Firebase Dynamic Links to redirect the user back to your app after they click the email link. In the Firebase console, go to Dynamic Links (under the Grow section). Click 'Get started' if you have not set it up before. Create a new URL prefix — this is the domain your links will use, like 'yourapp.page.link'. Under Link behavior, set both iOS and Android to 'Open the link in your app' and specify your app's bundle ID (iOS) and package name (Android). For web apps, set the fallback URL to your app's web URL. Note the full dynamic link domain — you will need it in your FlutterFlow action settings.

**Expected result:** A Dynamic Links URL prefix is created and appears in the Dynamic Links dashboard with your configured iOS and Android behavior.

### 3. Build the magic link sign-in flow in FlutterFlow

Create a page named MagicLinkPage. Add an EmailTextField widget, a 'Send Magic Link' button, and a text confirmation message. Create a Custom Action named sendMagicLink. Inside the action, call FirebaseAuth.instance.sendSignInLinkToEmail(email: emailAddress, actionCodeSettings: settings) where settings specifies your Dynamic Link URL, the iOS and Android bundle IDs, and handleCodeInApp: true. Store the email in Secure Storage after sending so you can read it when the user returns via the link. Show a confirmation message ('Check your inbox') and disable the send button to prevent duplicate sends.

```
// Custom Action: sendMagicLink
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';

Future<bool> sendMagicLink(String email) async {
  const storage = FlutterSecureStorage();

  final actionCodeSettings = ActionCodeSettings(
    // Replace with your Dynamic Links domain or custom domain
    url: 'https://yourapp.page.link/signin?email=${Uri.encodeComponent(email)}',
    handleCodeInApp: true,
    iOSBundleId: 'com.yourcompany.yourapp',
    androidPackageName: 'com.yourcompany.yourapp',
    androidInstallApp: true,
    androidMinimumVersion: '21',
  );

  try {
    await FirebaseAuth.instance.sendSignInLinkToEmail(
      email: email,
      actionCodeSettings: actionCodeSettings,
    );
    // Store email to retrieve when user returns via the link
    await storage.write(key: 'magic_link_email', value: email);
    return true;
  } catch (e) {
    return false;
  }
}
```

**Expected result:** Entering an email and tapping 'Send Magic Link' sends a Firebase magic link email. The user's email is stored in Secure Storage for later retrieval.

### 4. Handle the magic link redirect when the user returns

When the user taps the magic link in their email, your app must detect the incoming link and complete the sign-in. Add a Custom Action named handleMagicLinkReturn to your app's root page (the first page loaded on startup). The action checks if the app was opened with a sign-in link using FirebaseAuth.instance.isSignInWithEmailLink(currentUrl). If it was, reads the stored email from Secure Storage, calls FirebaseAuth.instance.signInWithEmailLink(email: storedEmail, emailLink: currentUrl), and navigates the user to the home page. Handle the case where the stored email is missing — show a prompt asking the user to re-enter their email address.

```
// Custom Action: handleMagicLinkReturn
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'dart:html' as html show window;

Future<bool> handleMagicLinkReturn() async {
  final currentUrl = html.window.location.href;

  if (!FirebaseAuth.instance.isSignInWithEmailLink(currentUrl)) {
    return false; // Not a magic link redirect
  }

  const storage = FlutterSecureStorage();
  String? email = await storage.read(key: 'magic_link_email');

  if (email == null) {
    // User opened the link on a different device — email is unknown
    // In this case, show a UI prompt to re-enter their email
    return false;
  }

  try {
    await FirebaseAuth.instance.signInWithEmailLink(
      email: email,
      emailLink: currentUrl,
    );
    await storage.delete(key: 'magic_link_email');
    return true; // Sign-in succeeded
  } catch (e) {
    return false;
  }
}
```

**Expected result:** When a user opens the app via a magic link, they are automatically signed in and redirected to the home page without entering a password.

### 5. Implement SMS OTP login as an alternative to magic links

Firebase Phone Authentication provides a simpler passwordless option. In FlutterFlow, go to Authentication → Actions and look for the Phone Sign-In action. Add a page with a phone number TextField (use the Phone type with country code picker) and a 'Send OTP' button. Use FlutterFlow's built-in Phone Auth action — no custom code needed for the basic flow. After the user submits their number, Firebase automatically sends an SMS and shows the OTP verification screen. On iOS, App Attest verification is required for production; on Android, SafetyNet handles verification automatically. For the web, a reCAPTCHA appears before the SMS is sent.

**Expected result:** Users can enter their phone number, receive an SMS with a 6-digit code, enter the code, and be signed in — all within your FlutterFlow app without a password.

## Complete code example

File: `magic_link_signin_page.dart`

```dart
// Standalone Custom Widget: MagicLinkSignInForm
// Handles both sending the magic link and the return URL check
import 'package:flutter/material.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';

class MagicLinkSignInForm extends StatefulWidget {
  final VoidCallback onSignedIn;
  const MagicLinkSignInForm({Key? key, required this.onSignedIn}) : super(key: key);

  @override
  State<MagicLinkSignInForm> createState() => _MagicLinkSignInFormState();
}

class _MagicLinkSignInFormState extends State<MagicLinkSignInForm> {
  final _emailController = TextEditingController();
  bool _sent = false;
  bool _loading = false;
  String? _error;
  final _storage = const FlutterSecureStorage();

  Future<void> _sendLink() async {
    final email = _emailController.text.trim();
    if (email.isEmpty || !email.contains('@')) {
      setState(() => _error = 'Please enter a valid email address');
      return;
    }

    setState(() { _loading = true; _error = null; });

    final settings = ActionCodeSettings(
      url: 'https://yourapp.page.link/signin?email=${Uri.encodeComponent(email)}',
      handleCodeInApp: true,
      iOSBundleId: 'com.yourcompany.yourapp',
      androidPackageName: 'com.yourcompany.yourapp',
      androidInstallApp: true,
      androidMinimumVersion: '21',
    );

    try {
      await FirebaseAuth.instance.sendSignInLinkToEmail(
        email: email,
        actionCodeSettings: settings,
      );
      await _storage.write(key: 'magic_link_email', value: email);
      setState(() { _sent = true; _loading = false; });
    } catch (e) {
      setState(() {
        _error = 'Failed to send link. Please try again.';
        _loading = false;
      });
    }
  }

  @override
  Widget build(BuildContext context) {
    if (_sent) {
      return Column(
        mainAxisSize: MainAxisSize.min,
        children: [
          const Icon(Icons.mark_email_read_outlined, size: 64, color: Colors.green),
          const SizedBox(height: 16),
          const Text('Check your inbox', style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold)),
          const SizedBox(height: 8),
          Text('We sent a sign-in link to ${_emailController.text}'),
        ],
      );
    }

    return Column(
      mainAxisSize: MainAxisSize.min,
      children: [
        TextFormField(
          controller: _emailController,
          keyboardType: TextInputType.emailAddress,
          decoration: const InputDecoration(labelText: 'Email address'),
        ),
        if (_error != null)
          Padding(
            padding: const EdgeInsets.only(top: 8),
            child: Text(_error!, style: const TextStyle(color: Colors.red)),
          ),
        const SizedBox(height: 16),
        SizedBox(
          width: double.infinity,
          child: ElevatedButton(
            onPressed: _loading ? null : _sendLink,
            child: _loading
                ? const SizedBox(height: 20, width: 20,
                    child: CircularProgressIndicator(strokeWidth: 2))
                : const Text('Send Magic Link'),
          ),
        ),
      ],
    );
  }
}
```

## Common mistakes

- **Not configuring Firebase Dynamic Links before implementing magic link sign-in** — Without Dynamic Links configured, the magic link email sends successfully but clicking the link opens a generic Firebase page or fails to redirect to your app. The user cannot complete sign-in. Fix: Configure a Dynamic Links URL prefix in the Firebase console under Dynamic Links before writing any sign-in code. Pass this domain as the url parameter in ActionCodeSettings.
- **Not storing the user's email before sending the magic link** — Firebase requires the original email address to complete signInWithEmailLink. If the user clicks the link on a different device, the email stored locally is not available and sign-in fails. Fix: Store the email in Secure Storage immediately after calling sendSignInLinkToEmail. When handling the return link, read from Secure Storage. If the email is missing, prompt the user to re-enter it.
- **Calling handleMagicLinkReturn on every page load instead of only on app startup** — Checking if the current URL is a magic link on every page load causes unnecessary processing and can incorrectly trigger when non-link URLs are open. Fix: Call handleMagicLinkReturn only from your app's first page (root route) On Page Load action, or use a Universal Links handler that fires only when the app is opened from a specific URL scheme.

## Best practices

- Store the user's email in Secure Storage immediately before calling sendSignInLinkToEmail so sign-in can complete even on a different device.
- Check handleCodeInApp: true in ActionCodeSettings — without this, Firebase opens the link in a browser instead of your app.
- For SMS OTP, add test phone numbers in the Firebase console during development to avoid using real SMS quota.
- Show a 60-second cooldown on the magic link send button to prevent users from requesting multiple links in rapid succession.
- Always check FirebaseAuth.instance.isSignInWithEmailLink(url) before attempting to sign in with a URL to avoid errors on normal page loads.
- Handle the 'opened on different device' case by showing a one-field form asking users to re-enter their email when the stored email is not found.
- Monitor your Firebase Authentication usage dashboard after launch — phone authentication has SMS costs and unexpectedly high volume can affect your bill.

## Frequently asked questions

### What is the difference between magic links and SMS OTP for passwordless login?

Magic links send a clickable URL to the user's email — they click it and are signed in. SMS OTP sends a 6-digit code to their phone — they enter it in the app. Magic links are better for web and desktop-first users. SMS OTP is better for mobile-first apps where switching to email is inconvenient.

### Do I need Firebase Dynamic Links for magic link sign-in?

Yes, for mobile apps. Dynamic Links ensure the magic link URL opens your app instead of a browser. Without them, tapping the link opens a generic Firebase page. For web-only apps, Dynamic Links are not required — the link opens the web app directly.

### Are Firebase Dynamic Links being deprecated?

Yes. Firebase announced Dynamic Links deprecation with shutdown planned for August 2025. For new projects, use a custom domain with Firebase Hosting and handle the redirect with a custom URL scheme in your app. Check the Firebase console for updated recommendations.

### Does Phone Authentication cost money in Firebase?

Phone Auth requires the Firebase Blaze (pay-as-you-go) plan. SMS messages cost approximately $0.01-0.06 per verification depending on the destination country. India and some other regions have higher SMS costs. Monitor usage in the Firebase console.

### What happens if the user opens the magic link on a different device than where they requested it?

The email stored in Secure Storage is not available on the other device. Firebase will fail to complete sign-in without the email. Handle this by detecting the missing email and showing a prompt asking the user to re-enter their email address before completing the sign-in.

### Can I use both passwordless and password-based login in the same app?

Yes. Enable both Email/Password and Email Link in Firebase Authentication settings. Some users may prefer to create a password account while others use magic links. Firebase handles both methods under the same user account when the same email is used.

### How long is a magic link valid before it expires?

Firebase magic links expire after 1 hour by default. You cannot change this expiry time — it is set by Firebase's security infrastructure. If the user does not click the link within an hour, they need to request a new one.

---

Source: https://www.rapidevelopers.com/flutterflow-tutorials/how-to-implement-passwordless-login-in-flutterflow
© RapidDev — https://www.rapidevelopers.com/flutterflow-tutorials/how-to-implement-passwordless-login-in-flutterflow
