Saltar al contenido principal
Añade firmas electrónicas legalmente vinculantes a cualquier aplicación de Supabase. Usa Supabase Edge Functions para llamar a la API de Firma, guarda el estado de la firma en tu base de datos Postgres y recibe actualizaciones en tiempo real mediante webhooks.

Qué puedes construir

  • Flujos de firma de contratos — activa Solicitudes de Firma cuando los usuarios completan un formulario o un pago
  • Paneles de estado de documentos — controla qué documentos están pendientes, firmados o rechazados
  • Firma incrustada — permite que los usuarios firmen documentos sin salir de tu app
  • Flujos de trabajo automatizados — usa los webhooks de Firma junto con triggers de la base de datos de Supabase para iniciar acciones posteriores cuando se firman los documentos

Requisitos previos

Descripción general de la arquitectura

Your Frontend (React, Next.js, Flutter, etc.)


Supabase Edge Function ──► Firma API
        │                      │
        ▼                      │ (webhook)
Supabase Postgres  ◄──────────┘
  (signing_requests table)
Tu frontend llama a una Supabase Edge Function, que a su vez llama de forma segura a la API de Firma para crear Solicitudes de Firma. Cuando los destinatarios firman (o rechazan), Firma envía un webhook a otra Edge Function que actualiza tu base de datos.

Paso 1: Guarda tu clave API de Firma

Añade tu clave API de Firma como un secreto de Supabase para que las Edge Functions puedan acceder a ella de forma segura:
supabase secrets set FIRMA_API_KEY=your_api_key_here

Paso 2: Crea una tabla para controlar las Solicitudes de Firma

Ejecuta este SQL en el Editor SQL de Supabase para crear una tabla que controle el estado de los documentos:
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);

Paso 3: Crea una Edge Function para enviar Solicitudes de Firma

Genera una nueva Edge Function:
supabase functions new send-signing-request
Luego reemplaza el contenido de 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" },
  });
});
Despliégala:
supabase functions deploy send-signing-request

Paso 4: Gestiona los webhooks de Firma

Crea otra Edge Function para recibir eventos de webhook de Firma cuando los documentos se firman, se rechazan o se actualizan:
supabase functions new firma-webhook
Reemplaza el contenido de 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 });
});
Despliégala y hazla accesible públicamente (los webhooks no envían encabezados de autenticación):
supabase functions deploy firma-webhook --no-verify-jwt
Luego registra la URL del webhook en tu panel de Firma, en Configuración → Webhooks. La URL de tu endpoint será:
https://<your-project-ref>.supabase.co/functions/v1/firma-webhook
Para uso en producción, verifica la firma del webhook usando tu secreto de firma de webhooks de Firma. Consulta la guía de webhooks para más detalles sobre la verificación de firmas.

Paso 5: Incrusta la experiencia de firma (opcional)

Para una experiencia de firma dentro de la app, usa el componente de firma incrustada de Firma. Una vez que tengas el signing_request_user_id de la respuesta de la API, cárgalo en un 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>
Consulta la guía de firma incrustada para las instrucciones de configuración completas, incluidas las prácticas recomendadas de seguridad.

Paso 6: Consulta el estado de la firma desde tu frontend

Con Row Level Security habilitado, tu frontend puede consultar el estado de la firma directamente:
const { data: requests } = await supabase
  .from("signing_requests")
  .select("*")
  .order("created_at", { ascending: false });

Próximos pasos