Documentation Index
Fetch the complete documentation index at: https://docs.firma.dev/llms.txt
Use this file to discover all available pages before exploring further.
Add legally binding e-signatures to any Supabase application. Use Supabase Edge Functions to call the Firma API, store signing status in your Postgres database, and receive real-time updates via webhooks.
What you can build
- Contract signing flows — trigger signing requests when users complete a form or checkout
- Document status dashboards — track which documents are pending, signed, or declined
- Embedded signing — let users sign documents without leaving your app
- Automated workflows — use Firma webhooks + Supabase database triggers to kick off downstream actions when documents are signed
Prerequisites
Architecture overview
Your Frontend (React, Next.js, Flutter, etc.)
│
▼
Supabase Edge Function ──► Firma API
│ │
▼ │ (webhook)
Supabase Postgres ◄──────────┘
(signing_requests table)
Your frontend calls a Supabase Edge Function, which securely calls the Firma API to create signing requests. When recipients sign (or decline), Firma sends a webhook to another Edge Function that updates your database.
Step 1: Store your Firma API key
Add your Firma API key as a Supabase secret so Edge Functions can access it securely:
supabase secrets set FIRMA_API_KEY=your_api_key_here
Step 2: Create a table to track signing requests
Run this SQL in the Supabase SQL Editor to create a table for tracking document status:
create table signing_requests (
id uuid primary key default gen_random_uuid(),
firma_request_id text unique,
user_id uuid references auth.users(id),
template_id text,
status text default 'pending',
created_at timestamptz default now(),
updated_at timestamptz default now()
);
-- Enable Row Level Security
alter table signing_requests enable row level security;
-- Users can only see their own signing requests
create policy "Users can view own requests"
on signing_requests for select
using (auth.uid() = user_id);
Step 3: Create an Edge Function to send signing requests
Generate a new Edge Function:
supabase functions new send-signing-request
Then replace the contents of supabase/functions/send-signing-request/index.ts:
import { createClient } from "https://esm.sh/@supabase/supabase-js@2";
const corsHeaders = {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Headers": "authorization, content-type",
};
Deno.serve(async (req) => {
if (req.method === "OPTIONS") {
return new Response("ok", { headers: corsHeaders });
}
const supabase = createClient(
Deno.env.get("SUPABASE_URL")!,
Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!
);
const { template_id, signer_email, signer_first_name, signer_last_name } =
await req.json();
// Get the authenticated user
const authHeader = req.headers.get("Authorization")!;
const {
data: { user },
} = await supabase.auth.getUser(authHeader.replace("Bearer ", ""));
if (!user) {
return new Response(JSON.stringify({ error: "Unauthorized" }), {
status: 401,
headers: { ...corsHeaders, "Content-Type": "application/json" },
});
}
// Create a signing request via Firma API
const firmaResponse = await fetch(
"https://api.firma.dev/functions/v1/signing-request-api/signing-requests",
{
method: "POST",
headers: {
Authorization: Deno.env.get("FIRMA_API_KEY"),
"Content-Type": "application/json",
},
body: JSON.stringify({
template_id,
recipients: [
{
first_name: signer_first_name,
last_name: signer_last_name,
email: signer_email,
designation: "Signer",
order: 1,
},
],
}),
}
);
const firmaData = await firmaResponse.json();
if (!firmaResponse.ok) {
return new Response(JSON.stringify({ error: firmaData }), {
status: firmaResponse.status,
headers: { ...corsHeaders, "Content-Type": "application/json" },
});
}
// Store the signing request in your database
const { error } = await supabase.from("signing_requests").insert({
firma_request_id: firmaData.id,
user_id: user.id,
template_id,
status: "pending",
});
if (error) {
return new Response(JSON.stringify({ error: error.message }), {
status: 500,
headers: { ...corsHeaders, "Content-Type": "application/json" },
});
}
return new Response(JSON.stringify(firmaData), {
status: 201,
headers: { ...corsHeaders, "Content-Type": "application/json" },
});
});
Deploy it:
supabase functions deploy send-signing-request
Step 4: Handle Firma webhooks
Create another Edge Function to receive webhook events from Firma when documents are signed, declined, or updated:
supabase functions new firma-webhook
Replace the contents of supabase/functions/firma-webhook/index.ts:
import { createClient } from "https://esm.sh/@supabase/supabase-js@2";
Deno.serve(async (req) => {
const supabase = createClient(
Deno.env.get("SUPABASE_URL")!,
Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!
);
const payload = await req.json();
const { type, data } = payload;
// Map Firma event types to a simple status
const statusMap: Record<string, string> = {
"signing_request.completed": "completed",
"signing_request.cancelled": "cancelled",
"signing_request.expired": "expired",
"signing_request.sent": "sent",
"signing_request.recipient.signed": "recipient_signed",
"signing_request.recipient.declined": "declined",
};
const status = statusMap[type];
if (!status) {
// Event type we don't need to track
return new Response(JSON.stringify({ received: true }), { status: 200 });
}
// The signing request ID is nested inside data.signing_request
const signingRequestId = data.signing_request?.id;
if (!signingRequestId) {
return new Response(JSON.stringify({ error: "Missing signing request ID" }), {
status: 400,
});
}
const { error } = await supabase
.from("signing_requests")
.update({
status,
updated_at: new Date().toISOString(),
})
.eq("firma_request_id", signingRequestId);
if (error) {
return new Response(JSON.stringify({ error: error.message }), {
status: 500,
});
}
return new Response(JSON.stringify({ received: true }), { status: 200 });
});
Deploy it and make it publicly accessible (webhooks don’t send auth headers):
supabase functions deploy firma-webhook --no-verify-jwt
Then register the webhook URL in your Firma dashboard under Settings → Webhooks. Your endpoint URL will be:
https://<your-project-ref>.supabase.co/functions/v1/firma-webhook
For production use, verify the webhook signature using your Firma webhook signing secret. See the webhooks guide for details on signature verification.
Step 5: Embed the signing experience (optional)
For an in-app signing experience, use Firma’s embeddable signing component. Once you have the signing_request_user_id from the API response, load it in an iframe:
<iframe
src="https://app.firma.dev/signing/{signing_request_user_id}"
style="width:100%;height:900px;border:0;"
allow="camera;microphone;clipboard-write"
title="Document Signing"
></iframe>
See the embedded signing guide for full setup instructions including security best practices.
Step 6: Query signing status from your frontend
With Row Level Security in place, your frontend can query the signing status directly:
const { data: requests } = await supabase
.from("signing_requests")
.select("*")
.order("created_at", { ascending: false });
Next steps