# How to Implement Data Export Features in FlutterFlow

- Tool: FlutterFlow
- Difficulty: Beginner
- Time required: 40-55 min
- Compatibility: FlutterFlow Pro+ (code export required for Cloud Function setup)
- Last updated: March 2026

## TL;DR

Implement data export in FlutterFlow using two approaches: client-side CSV with the share_plus package for small datasets under 1,000 rows, and Cloud Functions for large-scale CSV, PDF, and Excel generation. Cloud Functions query Firestore server-side, generate the file using pdfkit or exceljs, upload to Firebase Storage, and return a signed download URL that the app opens with Launch URL.

## Scalable Data Export for CSV, PDF, and Excel

Data export is one of the most requested features in business FlutterFlow apps. This tutorial covers two export approaches based on dataset size. For small datasets under 1,000 rows, a client-side Custom Action builds the CSV string in memory and shares it using share_plus — no server needed. For large datasets or formatted documents like PDF reports and Excel files, a Firebase Cloud Function handles the heavy lifting: it queries Firestore with full pagination support, formats the data, generates the file using battle-tested Node.js libraries, uploads it to Firebase Storage, and returns a 15-minute signed URL. The app opens this URL with the Launch URL action, triggering the device's file manager or browser to handle the download.

## Before you start

- FlutterFlow Pro account with Firebase connected
- Firebase Cloud Functions enabled in your project
- Firebase Storage enabled with a bucket configured
- Basic familiarity with FlutterFlow's Custom Actions panel

## Step-by-step guide

### 1. Build a Client-Side CSV Export for Small Datasets

In FlutterFlow, create a Custom Action named exportToCSV. Add the share_plus: ^7.0.0 dependency in the Custom Action's pubspec section. The action accepts a parameter documents (List of dynamic — your Firestore query result) and a filename (String). It iterates over the documents, extracts the fields you want to export, builds a CSV string with a header row, writes it to a temporary file using dart:io's File class and path_provider to get the temp directory, then calls ShareXFiles with the file. Wire this Custom Action to an Export CSV button on your data screen. Pass the Backend Query's documents list as the documents parameter.

```
// Custom Action: exportToCSV
// pubspec: share_plus: ^7.0.0, path_provider: ^2.1.0
import 'dart:io';
import 'package:path_provider/path_provider.dart';
import 'package:share_plus/share_plus.dart';

Future exportToCSV(List<dynamic> documents, String filename) async {
  final buffer = StringBuffer();
  // Header row
  buffer.writeln('Name,Email,Date,Amount');
  // Data rows
  for (final doc in documents) {
    final data = doc.data() as Map<String, dynamic>? ?? {};
    final name = (data['name'] ?? '').toString().replaceAll(',', ' ');
    final email = (data['email'] ?? '').toString();
    final date = (data['createdAt']?.toDate()?.toIso8601String() ?? '');
    final amount = (data['amount'] ?? 0).toString();
    buffer.writeln('$name,$email,$date,$amount');
  }
  final dir = await getTemporaryDirectory();
  final file = File('${dir.path}/$filename.csv');
  await file.writeAsString(buffer.toString());
  await Share.shareXFiles([XFile(file.path)], text: 'Exported data');
}
```

**Expected result:** Tapping Export CSV opens the device's native share sheet with the CSV file ready to save or send.

### 2. Create a Cloud Function for Large-Scale CSV Export

In the Firebase Console, create a Cloud Function named exportUserDataCSV. The function authenticates the caller, queries the target Firestore collection with pagination (using startAfter to page through all results without hitting memory limits), builds the CSV in a stream, uploads it to Firebase Storage, and returns a signed URL. In FlutterFlow's API Calls panel, create a new Cloud Function API call targeting the function's HTTPS endpoint. Pass currentUser.uid and any filter parameters (date range, project ID). On the Export button's action, call this API, then use Launch URL with the returned URL.

```
// Cloud Function: exportUserDataCSV (index.js)
const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp();

exports.exportUserDataCSV = functions.https.onCall(async (data, context) => {
  if (!context.auth) {
    throw new functions.https.HttpsError('unauthenticated', 'Login required');
  }
  const userId = context.auth.uid;
  const db = admin.firestore();
  const rows = ['Name,Email,Date,Amount'];
  let query = db.collection('orders').where('userId', '==', userId).orderBy('createdAt', 'desc');
  if (data.startDate) query = query.where('createdAt', '>=', new Date(data.startDate));
  
  // Paginate through all results
  let lastDoc = null;
  let hasMore = true;
  while (hasMore) {
    let pageQuery = lastDoc ? query.startAfter(lastDoc).limit(500) : query.limit(500);
    const snap = await pageQuery.get();
    snap.forEach(doc => {
      const d = doc.data();
      const name = (d.name || '').replace(/,/g, ' ');
      const email = d.email || '';
      const date = d.createdAt?.toDate()?.toISOString() || '';
      const amount = d.amount || 0;
      rows.push(`${name},${email},${date},${amount}`);
    });
    if (snap.size < 500) hasMore = false;
    else lastDoc = snap.docs[snap.docs.length - 1];
  }

  const csv = rows.join('\n');
  const bucket = admin.storage().bucket();
  const filePath = `exports/${userId}/data_${Date.now()}.csv`;
  const file = bucket.file(filePath);
  await file.save(csv, { contentType: 'text/csv' });
  const [url] = await file.getSignedUrl({ action: 'read', expires: Date.now() + 900000 });
  return { url, rowCount: rows.length - 1 };
});
```

**Expected result:** The Cloud Function generates a CSV from all matching Firestore documents regardless of size and returns a download URL.

### 3. Add PDF Report Generation with pdfkit

Create a second Cloud Function named exportPDFReport. Add pdfkit as a dependency in package.json. The function creates a PDFDocument, adds a styled header with your app name and report date, iterates over the Firestore query results, and adds formatted rows with alternating background colors. It pipes the PDF stream to a buffer, then uploads the buffer to Firebase Storage. More complex PDF reports can include totals rows, charts embedded as base64 images, and multi-page support with automatic page breaks. In FlutterFlow, add an Export PDF button alongside the CSV button with a separate API call to this function.

**Expected result:** Tapping Export PDF triggers the Cloud Function. The device opens the PDF via Launch URL in the system PDF viewer.

### 4. Create an Excel Export with Multiple Sheets Using exceljs

Create a third Cloud Function named exportExcelReport. Add exceljs to package.json. The function creates a new Workbook, adds a Summary sheet with aggregate totals, then adds a Detail sheet with all individual records. Apply cell formatting: bold headers, number format for currency columns, date format for timestamp columns. ExcelJS supports column width auto-fit, frozen header rows, and column filters — add all three for a professional output. Upload the .xlsx buffer to Firebase Storage and return a signed URL. In FlutterFlow, add an Export Excel button that calls this function.

**Expected result:** The exported Excel file opens with two sheets, formatted headers, and proper column widths.

### 5. Show Export Progress and Handle Errors

Large exports can take 10-30 seconds. Add a proper loading state to prevent the user from tapping the button multiple times and to set expectations. Create a Page State variable isExporting (Boolean, default false). When the Export button is tapped, set isExporting to true and show a CircularProgressIndicator with a Text widget saying 'Generating your export...' using Conditional Visibility. After the API call returns (whether success or error), set isExporting back to false. On success, call Launch URL with the returned URL. On error, show a Snack Bar with the error message. Disable the Export button using Conditional Interactivity tied to isExporting.

**Expected result:** A loading spinner appears during export. The button is disabled to prevent double-taps. Success opens the file; errors show a clear message.

## Complete code example

File: `index.js`

```javascript
// Cloud Functions: exportUserDataCSV + exportPDFReport
// package.json dependencies: pdfkit, firebase-admin, firebase-functions

const functions = require('firebase-functions');
const admin = require('firebase-admin');
const PDFDocument = require('pdfkit');
admin.initializeApp();

// ---- CSV EXPORT ----
exports.exportUserDataCSV = functions.https.onCall(async (data, context) => {
  if (!context.auth) throw new functions.https.HttpsError('unauthenticated', 'Login required');
  const userId = context.auth.uid;
  const db = admin.firestore();
  const rows = ['Name,Email,Date,Amount'];
  let query = db.collection('orders').where('userId', '==', userId).orderBy('createdAt', 'desc');
  let lastDoc = null;
  let hasMore = true;
  while (hasMore) {
    const pageQ = lastDoc ? query.startAfter(lastDoc).limit(500) : query.limit(500);
    const snap = await pageQ.get();
    snap.forEach(doc => {
      const d = doc.data();
      rows.push([
        (d.name || '').replace(/,/g, ' '),
        d.email || '',
        d.createdAt?.toDate()?.toISOString() || '',
        d.amount || 0
      ].join(','));
    });
    hasMore = snap.size === 500;
    if (hasMore) lastDoc = snap.docs[snap.docs.length - 1];
  }
  const csv = rows.join('\n');
  const bucket = admin.storage().bucket();
  const filePath = `exports/${userId}/data_${Date.now()}.csv`;
  await bucket.file(filePath).save(csv, { contentType: 'text/csv' });
  const [url] = await bucket.file(filePath).getSignedUrl({ action: 'read', expires: Date.now() + 900000 });
  return { url, rowCount: rows.length - 1 };
});

// ---- PDF EXPORT ----
exports.exportPDFReport = functions.https.onCall(async (data, context) => {
  if (!context.auth) throw new functions.https.HttpsError('unauthenticated', 'Login required');
  const userId = context.auth.uid;
  const db = admin.firestore();
  const snap = await db.collection('orders').where('userId', '==', userId).limit(200).get();
  return new Promise(async (resolve, reject) => {
    const doc = new PDFDocument({ margin: 40 });
    const buffers = [];
    doc.on('data', chunk => buffers.push(chunk));
    doc.on('end', async () => {
      const pdfBuffer = Buffer.concat(buffers);
      const filePath = `exports/${userId}/report_${Date.now()}.pdf`;
      const bucket = admin.storage().bucket();
      await bucket.file(filePath).save(pdfBuffer, { contentType: 'application/pdf' });
      const [url] = await bucket.file(filePath).getSignedUrl({ action: 'read', expires: Date.now() + 900000 });
      resolve({ url });
    });
    doc.on('error', reject);
    doc.fontSize(18).font('Helvetica-Bold').text('Orders Report', { align: 'center' });
    doc.moveDown();
    doc.fontSize(10).font('Helvetica');
    snap.forEach(d => {
      const row = d.data();
      doc.text(`${row.name || 'N/A'} — $${row.amount || 0} — ${row.createdAt?.toDate()?.toLocaleDateString() || ''}`);
    });
    doc.end();
  });
});
```

## Common mistakes

- **Building the entire CSV or PDF in a Custom Action on the client for large datasets** — Fetching 10,000 Firestore documents to the client, building a string in memory, and writing a large file all happens on the user's device. This crashes on low-memory devices, times out for large datasets, and burns through the user's mobile data. Fix: Use a Cloud Function for any export exceeding 1,000 rows. The function runs server-side with no memory constraint, uses Firestore Admin SDK (no read quotas), and uploads directly to Firebase Storage without touching the client device.
- **Using a non-paginated Firestore query in the Cloud Function (e.g., .get() with no limit)** — A single Firestore .get() call on a large collection can take minutes and exceed Cloud Functions' 60-second default timeout. Fix: Use pagination with .limit(500) and .startAfter() in a while loop, as shown in the Cloud Function code above. This processes the data in batches and completes well within the timeout.
- **Hardcoding column names in the export that differ from what users see in the app UI** — Exporting a column named 'createdAt' when the app shows 'Order Date' confuses users and requires them to manually rename columns in Excel before sharing. Fix: Use human-readable headers in the CSV/Excel header row: 'Order Date' not 'createdAt', 'Customer Name' not 'name'. Map your Firestore field names to display names in the export logic.

## Best practices

- Use client-side CSV for datasets under 1,000 rows and Cloud Functions for anything larger.
- Always paginate Firestore queries in Cloud Functions using limit() and startAfter() — never query without a limit.
- Generate signed URLs with a 15-minute expiry so the download link is available long enough to use but does not stay valid indefinitely.
- Clean up old export files from Firebase Storage using a Cloud Storage lifecycle rule that deletes files older than 24 hours.
- Escape CSV values that contain commas or newlines by wrapping them in double quotes and doubling any internal double quotes.
- Show a row count in the success message so users know the export captured all expected records.
- For scheduled exports (e.g., weekly reports), use a Cloud Scheduler trigger instead of on-demand Cloud Function calls.

## Frequently asked questions

### Can I export data as Excel (.xlsx) instead of CSV?

Yes. Use the exceljs npm package in a Cloud Function. It supports multiple sheets, cell formatting, number formats, and column widths. The generated .xlsx file is uploaded to Firebase Storage and returned as a signed URL, exactly like the CSV and PDF exports.

### How do I add images or charts to a PDF export?

PDFKit supports embedding images from URLs or buffers using doc.image(buffer, options). For charts, generate the chart as a PNG on the server using chart.js with the canvas npm package, then embed the PNG buffer in the PDF.

### The Cloud Function times out for very large exports — how do I fix this?

Increase the Cloud Function timeout to 540 seconds (9 minutes) in the Firebase Console under Functions settings. Also increase the memory allocation to 1GB for large file generation. For exports over 100,000 rows, consider generating the file asynchronously and emailing the download link when done.

### How do I let users pick a date range for their export?

Add two DateTime Picker widgets to the export dialog — Start Date and End Date. Pass these as parameters to the Cloud Function API call. In the Cloud Function, add .where('createdAt', '>=', startDate).where('createdAt', '<=', endDate) to the Firestore query.

### Can users download the export on a desktop browser?

Yes. The signed Firebase Storage URL works in any browser — desktop or mobile. On desktop, Launch URL opens the URL in the default browser which downloads the file. On mobile, the system file manager or appropriate app handles the file type.

### How do I prevent users from exporting other users' data?

The Cloud Function must use context.auth.uid (the authenticated user's ID) as the userId filter — never trust a userId parameter passed by the client. Since Cloud Functions verify the Firebase Auth token, a malicious user cannot impersonate another user's UID.

---

Source: https://www.rapidevelopers.com/flutterflow-tutorials/how-to-implement-data-export-features-in-flutterflow
© RapidDev — https://www.rapidevelopers.com/flutterflow-tutorials/how-to-implement-data-export-features-in-flutterflow
