Build legally defensible document signing in FlutterFlow using the pdfx package for PDF rendering, a Custom Widget with a Canvas for signature capture, a Cloud Function that merges the signature onto the PDF using pdf-lib, and a Firestore audit trail with SHA-256 document hashes. Store signatures as Firebase Storage file paths — never as base64 strings in Firestore, which would bloat each document by 500KB or more.
Building Document Signing That Holds Up
Document signing in a mobile app must satisfy three requirements to be legally useful: the signer's identity must be established (authentication), the signature must be captured with intent (deliberate action), and the signed document must be tamper-evident (hash-verified). FlutterFlow's Firebase authentication handles identity. A Canvas-based signature widget captures intent. A SHA-256 hash of the original PDF stored before signing detects any post-signing tampering. Together, these create an audit trail that is defensible in most jurisdictions for lower-stakes agreements like NDAs, consent forms, and service contracts.
Prerequisites
- A FlutterFlow project with Firebase Storage and Firestore connected
- FlutterFlow Pro plan for code export and Custom Widgets
- Firebase project on Blaze plan for Cloud Functions (required for server-side PDF merging with pdf-lib)
- PDF documents to be signed, hosted in Firebase Storage
Step-by-step guide
Set up the Firestore schema for signing requests and audit trail
Set up the Firestore schema for signing requests and audit trail
Create a Firestore collection named 'signingRequests'. Each document represents one document requiring signature(s) and contains: documentId (String), documentUrl (String, Firebase Storage URL to the original PDF), documentHash (String, SHA-256 of the original PDF), documentTitle (String), status (String: 'pending', 'partially_signed', 'completed'), signers (Array of Maps: {userId, email, displayName, signedAt (null or Timestamp), signatureStoragePath (String)}), createdAt (Timestamp), createdBy (String userId), signedDocumentUrl (String, populated after all signatures collected). Also create a 'auditTrail' sub-collection under each signing request document: one document per signing event with fields: action, userId, email, timestamp, ipAddress (optional), documentHashAtTime.
Expected result: Firestore shows the signingRequests collection with the correct field structure and an empty signingRequests/{id}/auditTrail sub-collection.
Render the PDF for review before signing
Render the PDF for review before signing
Add the pdfx package to your exported Flutter project's pubspec.yaml. Create a Custom Widget named 'PdfViewer' that accepts a pdfUrl (String) parameter. Inside, initialize a PdfController by loading the PDF from the network URL using PdfDocument.openFile(). Render it using the PdfView widget from pdfx, which provides page-by-page rendering with pinch-to-zoom. Add previous/next page buttons and a page counter ('Page 2 of 5'). Show a loading spinner while the PDF loads. Below the PdfViewer, add a 'I have read and agree to the terms above' CheckboxListTile that the user must check before the signature canvas appears. This mandatory review step is important for consent validity.
1// Custom Widget: PdfViewer2import 'package:pdfx/pdfx.dart';3import 'package:flutter/material.dart';45class PdfViewerWidget extends StatefulWidget {6 final String pdfUrl;7 const PdfViewerWidget({Key? key, required this.pdfUrl}) : super(key: key);89 @override10 State<PdfViewerWidget> createState() => _PdfViewerWidgetState();11}1213class _PdfViewerWidgetState extends State<PdfViewerWidget> {14 late final PdfControllerPinch _pdfController;15 int _currentPage = 1;16 int _totalPages = 1;1718 @override19 void initState() {20 super.initState();21 _pdfController = PdfControllerPinch(22 document: PdfDocument.openFile(widget.pdfUrl),23 );24 }2526 @override27 void dispose() {28 _pdfController.dispose();29 super.dispose();30 }3132 @override33 Widget build(BuildContext context) {34 return Column(children: [35 Expanded(36 child: PdfViewPinch(37 controller: _pdfController,38 onDocumentLoaded: (doc) =>39 setState(() => _totalPages = doc.pagesCount),40 onPageChanged: (page) =>41 setState(() => _currentPage = page),42 ),43 ),44 Padding(45 padding: const EdgeInsets.all(8),46 child: Text(47 'Page $_currentPage of $_totalPages',48 style: const TextStyle(fontSize: 14),49 ),50 ),51 ]);52 }53}Expected result: The PdfViewer widget renders the PDF document with page navigation and pinch-to-zoom working on a physical device.
Build the signature capture Canvas Custom Widget
Build the signature capture Canvas Custom Widget
Create a Custom Widget named 'SignatureCanvas'. It renders a white rectangular Canvas area with a 'Sign here' prompt. Capture touch points using a GestureDetector with onPanStart, onPanUpdate, and onPanEnd handlers that build a list of Offset points. Use CustomPainter to draw the signature path from these points. Add a 'Clear' button to reset the points list. Add a 'Save Signature' button that converts the Canvas to a PNG image using the RenderRepaintBoundary technique, then calls a callback function with the PNG bytes. The widget should expose a 'isSigned' boolean that returns true if any points have been drawn — use this to disable the Save button until the user has drawn something.
1// Key signature capture logic2import 'dart:ui' as ui;3import 'package:flutter/rendering.dart';4import 'package:flutter/material.dart';56class SignatureCanvas extends StatefulWidget {7 final Function(Uint8List bytes) onSigned;8 const SignatureCanvas({Key? key, required this.onSigned}) : super(key: key);910 @override11 State<SignatureCanvas> createState() => _SignatureCanvasState();12}1314class _SignatureCanvasState extends State<SignatureCanvas> {15 final List<List<Offset>> _strokes = [];16 List<Offset> _currentStroke = [];17 final GlobalKey _repaintKey = GlobalKey();1819 Future<void> _saveSignature() async {20 final boundary = _repaintKey.currentContext!.findRenderObject()21 as RenderRepaintBoundary;22 final image = await boundary.toImage(pixelRatio: 2.0);23 final byteData = await image.toByteData(24 format: ui.ImageByteFormat.png,25 );26 widget.onSigned(byteData!.buffer.asUint8List());27 }2829 @override30 Widget build(BuildContext context) {31 return Column(children: [32 RepaintBoundary(33 key: _repaintKey,34 child: GestureDetector(35 onPanStart: (d) {36 setState(() => _currentStroke = [d.localPosition]);37 },38 onPanUpdate: (d) {39 setState(() => _currentStroke.add(d.localPosition));40 },41 onPanEnd: (_) {42 setState(() {43 _strokes.add(List.from(_currentStroke));44 _currentStroke = [];45 });46 },47 child: Container(48 height: 150,49 decoration: BoxDecoration(50 color: Colors.white,51 border: Border.all(color: Colors.grey.shade300),52 borderRadius: BorderRadius.circular(8),53 ),54 child: CustomPaint(55 painter: _SignaturePainter(56 strokes: _strokes, current: _currentStroke),57 size: Size.infinite,58 ),59 ),60 ),61 ),62 Row(children: [63 TextButton(64 onPressed: () => setState(() {65 _strokes.clear();66 _currentStroke = [];67 }),68 child: const Text('Clear'),69 ),70 ElevatedButton(71 onPressed: _strokes.isEmpty ? null : _saveSignature,72 child: const Text('Save Signature'),73 ),74 ]),75 ]);76 }77}Expected result: The signature canvas accepts touch drawing, renders strokes in real time, and exports a clean PNG of the signature on Save tap.
Upload signature image and merge PDF via Cloud Function
Upload signature image and merge PDF via Cloud Function
When the user saves their signature, a Custom Action named 'submitSignature' runs: Step 1 — upload the signature PNG bytes to Firebase Storage at path 'signatures/{signingRequestId}/{userId}.png' and get the download URL. Step 2 — call a Cloud Function named 'mergeSignatureIntoPdf'. The Cloud Function downloads the original PDF from Firebase Storage, loads it with pdf-lib, creates an image object from the signature PNG, and stamps it at the designated signature field coordinates in the PDF. It saves the merged PDF to 'signed_documents/{signingRequestId}/{userId}_signed.pdf' in Firebase Storage. Step 3 — update the Firestore signing request document: set the signer's 'signedAt' timestamp and 'signatureStoragePath'. Step 4 — write an audit trail document with userId, timestamp, and document hash.
1// Cloud Function: mergeSignatureIntoPdf (Node.js)2const functions = require('firebase-functions');3const admin = require('firebase-admin');4const { PDFDocument } = require('pdf-lib');5const fetch = require('node-fetch');67exports.mergeSignatureIntoPdf = functions.https.onCall(8 async (data, context) => {9 if (!context.auth) {10 throw new functions.https.HttpsError('unauthenticated', 'Login required');11 }12 const { requestId, signatureUrl, signatureX, signatureY,13 signatureWidth, signatureHeight } = data;14 const db = admin.firestore();15 const bucket = admin.storage().bucket();1617 // Load original PDF18 const reqDoc = await db.collection('signingRequests').doc(requestId).get();19 const { documentUrl } = reqDoc.data();20 const pdfResponse = await fetch(documentUrl);21 const pdfBytes = await pdfResponse.arrayBuffer();22 const pdfDoc = await PDFDocument.load(pdfBytes);2324 // Load signature PNG25 const sigResponse = await fetch(signatureUrl);26 const sigBytes = await sigResponse.arrayBuffer();27 const sigImage = await pdfDoc.embedPng(sigBytes);2829 // Stamp signature on last page (or target page)30 const pages = pdfDoc.getPages();31 const targetPage = pages[pages.length - 1];32 targetPage.drawImage(sigImage, {33 x: signatureX,34 y: signatureY,35 width: signatureWidth,36 height: signatureHeight,37 });3839 // Save merged PDF40 const mergedBytes = await pdfDoc.save();41 const outPath = `signed_documents/${requestId}/${context.auth.uid}_signed.pdf`;42 const file = bucket.file(outPath);43 await file.save(Buffer.from(mergedBytes), {44 metadata: { contentType: 'application/pdf' },45 });46 const [signedUrl] = await file.getSignedUrl({47 action: 'read',48 expires: Date.now() + 7 * 24 * 60 * 60 * 1000,49 });50 return { signedDocumentUrl: signedUrl };51 }52);Expected result: After signing, a merged PDF appears in Firebase Storage with the signature visually embedded at the correct position on the document.
Implement the multi-signer workflow and completion logic
Implement the multi-signer workflow and completion logic
After each signature is collected, a Cloud Function triggered on Firestore update checks if all required signers have signed (all signer.signedAt values are non-null). If yes, it updates the document status to 'completed', sends a completion email to all signers via SendGrid with the final signed document attached, and creates a final merged PDF that includes all signatures. For the admin view, create a 'Document Status' page showing a list of signing requests with status indicators: pending (grey), partially signed (yellow, with count 'Signed by 2 of 3'), and completed (green). Clicking a request shows the signer list with individual sign timestamps and a download button for the completed document.
Expected result: When all required signers complete their signatures, the request status updates to 'completed' and all parties receive an email with the final signed document.
Complete working example
1// Document Signing Custom Actions2// Add to FlutterFlow Custom Code panel34import 'dart:convert';5import 'dart:typed_data';6import 'package:crypto/crypto.dart';7import 'package:firebase_storage/firebase_storage.dart';8import 'package:cloud_functions/cloud_functions.dart';9import 'package:firebase_auth/firebase_auth.dart';10import 'package:cloud_firestore/cloud_firestore.dart';1112// ─── Upload signature PNG to Firebase Storage ─────────────────────────────────13Future<String> uploadSignature(14 Uint8List signatureBytes,15 String requestId,16) async {17 final user = FirebaseAuth.instance.currentUser;18 if (user == null) throw Exception('Not authenticated');19 final path = 'signatures/$requestId/${user.uid}.png';20 final ref = FirebaseStorage.instance.ref(path);21 await ref.putData(22 signatureBytes,23 SettableMetadata(contentType: 'image/png'),24 );25 return await ref.getDownloadURL();26}2728// ─── Compute SHA-256 hash of PDF bytes ───────────────────────────────────────29String computeSha256(Uint8List bytes) {30 final digest = sha256.convert(bytes);31 return digest.toString();32}3334// ─── Write audit trail event to Firestore ────────────────────────────────────35Future<void> writeAuditEvent(36 String requestId,37 String action,38 String documentHashAtTime,39) async {40 final user = FirebaseAuth.instance.currentUser;41 if (user == null) return;42 await FirebaseFirestore.instance43 .collection('signingRequests')44 .doc(requestId)45 .collection('auditTrail')46 .add({47 'action': action,48 'userId': user.uid,49 'email': user.email,50 'documentHashAtTime': documentHashAtTime,51 'timestamp': FieldValue.serverTimestamp(),52 });53}5455// ─── Full signing submission flow ─────────────────────────────────────────────56Future<String> submitSignature({57 required Uint8List signatureBytes,58 required String requestId,59 required String originalDocumentHash,60 required double signatureX,61 required double signatureY,62 required double signatureWidth,63 required double signatureHeight,64}) async {65 // 1. Upload signature image66 final signatureUrl =67 await uploadSignature(signatureBytes, requestId);6869 // 2. Call Cloud Function to merge PDF70 final cf = FirebaseFunctions.instance;71 final result = await cf72 .httpsCallable('mergeSignatureIntoPdf')73 .call({74 'requestId': requestId,75 'signatureUrl': signatureUrl,76 'signatureX': signatureX,77 'signatureY': signatureY,78 'signatureWidth': signatureWidth,79 'signatureHeight': signatureHeight,80 });8182 // 3. Write audit trail83 await writeAuditEvent(84 requestId, 'document_signed', originalDocumentHash);8586 return result.data['signedDocumentUrl'] as String;87}Common mistakes when creating a Custom Document Signing Feature in FlutterFlow
Why it's a problem: Storing signature images as base64 strings directly in Firestore documents
How to avoid: Upload signature images to Firebase Storage and store only the Storage path or download URL in Firestore (a string of 100-200 characters). Firebase Storage is built for binary file storage and serves the image efficiently on demand.
Why it's a problem: Merging PDF signatures client-side on the mobile device using a Dart PDF library
How to avoid: Always merge signatures server-side via a Cloud Function using pdf-lib. The server has consistent, ample memory, produces a reliable signed PDF, and immediately stores it in Firebase Storage where all parties can access it.
Why it's a problem: Not establishing the document hash before the first signature is collected
How to avoid: Compute the SHA-256 hash of the original PDF when the signing request is created (before any signature), and store it in the signingRequests document as 'originalDocumentHash'. Record this hash in every audit trail event.
Best practices
- Always require users to scroll through or explicitly confirm they have reviewed the document before revealing the signature canvas — this establishes evidence of informed consent.
- Use Firebase Authentication's verified identity (email confirmed) for all signers — unsigned-in users cannot be legally identified as document signers.
- Send an immediate email confirmation to each signer with the signed document attached after they sign, creating a contemporaneous record outside your own system.
- Set an expiration date on signing requests — documents that have been pending for 30 days with some signers who have not yet signed should expire and require re-issuance.
- Store signed documents in a separate Firebase Storage bucket or folder with different retention rules from user-uploaded content — signed legal documents should never be auto-deleted.
- For high-stakes agreements, consider integrating with a dedicated e-signature API (DocuSign, HelloSign) that provides legally recognized e-signature infrastructure instead of building your own.
- Include the RapidDev Engineering Team contact in your admin UI for any custom document signing workflows that require special multi-party orchestration logic beyond standard FlutterFlow capabilities.
Still stuck?
Copy one of these prompts to get a personalized, step-by-step explanation.
I'm building a document signing feature in a FlutterFlow app using Firebase. I have a Cloud Function that receives a signing request: {requestId, signatureUrl (Firebase Storage URL to signature PNG), pdfUrl (Firebase Storage URL to original PDF)}. The function should download both files, use pdf-lib to embed the signature image at coordinates (x:100, y:50, width:200, height:80) on the last page of the PDF, save the merged PDF to Firebase Storage, and return the signed document URL. Write the complete Node.js Cloud Function code including firebase-admin Storage operations and pdf-lib integration.
I have a FlutterFlow Custom Widget that captures a handwritten signature on a Canvas. The widget correctly draws strokes and clears them. Now I need to export the Canvas content as a PNG byte array and pass it to my FlutterFlow Custom Action for Firebase Storage upload. Show me the specific Dart code to convert the RepaintBoundary to Uint8List bytes, and explain how to pass this byte data from the Custom Widget callback to a FlutterFlow Custom Action.
Frequently asked questions
Is an in-app signature legally binding?
In most jurisdictions (US, EU, UK, Australia), electronic signatures are legally valid under the ESIGN Act, eIDAS, and equivalent laws, provided you can demonstrate: the signer's identity (authenticated user), their intent to sign (deliberate action with consent confirmation), and document integrity (hash verification shows the document wasn't altered after signing). For high-value contracts, consult a lawyer and consider a dedicated e-signature service like DocuSign that has established legal precedent.
How do I handle a signer who wants to decline or refuse to sign?
Add a 'Decline to Sign' button alongside the signature canvas. On tap, update the Firestore signing request document with the decliner's reason, write a 'document_declined' audit trail event, update the status to 'declined', and notify the document creator by email. Design your workflow so document creators can choose whether a single declination voids the entire request or only prevents that signer's participation.
Can I pre-fill signature fields at specific positions on the PDF?
Yes. Define signature field coordinates in your signingRequests document as a 'signatureFields' Array of Maps: [{signerIndex: 0, pageNumber: 3, x: 100, y: 50, width: 200, height: 80}]. When rendering the PDF for signing, overlay a visual 'Sign here' marker at those coordinates using a Stack widget. Pass the coordinates to your Cloud Function when merging the signature so it places it at the exact correct position.
How do I support multiple signature pages or signatures on the same document?
Allow each signing session to include multiple signature captures — one per designated field. In the signature canvas workflow, loop through each required signature field, collect a signature for each, upload all to Storage, and pass the full array of {signatureUrl, pageNumber, x, y, width, height} to the Cloud Function. The Cloud Function iterates over all signature entries and applies each to its designated page and coordinates before saving the merged PDF.
What happens to the audit trail if a user disputes having signed the document?
Your Firestore audit trail records: the user's authenticated UID and email, the timestamp (from server-side FieldValue.serverTimestamp()), the hash of the document at the time of signing, and the signature image Storage path. Firebase Authentication ensures the UID cannot be impersonated by another user. The document hash proves the document content was not changed after signing. This combination creates a strong, timestamped record of the signing event.
How do I allow users to void or revoke a signed document?
Add a 'Void Document' action visible only to the document creator in the admin UI. Voiding should not delete the signed document or audit trail — these are permanent records. Instead, write a 'document_voided' event to the audit trail and update the status field to 'voided' with a reason field. Notify all signers by email that the document has been voided. The original signed PDF and audit trail remain in Firestore and Storage as evidence of the original transaction.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation