Skip to main content
RapidDev - Software Development Agency
flutterflow-tutorials

How to Implement a Secure Data Sharing Feature in FlutterFlow

Implement secure data sharing in FlutterFlow using a Firestore `shares` collection that maps resource IDs to arrays of permitted user IDs and permission levels. Critically, enforce access in Firestore Security Rules — not just the UI. Any sharing system that only checks permissions on the client side can be bypassed by users querying Firestore directly.

What you'll learn

  • Design a `shares` Firestore collection with sharedWith arrays and permission levels
  • Write Firestore Security Rules that enforce view/edit/admin permissions server-side
  • Generate and validate share link tokens for email-based sharing invitations
  • Build a share dialog UI in FlutterFlow with permission level selection
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Beginner11 min read40-55 minFlutterFlow Free+March 2026RapidDev Engineering Team
TL;DR

Implement secure data sharing in FlutterFlow using a Firestore `shares` collection that maps resource IDs to arrays of permitted user IDs and permission levels. Critically, enforce access in Firestore Security Rules — not just the UI. Any sharing system that only checks permissions on the client side can be bypassed by users querying Firestore directly.

Why Security Rules Are the Only Real Enforcement Layer

FlutterFlow makes it easy to show or hide UI elements based on a user's permission level — but UI-level checks are not security. Any user who knows the Firestore collection path can open a browser console and query your database directly, completely bypassing your app's UI. The only way to truly secure shared data is to write Firestore Security Rules that check permissions on every read and write request at the database level. This tutorial builds a sharing system where permission checks live in Rules, not just in FlutterFlow actions. The UI reflects permissions for usability; the Rules enforce them for security.

Prerequisites

  • FlutterFlow project with Firebase Authentication and Firestore enabled
  • Basic familiarity with Firestore Security Rules syntax
  • An existing resource collection (e.g., `documents`, `projects`) to share between users
  • Firebase console access to deploy updated Security Rules

Step-by-step guide

1

Design the `shares` Firestore collection schema

In FlutterFlow, go to Firestore and create a new collection named `shares`. Add the following fields: `resourceId` (String — the document ID of the shared item), `resourceType` (String — e.g., `document`, `project`), `ownerId` (String — the sharing user's UID), `sharedWith` (JSON Array of objects with `uid` String and `level` String — values: `view`, `edit`, `admin`), `shareToken` (String, nullable — for public link sharing), `tokenExpiry` (Timestamp, nullable), `createdAt` (Timestamp). One `shares` document exists per shared resource. The `sharedWith` array contains one entry per invited user. Using a single document per resource (rather than one document per share invitation) makes Security Rules simpler and atomic updates possible.

Expected result: A `shares` collection appears in your Firestore schema with all fields defined.

2

Write Firestore Security Rules that enforce permissions

This is the critical step. In the Firebase console, go to Firestore > Rules and replace the default rules for your resource collection. The rules should allow read if the requesting user is the owner OR if their UID appears in the `sharedWith` array with any permission level. Allow writes only if the user has `edit` or `admin` level. Allow admin operations (updating the shares document) only for the resource owner. Test all rules with the Firebase Rules Playground before saving. Deploy the rules from the Firebase console — FlutterFlow does not have a built-in rules editor.

firestore.rules
1rules_version = '2';
2service cloud.firestore {
3 match /databases/{database}/documents {
4
5 function getShare(resourceId) {
6 return get(/databases/$(database)/documents/shares/$(resourceId)).data;
7 }
8
9 function shareLevel(resourceId) {
10 let share = getShare(resourceId);
11 let entry = share.sharedWith
12 .filter(s, s.uid == request.auth.uid);
13 return entry.size() > 0 ? entry[0].level : null;
14 }
15
16 match /documents/{docId} {
17 allow read: if request.auth != null &&
18 (resource.data.ownerId == request.auth.uid ||
19 shareLevel(docId) != null);
20
21 allow write: if request.auth != null &&
22 (resource.data.ownerId == request.auth.uid ||
23 shareLevel(docId) in ['edit', 'admin']);
24 }
25
26 match /shares/{shareId} {
27 allow read: if request.auth != null &&
28 (resource.data.ownerId == request.auth.uid ||
29 shareLevel(shareId) != null);
30
31 allow write: if request.auth != null &&
32 resource.data.ownerId == request.auth.uid;
33
34 allow create: if request.auth != null &&
35 request.resource.data.ownerId == request.auth.uid;
36 }
37 }
38}

Expected result: Rules are published successfully. Test in the Rules Playground: a user not in `sharedWith` should get `denied` on a read request to a shared document.

3

Build the Share Dialog UI in FlutterFlow

Create a reusable Component called `ShareDialog`. Add a parameter `resourceId` (String) and `resourceType` (String). Inside the component, add a TextField for entering an email address and a DropdownButton for selecting permission level (View / Edit / Admin). Add an Invite button and a list of current shares — query the `shares` collection where `resourceId == resourceId parameter` to display existing collaborators with their levels and a Remove button for each. Add a separate section with a Toggle for enabling a public share link. The entire dialog is a reusable component so you can drop it into any page that has shareable resources.

Expected result: The Share Dialog component appears in the Components panel and can be added to any page with a resourceId parameter.

4

Implement the invite-by-email flow with UID lookup

When the user enters an email and clicks Invite, you need to convert the email to a Firebase UID before updating the `shares` document — because Firestore Security Rules and array queries use UIDs, not emails. Create a Cloud Function named `lookupUserByEmail` that takes an email string, calls `admin.auth().getUserByEmail(email)`, and returns the UID. In FlutterFlow, wire the Invite button to: call the Cloud Function, receive the UID, then run a Firestore Update Document action on the `shares/{resourceId}` document using `FieldValue.arrayUnion` equivalent — add `{uid: returnedUID, level: selectedLevel}` to the `sharedWith` array. Show an error message if the Cloud Function returns no user (email not registered).

Expected result: Entering a registered email and clicking Invite adds the user to the `sharedWith` array and they appear in the collaborators list.

5

Generate and validate share link tokens

For shareable links, create a Cloud Function named `createShareToken` that generates a cryptographically random token (using `crypto.randomBytes(32).toString('hex')`), writes it to `shares/{resourceId}.shareToken` with a `tokenExpiry` 7 days in the future, and returns the full share URL. In FlutterFlow, a second Cloud Function `redeemShareToken` accepts a token string, queries Firestore for a `shares` document where `shareToken == token` and `tokenExpiry > now()`, verifies the token is valid, then adds the current user's UID to the `sharedWith` array with `view` level and clears the token. The share link URL format should be `yourapp.com/join?token=abc123` — create a `/join` page in FlutterFlow that reads the `token` query parameter and calls `redeemShareToken` on page load.

Expected result: Copying the share link and opening it in a new browser window automatically adds the signed-in user as a View collaborator.

6

Show permission-gated UI elements in FlutterFlow

Query the current user's permission level from the `shares` document on pages that display shared resources. Store the level in an App State variable `currentPermissionLevel`. Use FlutterFlow's Conditional Visibility to show Edit buttons only when `currentPermissionLevel` is `edit` or `admin`, and the Share/Invite button only when the level is `admin` or the user is the owner. Add a visual indicator (a badge or subtitle) showing the user's current access level — `Owner`, `Admin`, `Editor`, or `Viewer`. Remember: this UI gating is for usability only. The Security Rules you wrote in Step 2 are what actually enforces these limits.

Expected result: Edit controls are hidden for View-level users. The Security Rules prevent direct API writes even if a user discovers the edit endpoint.

Complete working example

functions/index.js
1const { onCall } = require('firebase-functions/v2/https');
2const { initializeApp } = require('firebase-admin/app');
3const { getAuth } = require('firebase-admin/auth');
4const { getFirestore, FieldValue } = require('firebase-admin/firestore');
5const crypto = require('crypto');
6
7initializeApp();
8
9// Look up a user UID by email address
10exports.lookupUserByEmail = onCall(async (request) => {
11 const uid = request.auth?.uid;
12 if (!uid) throw new Error('Unauthenticated');
13 const { email } = request.data;
14 try {
15 const user = await getAuth().getUserByEmail(email);
16 return { uid: user.uid, displayName: user.displayName || '' };
17 } catch (e) {
18 return { uid: null, error: 'User not found' };
19 }
20});
21
22// Add a user to a resource's sharedWith array
23exports.addShareEntry = onCall(async (request) => {
24 const callerUid = request.auth?.uid;
25 if (!callerUid) throw new Error('Unauthenticated');
26 const { resourceId, targetUid, level } = request.data;
27 const validLevels = ['view', 'edit', 'admin'];
28 if (!validLevels.includes(level)) throw new Error('Invalid level');
29 const db = getFirestore();
30 const shareRef = db.collection('shares').doc(resourceId);
31 const shareDoc = await shareRef.get();
32 if (!shareDoc.exists || shareDoc.data().ownerId !== callerUid) {
33 throw new Error('Not authorized');
34 }
35 // Remove existing entry for this user if present, then add new one
36 const existing = shareDoc.data().sharedWith || [];
37 const filtered = existing.filter((e) => e.uid !== targetUid);
38 filtered.push({ uid: targetUid, level });
39 await shareRef.update({ sharedWith: filtered });
40 return { success: true };
41});
42
43// Generate a one-time share token
44exports.createShareToken = onCall(async (request) => {
45 const uid = request.auth?.uid;
46 if (!uid) throw new Error('Unauthenticated');
47 const { resourceId } = request.data;
48 const db = getFirestore();
49 const shareRef = db.collection('shares').doc(resourceId);
50 const shareDoc = await shareRef.get();
51 if (!shareDoc.exists || shareDoc.data().ownerId !== uid) {
52 throw new Error('Not authorized');
53 }
54 const token = crypto.randomBytes(32).toString('hex');
55 const expiry = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000);
56 await shareRef.update({ shareToken: token, tokenExpiry: expiry });
57 return { token, expiresAt: expiry.toISOString() };
58});
59
60// Redeem a share token — adds caller to sharedWith as viewer
61exports.redeemShareToken = onCall(async (request) => {
62 const uid = request.auth?.uid;
63 if (!uid) throw new Error('Unauthenticated');
64 const { token } = request.data;
65 const db = getFirestore();
66 const snapshot = await db
67 .collection('shares')
68 .where('shareToken', '==', token)
69 .where('tokenExpiry', '>', new Date())
70 .limit(1)
71 .get();
72 if (snapshot.empty) throw new Error('Invalid or expired token');
73 const shareDoc = snapshot.docs[0];
74 const existing = shareDoc.data().sharedWith || [];
75 if (!existing.find((e) => e.uid === uid)) {
76 existing.push({ uid, level: 'view' });
77 }
78 await shareDoc.ref.update({
79 sharedWith: existing,
80 shareToken: null,
81 tokenExpiry: null,
82 });
83 return { resourceId: shareDoc.id, success: true };
84});

Common mistakes when implementing a Secure Data Sharing Feature in FlutterFlow

Why it's a problem: Checking share permissions only in the FlutterFlow UI without Firestore Security Rules

How to avoid: Write Firestore Security Rules that evaluate the `sharedWith` array on every request. Deploy them from the Firebase console and test with the Rules Playground before going live.

Why it's a problem: Storing permission data in the shared resource document's own fields instead of a separate `shares` document

How to avoid: Keep sharing metadata in a dedicated `shares` collection, one document per resource. This separates concerns and lets non-owners view their own share entry without having write access to the resource.

Why it's a problem: Using email addresses as keys in the sharedWith array instead of UIDs

How to avoid: Always store UIDs in `sharedWith`. Use a Cloud Function to translate the invited user's email to their UID before writing to Firestore.

Best practices

  • Always enforce permissions in Firestore Security Rules — treat client-side checks as UX convenience only, never as security.
  • Use a dedicated `shares` collection with one document per resource so sharing metadata is cleanly separated from resource content.
  • Store UIDs (not emails) in the `sharedWith` array — UIDs are stable identifiers that Firestore Security Rules can directly compare to `request.auth.uid`.
  • Expire share link tokens automatically — set a `tokenExpiry` timestamp and check it in both the Cloud Function and Security Rules.
  • Send a notification to invited users after they are added to a share — use a Firestore onCreate trigger Cloud Function to send email via SendGrid.
  • Log all share and permission-change events to an `auditLog` Firestore collection for compliance and debugging.
  • Implement a Remove button that lets the resource owner remove a user from `sharedWith` — and revoke their access immediately via Security Rules.

Still stuck?

Copy one of these prompts to get a personalized, step-by-step explanation.

ChatGPT Prompt

I have a Firestore `shares` collection where each document has a `sharedWith` field that is an array of objects like `{uid: 'abc', level: 'edit'}`. I need Firestore Security Rules that allow a user to read a document in my `documents` collection if their UID appears in the corresponding `shares` document with any level, and allow writes only if their level is `edit` or `admin`. The `shares` document ID matches the `documents` document ID. Write the complete Security Rules for both collections.

FlutterFlow Prompt

In FlutterFlow, I want to build a Share button that opens a dialog, lets the user type an email address, and then calls a Firebase Cloud Function to look up the UID and add that user to a Firestore `shares` document. How do I pass the returned UID from the Cloud Function response into a subsequent Firestore Update Document action in the same action chain?

Frequently asked questions

Can I share data with users who do not have an account yet?

Yes, using share tokens. Generate a token and embed it in an invitation email link. When the recipient signs up and visits the link, your Cloud Function redeems the token and adds their new UID to the `sharedWith` array. Until they redeem the token, the resource is not accessible to them.

How do Firestore Security Rules handle array membership checks for sharedWith?

Use the `filter()` function to find entries matching the current user: `share.sharedWith.filter(s, s.uid == request.auth.uid).size() > 0`. This works for arrays of maps. Note that array-contains does not work for partial map matches, so you need the filter approach.

What happens to share permissions if the resource is deleted?

The `shares` document remains in Firestore even after the resource is deleted, which wastes storage and could leak metadata. Use a Cloud Function triggered by the resource document's deletion to also delete the corresponding `shares` document using `admin.firestore().doc('shares/' + resourceId).delete()`.

Can I set an expiry date on a user's access, not just on share tokens?

Yes. Add an `expiresAt` field to each entry in the `sharedWith` array: `{uid: 'abc', level: 'view', expiresAt: Timestamp}`. Update your Security Rules to check that `entry.expiresAt > request.time` when evaluating access. Run a scheduled Cloud Function daily to remove expired entries from all `shares` documents.

Is it safe to show the share token in the app's URL bar?

Share tokens are single-use in this design — they are cleared from the `shares` document after the first redemption. Single-use tokens in URLs are reasonably safe. If you need multi-use public links, do not clear the token after use, but make sure `tokenExpiry` is set so they expire after a reasonable period.

How do I let admins change another user's permission level from view to edit?

In your Cloud Function (or Firestore update action), filter out the user's existing entry from `sharedWith`, then push a new entry with the updated level. Firestore does not support updating a specific element inside an array — you must read the array, modify it in memory, and write the whole array back.

RapidDev

Talk to an Expert

Our team has built 600+ apps. Get personalized help with your project.

Book a free consultation

Need help with your project?

Our experts have built 600+ apps and can accelerate your development. Book a free consultation — no strings attached.

Book a free consultation

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We'll discuss your project and provide a custom quote at no cost.