# How to Implement Face Recognition for User Authentication in FlutterFlow

- Tool: FlutterFlow
- Difficulty: Intermediate
- Time required: 45-60 min
- Compatibility: FlutterFlow Pro+ (Cloud Functions and Firestore required)
- Last updated: March 2026

## TL;DR

Face recognition authentication in FlutterFlow uses a two-phase approach: during enrollment, capture a photo and send it to a Cloud Function that calls Google Cloud Vision API to extract a face embedding vector, which is stored in Firestore. During login, capture a live photo, extract a new embedding, and compare it with the stored vector using cosine similarity — accept if the score exceeds 0.85.

## Two-Phase Face Recognition System

Building face recognition in FlutterFlow requires combining the device camera, Firebase Cloud Functions, and Google Cloud Vision API. The system has two distinct phases: enrollment (run once per user at signup) and authentication (run on every login). The critical insight is that you should never store raw face images for comparison — instead, store mathematical face embeddings (arrays of 128 floating-point numbers). This is both more privacy-respecting and far more accurate for matching.

## Before you start

- FlutterFlow project connected to Firebase (Firestore and Authentication enabled)
- Firebase project with Cloud Functions enabled (Blaze plan required)
- Google Cloud Vision API enabled in your Google Cloud Console project
- Basic familiarity with FlutterFlow's Action Flows and Custom Actions

## Step-by-step guide

### 1. Set Up the Firestore Schema for Face Data

In your Firebase Console, open Firestore and navigate to your users collection (or create one). Each user document needs two new fields: 'faceEmbedding' (type: Array of numbers, storing 128 floats) and 'faceEnrolled' (type: Boolean, default false). In FlutterFlow, open Settings → Firebase → Firestore Schema and add these fields to your User data type. The faceEmbedding array will hold the numerical representation of the user's face. Never store the actual face photo URL for authentication — the embedding is the authentication credential. Set Firestore security rules to ensure only the authenticated user can read their own faceEmbedding field.

**Expected result:** Your Firestore users collection has faceEmbedding (array) and faceEnrolled (boolean) fields visible in the FlutterFlow schema editor.

### 2. Deploy the Face Embedding Cloud Function

This Cloud Function receives a base64-encoded photo, calls Google Cloud Vision API's face detection endpoint, extracts landmark coordinates, and returns a normalized embedding vector. The function runs server-side so your Cloud Vision API key never touches the client app. Deploy it to Firebase Cloud Functions using the Firebase CLI from your terminal. In FlutterFlow, go to the API Manager (left sidebar, cloud icon) and add the function as an authenticated API call. The function endpoint will be something like 'https://us-central1-yourproject.cloudfunctions.net/extractFaceEmbedding'. Set the Authorization header to use the Firebase ID token from the current user.

```
// Firebase Cloud Function: extractFaceEmbedding
// Deploy via: firebase deploy --only functions:extractFaceEmbedding

const functions = require('firebase-functions');
const vision = require('@google-cloud/vision');
const admin = require('firebase-admin');

const visionClient = new vision.ImageAnnotatorClient();

exports.extractFaceEmbedding = functions.https.onCall(async (data, context) => {
  if (!context.auth) {
    throw new functions.https.HttpsError('unauthenticated', 'Login required');
  }

  const { imageBase64 } = data;
  if (!imageBase64) {
    throw new functions.https.HttpsError('invalid-argument', 'imageBase64 required');
  }

  const [result] = await visionClient.faceDetection({
    image: { content: imageBase64 },
  });

  const faces = result.faceAnnotations;
  if (!faces || faces.length === 0) {
    throw new functions.https.HttpsError('not-found', 'No face detected in image');
  }
  if (faces.length > 1) {
    throw new functions.https.HttpsError('invalid-argument', 'Multiple faces detected');
  }

  const face = faces[0];
  // Build a 128-element embedding from landmark positions
  const landmarks = face.landmarks || [];
  const embedding = [];
  for (const landmark of landmarks) {
    embedding.push(landmark.position.x / 1000.0);
    embedding.push(landmark.position.y / 1000.0);
    embedding.push(landmark.position.z / 1000.0);
  }
  // Pad or truncate to exactly 128 dimensions
  while (embedding.length < 128) embedding.push(0.0);
  const normalized = embedding.slice(0, 128);

  return { embedding: normalized };
});
```

**Expected result:** The Cloud Function deploys successfully and returns a 128-element array when given a photo containing exactly one face.

### 3. Build the Enrollment Custom Action

In FlutterFlow, go to Custom Code → Custom Actions → '+'. Name it 'enrollFace'. This action will: (1) capture a photo using the camera, (2) convert it to base64, (3) call the Cloud Function, (4) save the returned embedding to the user's Firestore document. Use the 'image_picker' package (enable in Settings → Pubspec Dependencies) to open the camera. Convert the image bytes to base64 with dart:convert. Call the Cloud Function using the Firebase callable functions SDK. On success, update the Firestore user document with the returned embedding array and set faceEnrolled to true. Wire this action to an 'Enroll Face' button on your profile or signup page.

```
// Custom Action: enrollFace
// Packages required: image_picker, firebase_core, cloud_functions
// Return type: String (success message or error)

Future<String> enrollFace() async {
  final picker = ImagePicker();
  final XFile? photo = await picker.pickImage(
    source: ImageSource.camera,
    preferredCameraDevice: CameraDevice.front,
    imageQuality: 85,
  );

  if (photo == null) return 'Cancelled';

  final bytes = await photo.readAsBytes();
  final base64Image = base64Encode(bytes);

  final functions = FirebaseFunctions.instance;
  final callable = functions.httpsCallable('extractFaceEmbedding');

  try {
    final result = await callable.call({'imageBase64': base64Image});
    final embedding = List<double>.from(
      (result.data['embedding'] as List).map((e) => (e as num).toDouble()),
    );

    final uid = FirebaseAuth.instance.currentUser!.uid;
    await FirebaseFirestore.instance.collection('users').doc(uid).update({
      'faceEmbedding': embedding,
      'faceEnrolled': true,
      'faceEnrolledAt': FieldValue.serverTimestamp(),
    });

    return 'Face enrolled successfully';
  } on FirebaseFunctionsException catch (e) {
    return 'Enrollment failed: ${e.message}';
  }
}
```

**Expected result:** After tapping 'Enroll Face' and taking a selfie, the user's Firestore document shows faceEnrolled: true and a faceEmbedding array with 128 values.

### 4. Build the Authentication Custom Action

Create a second Custom Action named 'authenticateWithFace'. This action: (1) opens the front camera, (2) captures a photo, (3) calls the Cloud Function to get a live embedding, (4) fetches the stored embedding from Firestore, (5) computes cosine similarity between the two vectors, (6) returns true if similarity exceeds 0.85. Cosine similarity is calculated as the dot product of two unit vectors — a score of 1.0 means identical, 0.85 is a good threshold balancing security and usability. Wire this action to your login page's 'Login with Face' button. Chain it with a conditional: if the action returns true, navigate to the home page; if false, show a Snackbar error and increment a failed attempts counter in App State.

```
// Custom Action: authenticateWithFace
// Return type: bool

Future<bool> authenticateWithFace() async {
  final picker = ImagePicker();
  final XFile? photo = await picker.pickImage(
    source: ImageSource.camera,
    preferredCameraDevice: CameraDevice.front,
    imageQuality: 85,
  );

  if (photo == null) return false;

  final bytes = await photo.readAsBytes();
  final base64Image = base64Encode(bytes);

  final functions = FirebaseFunctions.instance;
  final callable = functions.httpsCallable('extractFaceEmbedding');

  try {
    final result = await callable.call({'imageBase64': base64Image});
    final liveEmbedding = List<double>.from(
      (result.data['embedding'] as List).map((e) => (e as num).toDouble()),
    );

    final uid = FirebaseAuth.instance.currentUser!.uid;
    final doc = await FirebaseFirestore.instance
        .collection('users')
        .doc(uid)
        .get();

    final storedEmbedding = List<double>.from(
      (doc.data()!['faceEmbedding'] as List).map((e) => (e as num).toDouble()),
    );

    final similarity = _cosineSimilarity(liveEmbedding, storedEmbedding);
    return similarity >= 0.85;
  } catch (e) {
    return false;
  }
}

double _cosineSimilarity(List<double> a, List<double> b) {
  double dot = 0, normA = 0, normB = 0;
  for (int i = 0; i < a.length; i++) {
    dot += a[i] * b[i];
    normA += a[i] * a[i];
    normB += b[i] * b[i];
  }
  if (normA == 0 || normB == 0) return 0;
  return dot / (sqrt(normA) * sqrt(normB));
}
```

**Expected result:** The action returns true when the live face matches the enrolled face (similarity >= 0.85) and false otherwise.

### 5. Add Lockout Logic After Failed Attempts

In FlutterFlow's App State, create two variables: 'faceFailedAttempts' (integer, initial value 0) and 'faceLockoutUntil' (DateTime, nullable). In the Action Flow for the 'Login with Face' button, first check if faceLockoutUntil is set and in the future — if so, show a Snackbar with the remaining lockout time and stop. If authenticateWithFace returns false, increment faceFailedAttempts. Add a conditional: if faceFailedAttempts >= 3, set faceLockoutUntil to 30 minutes from now using a Custom Function that returns DateTime.now().add(Duration(minutes: 30)), then reset faceFailedAttempts to 0. On successful authentication, reset both variables. This prevents brute-force photo attacks.

**Expected result:** After 3 failed face authentication attempts, the login button shows a '30-minute lockout' message and the authentication action is blocked.

## Complete code example

File: `face_auth_complete.dart`

```dart
// ============================================================
// FlutterFlow Face Recognition Authentication — Complete
// ============================================================
// Requires packages: image_picker, cloud_functions, firebase_auth,
// cloud_firestore, dart:convert, dart:math

// --- ENROLLMENT ---
Future<String> enrollFace() async {
  final picker = ImagePicker();
  final XFile? photo = await picker.pickImage(
    source: ImageSource.camera,
    preferredCameraDevice: CameraDevice.front,
    imageQuality: 85,
  );
  if (photo == null) return 'Cancelled';

  final bytes = await photo.readAsBytes();
  final base64Image = base64Encode(bytes);

  try {
    final callable = FirebaseFunctions.instance
        .httpsCallable('extractFaceEmbedding');
    final result = await callable.call({'imageBase64': base64Image});
    final embedding = List<double>.from(
      (result.data['embedding'] as List).map((e) => (e as num).toDouble()),
    );
    final uid = FirebaseAuth.instance.currentUser!.uid;
    await FirebaseFirestore.instance.collection('users').doc(uid).update({
      'faceEmbedding': embedding,
      'faceEnrolled': true,
      'faceEnrolledAt': FieldValue.serverTimestamp(),
    });
    return 'Enrolled successfully';
  } on FirebaseFunctionsException catch (e) {
    return 'Error: ${e.message}';
  }
}

// --- AUTHENTICATION ---
Future<bool> authenticateWithFace() async {
  final picker = ImagePicker();
  final XFile? photo = await picker.pickImage(
    source: ImageSource.camera,
    preferredCameraDevice: CameraDevice.front,
    imageQuality: 85,
  );
  if (photo == null) return false;

  final bytes = await photo.readAsBytes();
  final base64Image = base64Encode(bytes);

  try {
    final callable = FirebaseFunctions.instance
        .httpsCallable('extractFaceEmbedding');
    final result = await callable.call({'imageBase64': base64Image});
    final liveEmbedding = List<double>.from(
      (result.data['embedding'] as List).map((e) => (e as num).toDouble()),
    );
    final uid = FirebaseAuth.instance.currentUser!.uid;
    final doc = await FirebaseFirestore.instance
        .collection('users').doc(uid).get();
    final storedEmbedding = List<double>.from(
      (doc.data()!['faceEmbedding'] as List)
          .map((e) => (e as num).toDouble()),
    );
    return _cosineSimilarity(liveEmbedding, storedEmbedding) >= 0.85;
  } catch (_) {
    return false;
  }
}

double _cosineSimilarity(List<double> a, List<double> b) {
  double dot = 0, normA = 0, normB = 0;
  for (int i = 0; i < a.length; i++) {
    dot += a[i] * b[i];
    normA += a[i] * a[i];
    normB += b[i] * b[i];
  }
  if (normA == 0 || normB == 0) return 0;
  return dot / (sqrt(normA) * sqrt(normB));
}
```

## Common mistakes

- **Storing raw face images in Firestore instead of embedding vectors** — Raw face photos are large (hundreds of KB), expensive to store and transfer, and pose serious privacy and GDPR risks. Comparing raw pixels for identity matching also has very poor accuracy and is computationally infeasible on a mobile device. Fix: Always extract and store only the mathematical embedding — an array of 128 floating-point numbers. This is tiny (about 1KB), privacy-respecting, and designed for fast similarity comparison. The Cloud Function extracts the embedding and discards the image.
- **Setting the similarity threshold too low (e.g., 0.6) to reduce false rejections** — A threshold below 0.80 means similar-looking people (siblings, identical twins) may be able to authenticate as each other. It also allows printed photos to succeed. Lower thresholds dramatically reduce security while only marginally improving convenience. Fix: Keep the threshold at 0.85 or higher for authentication use cases. If users frequently fail with legitimate faces, improve the enrollment photo quality (better lighting instructions) rather than lowering the threshold.
- **Calling the Cloud Vision API directly from the Flutter client app** — Your Cloud Vision API key would need to be embedded in the app binary, where it can be extracted by anyone who decompiles the APK or IPA. This exposes the key to misuse and billing abuse. Fix: Always call the API from a Firebase Cloud Function. The client authenticates to the Cloud Function using a Firebase ID token, and the Cloud Function uses the API key from its server environment. The key never reaches the device.
- **Not handling the 'no face detected' error from Cloud Vision API** — Users will inevitably submit photos with no face (camera pointed wrong, too dark, obscured). If your Custom Action does not handle this case, it throws an unhandled exception and the app crashes or freezes on the loading state. Fix: Wrap the Cloud Function call in a try/catch block. Show a user-friendly message like 'No face detected — please try again in better lighting' when the Cloud Function returns a not-found error.
- **Skipping camera permission requests on Android before calling image_picker** — On Android 13+, camera permissions are not automatically granted. If the app has not requested CAMERA permission, image_picker silently returns null and the enrollment or authentication silently fails. Fix: Use the permission_handler package in a Custom Action to request camera permission before calling image_picker. Check the permission status and show an explanation dialog if the user previously denied it.

## Best practices

- Always combine face recognition with at least one other factor (password or OTP) — face recognition alone is not sufficient for high-security use cases due to spoofing risks with printed photos.
- Provide clear lighting guidance to users during enrollment: 'Face the screen directly, ensure your face is well-lit, remove glasses or hats'.
- Allow users to re-enroll their face from the settings screen — face appearance changes over time and embeddings become stale.
- Log all authentication attempts (success/failure timestamp, attempt count) to Firestore for security auditing, without logging the actual embedding or photo.
- Implement a server-side lockout in Firestore, not just client-side App State — client state can be reset by killing and relaunching the app.
- Use front camera only (preferredCameraDevice: CameraDevice.front) for consistent enrollment and authentication conditions.
- Test on multiple devices and in varied lighting conditions before launching — face recognition accuracy varies significantly by camera quality and ambient light.
- If you use RapidDev for custom face recognition implementations, ensure your Cloud Function architecture is reviewed for the specific GDPR and CCPA obligations that apply to biometric data in your jurisdiction.

## Frequently asked questions

### How accurate is face recognition via Google Cloud Vision API compared to dedicated face recognition services?

Cloud Vision provides face detection and landmark positions, not a dedicated face embedding model like FaceNet or DeepFace. The landmark-based embeddings in this tutorial have moderate accuracy (suitable for low-risk authentication) but not the 99.9%+ accuracy of dedicated services. For high-security applications, use a Cloud Run container running a FaceNet model instead.

### Does face recognition work offline in FlutterFlow apps?

No, this implementation requires an internet connection to call the Firebase Cloud Function which calls Cloud Vision. For offline face recognition, you would need to integrate ML Kit's on-device face detection, but ML Kit does not provide face embeddings — only landmark positions and bounding boxes.

### Is it legal to store face embeddings in my app?

This depends on your jurisdiction. In the EU (GDPR), face embeddings are considered biometric data and require explicit user consent, a legitimate purpose, and a Data Processing Agreement with Firebase/Google. In Illinois (BIPA), strict requirements apply. Always consult a legal professional before launching biometric authentication to users.

### What threshold should I use for cosine similarity in face authentication?

0.85 is a good starting point for the landmark-based embeddings from Cloud Vision. If you see too many false rejections (legitimate users failing), increase lighting quality guidance rather than lowering the threshold. If using FaceNet embeddings, the threshold for Euclidean distance comparison (not cosine) is different — typically below 1.0 for a match.

### Can users spoof this system with a photo on their phone screen?

Yes — the basic implementation in this tutorial has no liveness detection, meaning a printed photo or screen photo could potentially match. For higher security, add liveness detection (blink detection, head movement) or use a dedicated anti-spoofing model. See the 'How to Implement Facial Recognition for Enhanced Security' tutorial for the hardened implementation.

### How do I test the face recognition system without deploying to a real device?

You cannot test camera capture in FlutterFlow's browser-based Run Mode. Export the project code (Pro plan required), open it in Android Studio or VS Code, and run it on a connected physical device or emulator with camera emulation enabled. The Cloud Function can be tested independently using the Firebase Emulator Suite.

---

Source: https://www.rapidevelopers.com/flutterflow-tutorials/how-to-implement-face-recognition-for-user-authentication-in-flutterflow
© RapidDev — https://www.rapidevelopers.com/flutterflow-tutorials/how-to-implement-face-recognition-for-user-authentication-in-flutterflow
