# How to Add Document Signing to Your App (Copy-Paste Prompts Included)

- Tool: App Features
- Last updated: July 2026

## TL;DR

Document signing needs three pieces beyond a signature capture widget: a third-party e-signature API (Dropbox Sign or DocuSign) that creates the legally binding audit trail, a Supabase Edge Function that handles webhooks and keeps your API keys server-side, and precise PDF coordinate mapping so the signature lands on the right spot. With Lovable or V0 you can wire a working signing flow in 3–5 days. Running cost starts at $20–$30/month for the e-signature service; storage and webhooks run on Supabase at $0–$25/month.

## What Document Signing Actually Is

Document signing lets your app collect legally binding electronic signatures on contracts, agreements, and forms. A SaaS startup collects onboarding contracts from new customers without printing anything. A freelance platform has clients sign project agreements before work begins. A real estate app enables lease signing on mobile. The collect-a-signature widget is the easy part — any canvas element can capture a drawn signature. The legally binding part requires an e-signature service (Dropbox Sign or DocuSign) that creates a timestamped, tamper-evident audit trail compliant with the ESIGN Act (US) or eIDAS (EU). Your app calls the API, the service hosts the signing ceremony, and a webhook notifies your app when signing completes — so your API keys never travel to the browser and the legal trail lives in the provider's infrastructure.

## Anatomy of the Feature

Six components — the e-signature API integration and the webhook handler are where builds most commonly stall. PDF coordinate mapping and Supabase Storage private bucket setup are the two details that break the first demo.

- **Signature capture widget** (ui): react-signature-canvas (web) renders a canvas element where users draw their signature with mouse or touch. A tab switcher offers three modes: Draw (canvas), Type (typed name rendered in a Google Fonts cursive font), and Upload (file input accepting PNG/JPG). On mobile Flutter: the hand_signature package or syncfusion_flutter_signaturepad.
- **Document renderer with field placement** (ui): react-pdf (pdfjs-dist) renders the base PDF in the browser as a canvas or SVG. Signature fields are overlaid as absolutely-positioned div elements using react-dnd or react-draggable, allowing the document owner to drag and drop signature fields to the exact position. Field coordinates are stored as percentage of page width and height for device-independent positioning.
- **E-signature API integration** (service): Dropbox Sign (formerly HelloSign) or DocuSign REST API manages the legally binding signing workflow, audit trail generation, and tamper-evident document sealing. Called exclusively from a Supabase Edge Function — the API key must never be sent to the client. Dropbox Sign's embedded signing flow or redirect to the provider's hosted page both achieve compliance.
- **Signed document storage** (backend): Supabase Storage private bucket stores both the unsigned template PDF and the completed signed PDF. The signed version arrives as a download URL in the webhook payload from the e-sign provider after the signing ceremony completes. The signed file is downloaded server-side and re-uploaded to Supabase Storage, then linked in the signing_requests table.
- **Signing status + webhook handler** (backend): A Supabase Edge Function receives POST requests from Dropbox Sign or DocuSign when events occur: signature_request_sent, signature_request_signed, signature_request_all_signed, and signature_request_declined. The handler verifies the webhook signature (HMAC), updates the signing_requests and signers tables, downloads the signed PDF on completion, and sends email notification via Resend API.
- **Multi-party orchestration** (data): The signing_requests table tracks document metadata and overall status. The signers table tracks each individual signer with their order_index, status, and signed_at timestamp. When a signer completes, the webhook handler checks whether all previous-order signers are done before sending the invitation to the next signer.

## Data model

Three tables: documents stores the PDF files, signing_requests tracks each signing workflow, and signers tracks each individual signer's status and audit data. The Edge Function uses the service role key for all status updates. Run this in the Supabase SQL editor:

```sql
create table public.documents (
  id uuid primary key default gen_random_uuid(),
  owner_id uuid references auth.users(id) on delete cascade not null,
  name text not null,
  file_path text not null,
  signed_file_path text,
  created_at timestamptz not null default now()
);

create table public.signing_requests (
  id uuid primary key default gen_random_uuid(),
  document_id uuid references public.documents(id) on delete cascade not null,
  created_by uuid references auth.users(id) on delete set null not null,
  status text not null default 'pending'
    check (status in ('pending', 'in_progress', 'completed', 'declined', 'expired')),
  external_ref text unique,
  audit_log jsonb not null default '[]',
  completed_at timestamptz,
  created_at timestamptz not null default now()
);

create table public.signers (
  id uuid primary key default gen_random_uuid(),
  request_id uuid references public.signing_requests(id) on delete cascade not null,
  name text not null,
  email text not null,
  order_index int not null default 1,
  status text not null default 'pending'
    check (status in ('pending', 'sent', 'viewed', 'signed', 'declined')),
  signed_at timestamptz,
  ip_address text
);

create index signing_requests_owner_idx
  on public.signing_requests (created_by, created_at desc);

create index signers_request_idx
  on public.signers (request_id, order_index);

alter table public.documents enable row level security;
alter table public.signing_requests enable row level security;
alter table public.signers enable row level security;

create policy "Owners can manage their documents"
  on public.documents for all
  using (auth.uid() = owner_id);

create policy "Request creator can select signing requests"
  on public.signing_requests for select
  using (auth.uid() = created_by);

create policy "Request creator can insert signing requests"
  on public.signing_requests for insert
  with check (auth.uid() = created_by);

create policy "Signers can view their own signer row"
  on public.signers for select
  using (
    request_id in (
      select id from public.signing_requests
      where created_by = auth.uid()
    )
  );
```

The Edge Function webhook handler must use the Supabase service role key (not the anon key) to update signing_requests status and insert into signers after signing events — the RLS policies above do not allow anonymous updates. Store the service role key in the Edge Function's Secrets in the Lovable Cloud tab, never in client-side code. The external_ref column stores the Dropbox Sign or DocuSign signature_request_id for webhook correlation.

## Build paths

### Lovable — fit 3/10, 3–5 days

Lovable can wire the Dropbox Sign or DocuSign API via Edge Functions and handle the signing flow UI. Use Plan Mode to architect the signing flow before Agent Mode builds it — this is too complex for a single unstructured prompt.

1. In Plan Mode, describe the signing flow: document upload, signer email collection, API call to create the signing request, webhook handling, and signed PDF download. Review the plan before switching to Agent Mode.
2. Add HELLOSIGN_API_KEY (or DOCUSIGN_ACCESS_TOKEN) to Lovable Cloud tab → Secrets; the Edge Function will access it via Deno.env.get()
3. Run the SQL schema from this page in the Supabase SQL editor to create documents, signing_requests, and signers tables
4. In Agent Mode, paste the prompt below; the Edge Function for creating signing requests and the webhook handler are the two critical pieces to verify after generation
5. Deploy to your custom domain or Vercel before registering the webhook URL with Dropbox Sign or DocuSign — preview URLs are not stable and will not receive webhook events
6. Test by sending a signing request to yourself and verifying the webhook updates the signing_requests status and triggers the Resend confirmation email

Starter prompt:

```
Build a document signing feature using the Dropbox Sign API. Three pages: 1) Upload & Send: file upload input for PDF (stored in Supabase Storage private bucket 'documents'), text fields for signer name and email (add a second signer row with an 'Add another signer' button for multi-party signing), submit button that calls a Supabase Edge Function POST /create-signing-request. The Edge Function: downloads the uploaded PDF from Supabase Storage, calls the Dropbox Sign API (api.hellosign.com/v3/signature_request/send_with_template or /send) with the signer list and their order indices, stores the returned signature_request_id in signing_requests.external_ref. 2) Status page: list signing_requests for the current user showing document name, status badge (pending/in_progress/completed/declined), signer names with their individual status, and a Download Signed PDF button that generates a Supabase Storage signed URL (expiry 3600 seconds) for completed documents. 3) Webhook handler at /functions/v1/signing-webhook: verify X-HelloSign-Signature HMAC header using HELLOSIGN_WEBHOOK_KEY from Secrets; on signature_request_all_signed event, download the signed PDF from the Dropbox Sign API, upload it to Supabase Storage as signed_file_path, update signing_requests status to 'completed', update each signer's status and signed_at, send completion email via Resend to all parties; on signature_request_declined, update status to 'declined' and notify the document creator. Handle errors: if the API call fails, show an inline error on the Upload & Send page. If a signer declines, update their status to 'declined' and stop the sequence.
```

Limitations:

- PDF rendering with overlaid draggable signature fields (for custom field placement) is complex — Lovable may generate a simplified version where the signature is appended at a default position rather than placed at custom coordinates
- The Lovable preview iframe is not a stable public URL; webhooks from Dropbox Sign will never reach it. Test only on the published production URL
- Multi-party sequential signing logic (waiting for signer 1 before sending to signer 2) should be handled by Dropbox Sign's native order_index parameter, not by custom webhook orchestration — verify the Edge Function passes order indices correctly

### V0 — fit 3/10, 3–5 days

V0 produces a clean signing UI and Next.js API routes handle DocuSign or Dropbox Sign webhooks reliably. Better for embedding a signing flow into an existing Next.js SaaS than building a standalone signing app.

1. Prompt V0 with the spec below to generate the upload page, status dashboard, and signing-related components
2. Add environment variables in the V0 Vars panel: NEXT_PUBLIC_SUPABASE_URL, NEXT_PUBLIC_SUPABASE_ANON_KEY, SUPABASE_SERVICE_ROLE_KEY, HELLOSIGN_API_KEY (or DOCUSIGN_ACCESS_TOKEN), HELLOSIGN_WEBHOOK_KEY
3. Run the SQL schema from this page in the Supabase SQL editor
4. Deploy to Vercel (via the V0 Git panel) before registering the /api/signing-webhook URL with Dropbox Sign or DocuSign — the V0 sandbox preview cannot receive webhooks
5. Verify the /api/create-signing-request route uses HELLOSIGN_API_KEY from process.env (not a NEXT_PUBLIC_ variable) — the API key must stay server-side

Starter prompt:

```
Build a Next.js document signing feature using Dropbox Sign API. Page 1 (/sign/new): PDF file upload (store in Supabase Storage private bucket), signer form with name + email fields plus an 'Add signer' button for up to 5 signers, submit calls POST /api/create-signing-request. API route /api/create-signing-request (server-side only, uses HELLOSIGN_API_KEY from process.env): reads uploaded PDF from Supabase Storage, calls Dropbox Sign API to create a signature_request with signers array including order indices, inserts into signing_requests table (status: 'pending', external_ref: signature_request_id) and signers table via Supabase service role client, returns the signing URL for embedded signing or redirect. Page 2 (/sign/dashboard): lists signing_requests for the authenticated user with document name, status badge, each signer's name and status indicator (pending/signed/declined), and a 'Download Signed PDF' button that calls GET /api/signed-url/[requestId] to generate a 3600-second Supabase Storage signed URL for completed documents. API route /api/signing-webhook (POST): verify HMAC signature from X-HelloSign-Signature header using HELLOSIGN_WEBHOOK_KEY; on signature_request_all_signed — download signed PDF from Dropbox Sign download API, upload to Supabase Storage as signed_file_path, update signing_requests status to 'completed', update signers table with signed_at and ip_address from event payload, send Resend email to all parties with download link; on signature_request_declined — update status to 'declined', notify creator via Resend. Handle: signer sees a 'Thank you for signing' confirmation page after signing via redirect; document owner sees status update in real-time via Supabase Realtime on the dashboard.
```

Limitations:

- react-pdf + react-dnd for drag-and-drop signature field placement on the PDF is achievable but requires careful prompting and typically one follow-up correction prompt after initial generation
- Multi-party sequential signing needs explicit prompt instruction; V0 tends to scaffold the happy-path single-signer flow without the ordered invitation chain
- DocuSign's OAuth flow is more complex than Dropbox Sign's API key authentication — Dropbox Sign is recommended for V0-assisted builds to reduce integration complexity

### Flutterflow — fit 2/10, 3–7 days

FlutterFlow can capture signatures using a signature pad widget and upload them to Supabase Storage. Use only if the 'signature' is a standalone form field — not positioned on a specific PDF page. Full document-level signing is not feasible in FlutterFlow without extensive custom Dart code.

1. Add the syncfusion_flutter_signaturepad or hand_signature package via FlutterFlow's custom packages section
2. Add a SignaturePad widget to your form page; on Save action, convert the signature to bytes and upload to Supabase Storage
3. Store the returned Supabase Storage URL in a form_responses or contracts table field
4. For legally binding requirements, call a Supabase Edge Function via an HTTP request action to trigger the Dropbox Sign API with the signer info and document

Limitations:

- No native PDF renderer in FlutterFlow — displaying a PDF with overlaid signature fields requires a WebView widget wrapping a hosted signing page
- Document-level signing (signature placed at specific PDF coordinates) is not achievable in FlutterFlow's visual editor without a fully custom widget
- Multi-party orchestration, webhook handling, and audit trail management all require custom Dart code outside the visual action editor

### Custom — fit 5/10, 2–4 weeks

Required for multi-party enterprise contract workflows, eIDAS Qualified Electronic Signature compliance, or in-person kiosk signing. The only path where the audit trail, PDF sealing, and legal defensibility are production-grade from day one.

1. Next.js App Router with DocuSign or Adobe Sign API integration via server-side API routes; Supabase for document storage, status tracking, and webhook handling
2. react-pdf for PDF rendering in the browser; react-dnd for drag-and-drop signature field placement during document setup; coordinate conversion from CSS pixels to PDF points on save
3. HMAC webhook verification with retry handling — failed webhook deliveries from DocuSign retry for 72 hours; the handler must be idempotent (same event processed twice = same result)
4. Audit log completeness review with legal counsel: ensure the signing certificate contains IP address, timestamp, document hash, and provider-issued audit trail before representing the solution as ESIGN Act or eIDAS compliant

Limitations:

- Legal review of the audit trail and signing certificate is recommended before using in high-stakes contracts (real estate, employment, financial services) — the technical implementation alone does not guarantee compliance
- Adobe AATL certificate-based signing (for eIDAS QES) requires a different technical approach and is significantly more expensive than standard API-based signing

## Gotchas

- **Webhook from Dropbox Sign or DocuSign never arrives — signing status stays 'pending' indefinitely** — Lovable's preview pane (*.lovable.app/preview) and V0's sandbox are not stable, publicly accessible URLs. Dropbox Sign and DocuSign require a registered webhook endpoint that responds to HTTP POST requests from the public internet. Preview URLs are either private, behind an iframe, or change between sessions — none of these work as webhook targets. Fix: Deploy your app to a custom domain or Vercel production environment before registering the webhook URL with the signing provider. In Lovable, use the published app URL (not the preview pane URL). In V0, deploy via the Git panel to Vercel and use the Vercel production URL. Re-register the webhook URL in the Dropbox Sign or DocuSign dashboard pointing to /api/signing-webhook or /functions/v1/signing-webhook.
- **Signature field appears in the wrong position on the PDF — shifted or offset from where the user placed it** — PDF coordinates use a bottom-left origin measured in points (72 points per inch), while the browser UI overlay uses a top-left origin in CSS pixels. The page zoom level and DPI scaling add additional offset. AI-generated code commonly passes the raw CSS pixel position directly to the PDF API, which places the signature in the wrong location — often off the page entirely. Fix: Convert the CSS pixel position to PDF points before sending to the API. For Dropbox Sign: `x_pos = Math.round((css_x / container_width_px) * page_width_pts)` and `y_pos = Math.round(page_height_pts - ((css_y + field_height_px) / container_height_px) * page_height_pts)`. The y-axis inversion is critical — CSS counts from the top, PDF counts from the bottom.
- **'Request failed with status 403' when calling Dropbox Sign API** — The Dropbox Sign API key was set as a VITE_HELLOSIGN_API_KEY environment variable (with the VITE_ prefix) and is being called from client-side React code. Dropbox Sign rejects requests from browsers because the API key would be exposed in the browser's network requests to anyone who inspects the app. Fix: Move the Dropbox Sign API call to a Supabase Edge Function. Store the API key in Lovable's Cloud tab → Secrets (or Vercel's environment variables for V0), accessible via Deno.env.get('HELLOSIGN_API_KEY') in the Edge Function. The Edge Function is called from your client-side code; the API key never leaves the server.
- **Signed PDF download link expires before the user clicks it** — Supabase Storage signed URLs have a configurable expiry; AI-generated code commonly calls createSignedUrl() on page mount and passes the resulting URL directly to the download button. If the user stays on the page for more than 60 seconds (the default expiry), the URL has expired by the time they click download — resulting in a 403 or 'The resource you are looking for has been removed' error. Fix: Generate the Supabase Storage signed URL on button click, not on page mount. Call createSignedUrl(path, 3600) — a 1-hour expiry — at the moment the user clicks the Download button, then open the returned URL immediately. This ensures the URL is always fresh when used.

## Best practices

- Always call the e-signature API from a Supabase Edge Function or Next.js Server Action — never from client-side code where the API key would be visible in browser network requests
- Verify the webhook HMAC signature header before processing any event — Dropbox Sign sends X-HelloSign-Signature, DocuSign sends X-DocuSign-Signature-1; reject any webhook that fails verification
- Make your webhook handler idempotent: processing the same event twice should produce the same result without creating duplicate rows or sending duplicate emails — use a unique constraint on external_ref in signing_requests
- Generate Supabase Storage signed URLs on button click with a 3600-second expiry, not on page load — URLs generated on page load expire before users act on them
- Store the complete signing event payload from the provider in the audit_log jsonb column for every status change — this becomes the legal record if a signing dispute arises
- Test the decline flow before launch: what happens if a signer declines is often untested and leaves document owners with a 'pending' status that never resolves
- Use Dropbox Sign's embedded signing (iFrame mode) for a smoother in-app experience rather than redirecting users to the provider's hosted page — it keeps users in your app while the signing happens

## Frequently asked questions

### Is a signature collected with react-signature-canvas legally binding?

A canvas signature alone is not legally binding — it's just an image of a squiggle. Legal validity under the ESIGN Act requires an intent to sign, an audit trail with timestamps and signer identity, and tamper-evident document sealing. That's what e-signature APIs like Dropbox Sign and DocuSign provide. Use react-signature-canvas to capture the signature image, then submit it to the provider's API to create the legally binding record.

### What's the difference between Dropbox Sign and DocuSign for a startup?

Dropbox Sign is easier to integrate (API key authentication, simpler endpoint structure), cheaper for low volumes ($20/mo for 15 docs/mo, approx), and has a better developer experience for custom integrations. DocuSign is the enterprise standard: more signers per envelope, eIDAS compliance, Salesforce integration, and a name that enterprise legal teams recognize and trust. Start with Dropbox Sign; migrate to DocuSign when a customer's legal team requires it.

### Can multiple people sign the same document in sequence?

Yes. Both Dropbox Sign and DocuSign support sequential signing via an order_index on each signer. The provider sends the signing invitation to signer 1 first; only after they complete (or the flow reaches their position in the sequence) does the invitation go to signer 2. Your database mirrors this state in the signers table via webhook events.

### How do I store signed PDFs securely in Supabase?

Use a private Supabase Storage bucket with RLS policies that allow only the document owner and named signers to access files. Download the signed PDF from the provider's API in your webhook handler (server-side, using the service role key), upload it to Supabase Storage, and store the path in signing_requests.signed_file_path. Generate short-lived signed URLs (3600 seconds) on demand when a user clicks download — never store or display public URLs to private documents.

### Why does my webhook handler not receive events from DocuSign?

The most common causes: (1) The webhook URL is a preview or local URL — both Dropbox Sign and DocuSign require a publicly accessible HTTPS endpoint, so you must deploy to production before registering the webhook. (2) The HMAC signature verification is failing and your handler returns a 400 before processing — check the verification logic against the provider's documentation. (3) The URL was registered with a trailing slash that doesn't match the route.

### How do I add a signature field to a specific location on a PDF?

Render the PDF with react-pdf (pdfjs-dist), overlay absolutely-positioned div elements for signature fields, and let users drag them to the desired position. Store field coordinates as percentages of the page dimensions. Before sending to Dropbox Sign or DocuSign, convert to PDF points: x_pts = (x_fraction * page_width_pts) and y_pts = page_height_pts - (y_fraction * page_height_pts). The y-axis is inverted because PDF coordinates start at the bottom-left.

### Can FlutterFlow handle document signing on mobile?

FlutterFlow can capture a signature image using a signature pad widget and upload it to Supabase. For a standalone signature-as-form-field use case (like collecting agreement at the end of an intake form), that's sufficient. For true document signing — where the signature appears at a specific position in a PDF with a legal audit trail — FlutterFlow cannot render a PDF with overlaid fields natively. A WebView pointing to a Dropbox Sign embedded signing URL is the practical workaround.

### How much does it cost to add e-signatures to my app?

Budget $20–$30/month to start: Dropbox Sign Essentials ($20/mo, approx) covers 15 signed documents per month, which is enough for most early-stage apps. Supabase handles document storage on its free tier. As volume grows, Dropbox Sign Standard ($30/mo, approx) is unlimited documents. Enterprise volumes require custom pricing from DocuSign or Dropbox Sign. The signature capture widget itself (react-signature-canvas) is free.

---

Source: https://www.rapidevelopers.com/app-features/document-signing
© RapidDev — https://www.rapidevelopers.com/app-features/document-signing
