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

How to Create a Custom Document Signing Feature in FlutterFlow

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.

What you'll learn

  • How to render PDF documents inside a FlutterFlow app using the pdfx package
  • How to capture a handwritten signature on a Canvas Custom Widget and save it to Firebase Storage
  • How to use a Cloud Function with pdf-lib to merge a signature image onto a PDF
  • How to create an immutable audit trail with document hash, signer identity, and timestamp
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Beginner13 min read55-65 minFlutterFlow Pro+ (code export required for Canvas signature widget and pdf-lib integration)March 2026RapidDev Engineering Team
TL;DR

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

1

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.

2

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.

pdf_viewer_widget.dart
1// Custom Widget: PdfViewer
2import 'package:pdfx/pdfx.dart';
3import 'package:flutter/material.dart';
4
5class PdfViewerWidget extends StatefulWidget {
6 final String pdfUrl;
7 const PdfViewerWidget({Key? key, required this.pdfUrl}) : super(key: key);
8
9 @override
10 State<PdfViewerWidget> createState() => _PdfViewerWidgetState();
11}
12
13class _PdfViewerWidgetState extends State<PdfViewerWidget> {
14 late final PdfControllerPinch _pdfController;
15 int _currentPage = 1;
16 int _totalPages = 1;
17
18 @override
19 void initState() {
20 super.initState();
21 _pdfController = PdfControllerPinch(
22 document: PdfDocument.openFile(widget.pdfUrl),
23 );
24 }
25
26 @override
27 void dispose() {
28 _pdfController.dispose();
29 super.dispose();
30 }
31
32 @override
33 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.

3

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.

signature_canvas_widget.dart
1// Key signature capture logic
2import 'dart:ui' as ui;
3import 'package:flutter/rendering.dart';
4import 'package:flutter/material.dart';
5
6class SignatureCanvas extends StatefulWidget {
7 final Function(Uint8List bytes) onSigned;
8 const SignatureCanvas({Key? key, required this.onSigned}) : super(key: key);
9
10 @override
11 State<SignatureCanvas> createState() => _SignatureCanvasState();
12}
13
14class _SignatureCanvasState extends State<SignatureCanvas> {
15 final List<List<Offset>> _strokes = [];
16 List<Offset> _currentStroke = [];
17 final GlobalKey _repaintKey = GlobalKey();
18
19 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 }
28
29 @override
30 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.

4

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.

merge_signature_function.js
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');
6
7exports.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();
16
17 // Load original PDF
18 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);
23
24 // Load signature PNG
25 const sigResponse = await fetch(signatureUrl);
26 const sigBytes = await sigResponse.arrayBuffer();
27 const sigImage = await pdfDoc.embedPng(sigBytes);
28
29 // 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 });
38
39 // Save merged PDF
40 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.

5

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

document_signing_helpers.dart
1// Document Signing Custom Actions
2// Add to FlutterFlow Custom Code panel
3
4import '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';
11
12// ─── 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}
27
28// ─── Compute SHA-256 hash of PDF bytes ───────────────────────────────────────
29String computeSha256(Uint8List bytes) {
30 final digest = sha256.convert(bytes);
31 return digest.toString();
32}
33
34// ─── 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.instance
43 .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}
54
55// ─── 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 image
66 final signatureUrl =
67 await uploadSignature(signatureBytes, requestId);
68
69 // 2. Call Cloud Function to merge PDF
70 final cf = FirebaseFunctions.instance;
71 final result = await cf
72 .httpsCallable('mergeSignatureIntoPdf')
73 .call({
74 'requestId': requestId,
75 'signatureUrl': signatureUrl,
76 'signatureX': signatureX,
77 'signatureY': signatureY,
78 'signatureWidth': signatureWidth,
79 'signatureHeight': signatureHeight,
80 });
81
82 // 3. Write audit trail
83 await writeAuditEvent(
84 requestId, 'document_signed', originalDocumentHash);
85
86 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.

ChatGPT Prompt

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.

FlutterFlow Prompt

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.

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.