I needed to help clients dispute credit report errors. Writing FCRA dispute letters manually took 30 minutes per letter. With 50+ clients, that's 25 hours of repetitive work monthly.
I built an automated system using Supabase edge functions that generates personalized dispute letters in under 5 seconds. Here's exactly how I did it.
The problem with manual dispute letters
Every FCRA dispute letter needs specific elements:
- Client personal information
- Account details from credit reports
- Legal language citing Fair Credit Reporting Act sections
- Proper formatting for credit bureaus
I was copying and pasting the same templates, changing names and account numbers. Human error crept in. Clients waited days for their letters.
Why Supabase edge functions
I already run my business infrastructure on a single Vultr VPS. Adding another SaaS subscription goes against my philosophy of infrastructure ownership.
Supabase edge functions gave me:
- Server-side processing for sensitive data
- Built-in database integration
- No cold starts (important for client-facing tools)
- Full control over my stack
Database schema design
I created three main tables in Supabase:
-- Client information
CREATE TABLE clients (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
first_name text NOT NULL,
last_name text NOT NULL,
address text NOT NULL,
city text NOT NULL,
state text NOT NULL,
zip_code text NOT NULL,
ssn_last_four text NOT NULL,
created_at timestamptz DEFAULT now()
);
-- Dispute items
CREATE TABLE dispute_items (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
client_id uuid REFERENCES clients(id),
creditor_name text NOT NULL,
account_number text NOT NULL,
dispute_reason text NOT NULL,
bureau text NOT NULL, -- Experian, Equifax, TransUnion
created_at timestamptz DEFAULT now()
);
-- Generated letters
CREATE TABLE generated_letters (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
client_id uuid REFERENCES clients(id),
letter_content text NOT NULL,
bureau text NOT NULL,
item_count integer NOT NULL,
created_at timestamptz DEFAULT now()
);
The edge function
My edge function pulls client data, formats dispute items, and generates the complete letter:
import { serve } from 'https://deno.land/std@0.168.0/http/server.ts'
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2'
serve(async (req) => {
if (req.method !== 'POST') {
return new Response('Method not allowed', { status: 405 })
}
const { client_id, bureau } = await req.json()
const supabase = createClient(
Deno.env.get('SUPABASE_URL') ?? '',
Deno.env.get('SUPABASE_ANON_KEY') ?? ''
)
// Get client info
const { data: client } = await supabase
.from('clients')
.select('*')
.eq('id', client_id)
.single()
// Get dispute items for this bureau
const { data: items } = await supabase
.from('dispute_items')
.select('*')
.eq('client_id', client_id)
.eq('bureau', bureau)
const letterContent = generateFCRALetter(client, items, bureau)
// Save generated letter
await supabase
.from('generated_letters')
.insert({
client_id,
letter_content: letterContent,
bureau,
item_count: items.length
})
return new Response(
JSON.stringify({ letter: letterContent }),
{ headers: { 'Content-Type': 'application/json' } }
)
})
The letter generation logic
I created templates for each section with dynamic content insertion:
function generateFCRALetter(client: any, items: any[], bureau: string) {
const bureauAddresses = {
'Experian': 'P.O. Box 4500, Allen, TX 75013',
'Equifax': 'P.O. Box 740256, Atlanta, GA 30374',
'TransUnion': 'P.O. Box 2000, Chester, PA 19016'
}
let letter = `${new Date().toLocaleDateString()}
${bureauAddresses[bureau]}
Re: Request for Investigation of Credit Report Information
Name: ${client.first_name} ${client.last_name}
Address: ${client.address}, ${client.city}, ${client.state} ${client.zip_code}
SSN: ***-**-${client.ssn_last_four}
Dear ${bureau} Credit Bureau,
I am writing to dispute the following information on my credit report pursuant to my rights under the Fair Credit Reporting Act (15 U.S.C. ยง1681i).
ITEMS BEING DISPUTED:
`
items.forEach((item, index) => {
letter += `
${index + 1}. Creditor: ${item.creditor_name}
Account Number: ${item.account_number}
Reason for Dispute: ${item.dispute_reason}
`
})
letter += `
I request that you investigate these items and remove any information that cannot be verified as accurate and complete.
Please provide me with written results of your investigation within 30 days as required by law.
Sincerely,
${client.first_name} ${client.last_name}`
return letter
}
Results that matter
Since implementing this system 6 months ago:
- Letter generation time: 30 minutes โ 5 seconds
- Monthly time saved: 25 hours
- Error rate: Dropped to zero (no more copy-paste mistakes)
- Client satisfaction: Improved due to same-day turnaround
I process 200+ dispute letters monthly now. The system handles peak loads without issues.
Cost comparison
My total monthly costs:
- Supabase Pro: $25
- Vultr VPS: $12
Equivalent services would cost $200+ monthly with traditional SaaS providers. You get vendor lock-in and data restrictions too.
Next steps for you
If you're handling repetitive document generation, start with Supabase edge functions. The PostgreSQL integration makes complex data relationships simple.
Set up your database schema first. Build templates that work manually before automating. Test with real data, not sample data.
Your clients will notice the speed difference. You'll get your evenings back.
Top comments (0)