What We're Building
Professional services firms spend countless hours crafting proposals for potential clients. This AI Proposal Generator automates that process by collecting client requirements through an intake form, using Claude to generate customized proposal content, and producing polished PDF documents ready to send.
By the end of this guide, you'll have a full-stack application with:
Prerequisites
Before starting this guide, make sure you have the following:
- Node.js 18+ installed on your machine
- Claude API key from Anthropic (get one at console.anthropic.com)
- Supabase account for database and authentication
- Vercel account for deployment (free tier works)
- Basic knowledge of React and TypeScript
Tech Stack Specification
Here's the technology stack we'll use for this build:
| Layer | Technology | Why This Choice |
|---|---|---|
| Frontend | Next.js 14, Tailwind CSS | Form UI, server components, and rapid styling |
| Backend | Next.js API Routes | Serverless processing with same-repo deployment |
| Database | Supabase | Store templates, proposals, and client data |
| AI | Claude API | Best-in-class content generation quality |
| React-PDF | React-native PDF generation with styling | |
| Hosting | Vercel | Zero-config deployment for Next.js |
AI Agent Workflow
Here's how to leverage AI tools throughout this build to maximize productivity:
Project Scaffolding with Claude Code
Use Claude Code to scaffold the entire project structure with a single prompt. This approach ensures consistent patterns and saves hours of boilerplate setup.
# Example prompt for Claude Code
Create a Next.js 14 proposal generator app with:
- App router structure
- Supabase client setup with types
- API routes for Claude integration
- Tailwind CSS with professional theme
- TypeScript interfaces for Proposal, Template, Client
- Form components with react-hook-form
- PDF generation setup with @react-pdf/renderer
UI Generation with v0.dev
Generate beautiful form components and dashboard layouts using v0.dev. The generated Tailwind components integrate seamlessly with your Next.js project.
When prompting v0.dev, specify "professional services" or "consulting firm" aesthetic to get corporate-appropriate styling that clients expect from proposals.
Development with Cursor
Use Cursor's AI features for real-time debugging and code completion. The inline chat is particularly useful for crafting Claude prompts and handling edge cases in PDF generation.
Step-by-Step Build Guide
Phase 1: Proposal Template System
First, we'll set up the database schema and template management. Templates define the structure of your proposals with sections like Executive Summary, Scope of Work, Timeline, and Pricing.
-- Supabase SQL for templates table
CREATE TABLE proposal_templates (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
name TEXT NOT NULL,
description TEXT,
sections JSONB NOT NULL DEFAULT '[]',
industry TEXT,
created_at TIMESTAMPTZ DEFAULT now(),
updated_at TIMESTAMPTZ DEFAULT now()
);
-- Example section structure in JSONB
-- [
-- { "name": "Executive Summary", "prompt_hint": "Brief overview of proposed solution" },
-- { "name": "Scope of Work", "prompt_hint": "Detailed deliverables and milestones" },
-- { "name": "Timeline", "prompt_hint": "Project phases with dates" },
-- { "name": "Investment", "prompt_hint": "Pricing and payment terms" }
-- ]
Phase 2: Client Intake Form
Build a multi-step form that captures all the information Claude needs to generate a relevant proposal. Include fields for company info, project requirements, budget range, and timeline expectations.
// types/proposal.ts
export interface ClientIntake {
// Company Information
companyName: string;
industry: string;
companySize: 'startup' | 'smb' | 'enterprise';
// Project Details
projectType: string;
problemStatement: string;
desiredOutcomes: string[];
// Constraints
budgetRange: { min: number; max: number };
timelineWeeks: number;
// Contact
contactName: string;
contactEmail: string;
}
Phase 3: AI Content Generation
This is where Claude transforms client intake data into compelling proposal content. The key is crafting prompts that produce professional, specific, and persuasive text.
// app/api/generate-proposal/route.ts
import Anthropic from '@anthropic-ai/sdk';
const anthropic = new Anthropic();
export async function POST(request: Request) {
const { intake, template } = await request.json();
const sections = await Promise.all(
template.sections.map(async (section: Section) => {
const response = await anthropic.messages.create({
model: 'claude-sonnet-4-20250514',
max_tokens: 1024,
messages: [{
role: 'user',
content: buildSectionPrompt(section, intake)
}]
});
return {
name: section.name,
content: response.content[0].text
};
})
);
return Response.json({ sections });
}
The prompt structure is critical for quality output. See the detailed prompt templates in the next section.
Claude Proposal Writing Prompts
Here are the optimized prompts for each proposal section:
// lib/prompts.ts
export const PROPOSAL_PROMPTS = {
executiveSummary: (intake: ClientIntake) => `
You are a senior business consultant writing a proposal for ${intake.companyName}.
Write a compelling Executive Summary (250-350 words) that:
1. Acknowledges their challenge: "${intake.problemStatement}"
2. Positions our solution as the ideal fit
3. Highlights 2-3 key benefits aligned with their goals
4. Creates urgency without being pushy
5. Ends with a confident value proposition
Company context:
- Industry: ${intake.industry}
- Size: ${intake.companySize}
- Desired outcomes: ${intake.desiredOutcomes.join(', ')}
Write in a professional but warm tone. Use "we" for our firm and "you" for the client.
Avoid jargon. Be specific, not generic.`,
scopeOfWork: (intake: ClientIntake) => `
You are a project manager drafting the Scope of Work for ${intake.companyName}.
Create a detailed Scope of Work that includes:
## Deliverables
List 5-7 specific deliverables based on: "${intake.problemStatement}"
Each deliverable should be measurable and tied to their outcomes.
## Methodology
Describe our approach in 3-4 phases:
- Discovery & Planning
- Development & Implementation
- Testing & Refinement
- Launch & Handoff
## Out of Scope
List 3-4 items explicitly excluded to set clear boundaries.
## Success Criteria
Define 3-5 measurable success metrics.
Budget context: $${intake.budgetRange.min.toLocaleString()} - $${intake.budgetRange.max.toLocaleString()}
Timeline: ${intake.timelineWeeks} weeks
Be specific to their industry (${intake.industry}) and company size (${intake.companySize}).`,
timeline: (intake: ClientIntake) => `
Create a project timeline for a ${intake.timelineWeeks}-week engagement with ${intake.companyName}.
Structure as phases with:
- Phase name
- Duration (weeks)
- Key milestones
- Client touchpoints/review sessions
Project type: ${intake.projectType}
Include buffer time for revisions.
Add specific dates assuming project starts 2 weeks from today.
Format as a clear, scannable timeline that builds client confidence.`,
investment: (intake: ClientIntake) => `
Write the Investment/Pricing section for ${intake.companyName}'s proposal.
Budget range provided: $${intake.budgetRange.min.toLocaleString()} - $${intake.budgetRange.max.toLocaleString()}
Include:
1. **Investment Summary**: Total project investment with brief justification
2. **Payment Schedule**: 3-4 milestone-based payments
3. **What's Included**: Bullet list of everything covered
4. **Optional Add-ons**: 2-3 upgrade options for future phases
5. **Terms**: Standard payment terms (Net 15, etc.)
Position the investment as valuable, not cheap. Connect cost to outcomes.
Use confident language: "Your investment" not "The cost".`
};
export function buildSectionPrompt(
section: Section,
intake: ClientIntake
): string {
const promptBuilder = PROPOSAL_PROMPTS[section.key];
if (promptBuilder) {
return promptBuilder(intake);
}
// Fallback for custom sections
return `Write the "${section.name}" section for a proposal to ${intake.companyName}.
Context: ${section.prompt_hint}
Company: ${intake.industry} ${intake.companySize}
Project: ${intake.problemStatement}
Keep it professional, specific, and under 400 words.`;
}
Phase 4: Section-by-Section Editing
Build an editor interface that allows users to review and refine each AI-generated section. Include regeneration options with feedback for Claude.
// components/SectionEditor.tsx
'use client';
import { useState } from 'react';
import { Textarea } from '@/components/ui/textarea';
import { Button } from '@/components/ui/button';
interface SectionEditorProps {
section: { name: string; content: string };
onUpdate: (content: string) => void;
onRegenerate: (feedback: string) => Promise<string>;
}
export function SectionEditor({
section,
onUpdate,
onRegenerate
}: SectionEditorProps) {
const [content, setContent] = useState(section.content);
const [feedback, setFeedback] = useState('');
const [isRegenerating, setIsRegenerating] = useState(false);
const handleRegenerate = async () => {
setIsRegenerating(true);
const newContent = await onRegenerate(feedback);
setContent(newContent);
setFeedback('');
setIsRegenerating(false);
};
return (
<div className="space-y-4 p-6 border rounded-lg">
<h3 className="text-lg font-semibold">{section.name}</h3>
<Textarea
value={content}
onChange={(e) => {
setContent(e.target.value);
onUpdate(e.target.value);
}}
rows={12}
className="font-mono text-sm"
/>
<div className="flex gap-4">
<input
type="text"
placeholder="Feedback for regeneration (e.g., 'Make it more concise')"
value={feedback}
onChange={(e) => setFeedback(e.target.value)}
className="flex-1 px-3 py-2 border rounded"
/>
<Button
onClick={handleRegenerate}
disabled={isRegenerating}
>
{isRegenerating ? 'Regenerating...' : 'Regenerate'}
</Button>
</div>
</div>
);
}
Phase 5: PDF Export Functionality
Use React-PDF to generate professional PDF documents with your branding. The library renders React components to PDF format.
// components/ProposalPDF.tsx
import {
Document,
Page,
Text,
View,
StyleSheet,
Font
} from '@react-pdf/renderer';
// Register custom fonts for professional look
Font.register({
family: 'Inter',
src: 'https://fonts.gstatic.com/s/inter/v12/UcCO3FwrK3iLTeHuS_fvQtMwCp50KnMw2boKoduKmMEVuLyfAZ9hjp-Ek-_EeA.woff2'
});
const styles = StyleSheet.create({
page: {
padding: 50,
fontFamily: 'Inter',
},
header: {
marginBottom: 30,
borderBottom: '2px solid #4F46E5',
paddingBottom: 20,
},
title: {
fontSize: 28,
fontWeight: 'bold',
color: '#1F2937',
},
subtitle: {
fontSize: 14,
color: '#6B7280',
marginTop: 8,
},
section: {
marginBottom: 24,
},
sectionTitle: {
fontSize: 18,
fontWeight: 'bold',
color: '#4F46E5',
marginBottom: 12,
},
sectionContent: {
fontSize: 11,
lineHeight: 1.6,
color: '#374151',
},
footer: {
position: 'absolute',
bottom: 30,
left: 50,
right: 50,
textAlign: 'center',
fontSize: 10,
color: '#9CA3AF',
},
});
interface ProposalPDFProps {
proposal: {
clientName: string;
projectTitle: string;
date: string;
sections: Array<{ name: string; content: string }>;
};
}
export function ProposalPDF({ proposal }: ProposalPDFProps) {
return (
<Document>
<Page size="A4" style={styles.page}>
<View style={styles.header}>
<Text style={styles.title}>{proposal.projectTitle}</Text>
<Text style={styles.subtitle}>
Prepared for {proposal.clientName} | {proposal.date}
</Text>
</View>
{proposal.sections.map((section, index) => (
<View key={index} style={styles.section}>
<Text style={styles.sectionTitle}>{section.name}</Text>
<Text style={styles.sectionContent}>{section.content}</Text>
</View>
))}
<Text style={styles.footer}>
Confidential | Page 1 of 1 | Valid for 30 days
</Text>
</Page>
</Document>
);
}
Phase 6: Proposal Tracking Dashboard
Finally, build a dashboard to track all proposals with status indicators (Draft, Sent, Viewed, Accepted, Declined) and key metrics.
-- Proposals table with tracking
CREATE TABLE proposals (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
template_id UUID REFERENCES proposal_templates(id),
client_intake JSONB NOT NULL,
sections JSONB NOT NULL,
status TEXT DEFAULT 'draft',
total_value DECIMAL(10,2),
sent_at TIMESTAMPTZ,
viewed_at TIMESTAMPTZ,
responded_at TIMESTAMPTZ,
created_at TIMESTAMPTZ DEFAULT now(),
updated_at TIMESTAMPTZ DEFAULT now()
);
-- Track proposal views
CREATE TABLE proposal_views (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
proposal_id UUID REFERENCES proposals(id),
viewed_at TIMESTAMPTZ DEFAULT now(),
ip_address INET,
user_agent TEXT
);
Common Issues & Solutions
Here are some common issues you might encounter and how to solve them:
React-PDF doesn't work with Server Components. Use dynamic imports with ssr: false to load PDF components only on the client side.
When generating multiple sections, use Promise.all carefully. Consider adding a small delay between requests or using a queue system for production.
If Claude generates generic content, add more specific context to your prompts. Include industry-specific terminology, company size considerations, and explicit instructions to avoid cliches.
Next Steps
Congratulations on building your AI Proposal Generator! Here are some ways to extend the project:
- Add authentication: Use Supabase Auth to support multiple users and teams
- Implement e-signatures: Integrate DocuSign or HelloSign for proposal acceptance
- Build proposal analytics: Track which sections clients spend the most time reading
- Create a client portal: Let clients view, comment on, and accept proposals online
- Add CRM integration: Sync proposals with HubSpot or Salesforce
Want to build more AI-powered tools for your firm? Check out our other guides for more step-by-step tutorials.
Follow the Vibe Coding Enthusiast
Follow JD — product updates on LinkedIn, personal takes on X.