Supabase e-sign webhook pipeline
Supabase Postgres plus Edge Functions is a common backend for Vercel frontends. Atlas webhooks land in Supabase through an Edge Function that verifies HMAC, writes rows, and triggers Realtime updates to your app.
> Share: "Atlas signs. Supabase stores envelope state. Your app subscribes to changes."
Schema
create table contracts ( id uuid primary key default gen_random_uuid(), atlas_envelope_id text unique not null, external_id text, status text not null default 'draft', review_url text, signed_pdf_url text, signer_email text, created_at timestamptz default now(), signed_at timestamptz ); create index contracts_atlas_envelope_id on contracts (atlas_envelope_id); create index contracts_external_id on contracts (external_id);
Store external_id from Atlas metadata for CRM correlation.
Create from Supabase Edge Function
supabase/functions/create-contract/index.ts:
import { serve } from 'https://deno.land/std@0.168.0/http/server.ts';
serve(async (req) => {
const { documentUrl, signerEmail, signerName, externalId } = await req.json();
const atlasKey = Deno.env.get('ATLAS_API_KEY')!;
const res = await fetch('https://atlaswork.ai/api/envelope', {
method: 'POST',
headers: {
Authorization: `Bearer ${atlasKey}`,
'Content-Type': 'application/json',
'Idempotency-Key': crypto.randomUUID(),
},
body: JSON.stringify({
document_url: documentUrl,
webhook_url: `${Deno.env.get('SUPABASE_URL')}/functions/v1/atlas-webhook`,
metadata: { external_id: externalId, client_reference_id: externalId },
parties: [{ email: signerEmail, name: signerName, role: 'Customer' }],
}),
});
const data = await res.json();
if (!res.ok) return new Response(JSON.stringify(data), { status: res.status });
const supabase = createClient(/* service role */);
await supabase.from('contracts').insert({
atlas_envelope_id: data.envelope_id,
review_url: data.review_url,
signer_email: signerEmail,
external_id: externalId,
status: 'draft',
});
return new Response(JSON.stringify(data), { headers: { 'Content-Type': 'application/json' } });
});
Webhook Edge Function
Atlas POSTs JSON events. Verify signature before writing:
async function verify(raw: string, header: string, key: string): Promise<boolean> {
const enc = new TextEncoder();
const cryptoKey = await crypto.subtle.importKey(
'raw',
enc.encode(key),
{ name: 'HMAC', hash: 'SHA-256' },
false,
['sign'],
);
const sig = await crypto.subtle.sign('HMAC', cryptoKey, enc.encode(raw));
const hex = Array.from(new Uint8Array(sig), (b) => b.toString(16).padStart(2, '0')).join('');
return header === `sha256=${hex}`;
}
serve(async (req) => {
const raw = await req.text();
const sig = req.headers.get('x-atlas-signature') ?? '';
if (!verify(raw, sig, Deno.env.get('ATLAS_API_KEY')!)) {
return new Response('Unauthorized', { status: 401 });
}
const event = JSON.parse(raw);
const supabase = createClient(/* service role */);
if (event.event === 'envelope.signed') {
await supabase.from('contracts').update({
status: 'signed',
signed_at: new Date().toISOString(),
signed_pdf_url: event.signed_download_url ?? null,
}).eq('atlas_envelope_id', event.envelope_id);
}
return new Response('ok');
});
Use Supabase service role in the webhook function. Row Level Security on contracts should block client writes; server functions bypass RLS with service role.
Realtime UI
Enable Realtime on contracts. Your Next.js dashboard subscribes:
supabase
.channel('contracts')
.on('postgres_changes', { event: 'UPDATE', schema: 'public', table: 'contracts' }, payload => {
setStatus(payload.new.status);
})
.subscribe();
Users see "Signed" without polling Atlas.
Storage option
Download signed PDF from Atlas API and upload to Supabase Storage bucket signed-contracts. Store storage_path on the row for private signed URLs.
Auth model
Atlas dashboard uses password auth. Your Supabase app may use Supabase Auth for customers. Keep Atlas API key only in Edge secrets. End users never see it.
Idempotency
Atlas may retry webhooks. Use atlas_envelope_id plus event type as idempotency key in a webhook_events table before mutating state.
Row Level Security pattern
Example RLS: authenticated users read contracts where org_id = auth.jwt()->>'org_id'. Only service role inserts or updates from Edge Functions. Never expose service role key to browser clients.
pg_net alternative
Some teams call Atlas create from Postgres via pg_net extension instead of Edge Functions. That keeps logic close to data but makes HMAC webhook verification harder inside SQL. Edge Functions remain the default recommendation for verify-then-write flows.
Backup signed artifacts
Replicate Supabase Storage bucket holding signed PDFs to secondary region if counsel requires geographic redundancy. Atlas also serves signed downloads via API after sign for re-fetch jobs.
Local supabase start
Developers running Supabase locally can point Edge Functions at Atlas production or a dedicated test org. Use separate API keys per developer to trace envelope noise during debugging.
FAQ
Edge Function timeout? Respond 2xx fast. Queue heavy PDF downloads with Supabase background job or separate worker.
Can Supabase Auth users create envelopes? Your Edge Function maps JWT user to create call. Review URL still gates Send.
402 credits? Surface billing link to org admin. Credits live on Atlas org, not Supabase.
MCP + Supabase? Agents create via MCP; Supabase syncs via the same webhook function if you also set webhook_url on those envelopes.
DOCX? Supported on Atlas create when URL serves DOCX.
Related
Extended FAQ
Supabase Auth vs Atlas Auth? Separate systems. Edge Functions bridge them with service role.
Can RLS block webhooks? Service role bypasses RLS. Do not use anon key in webhook function.
Realtime latency? Postgres change events typically sub-second after webhook update.
Storage vs external URL? Either works for signed artifact retention policy.
Local Edge Function secrets? Use supabase secrets set for Atlas key in remote projects.
API reference
Full route list and request schemas live at /dev. Start with E-signature API for the mental model, then use this guide for copy-paste examples.