Build a Privacy-First Self-Hosted AI Freelance Proposal & Scope of Work Generator for Indian Full-Stack Developers with Elysia.js, Ollama, and pgvector: 2026 Step-by-Step Guide
A 2026 step-by-step guide for Indian full-stack developers to build a DPDP Act-compliant, self-hosted AI tool that generates custom freelance proposals and scopes of work using Elysia.js, Ollama, and pgvector—no third-party API costs, no client data leaks.
Agentic Academy Labs
Author
If you’re an Indian full-stack developer balancing freelance client work, you’ve probably lost hours drafting tailored proposals and scopes of work (SOW) for every new lead. Generic AI tools like ChatGPT feel like a quick fix, but they come with two massive flaws for local freelancers: they leak sensitive client data to third-party servers, and they don’t understand the nuances of the Indian freelance market—GST invoicing, milestone payment terms, popular local tech stacks, and cultural communication norms.
In 2026, with India’s DPDP Act 2023 enforcing strict data localization rules for client data, a privacy-first, self-hosted AI proposal generator is no longer a nice-to-have: it’s a competitive edge. In this step-by-step guide, we’ll build exactly that tool using Elysia.js, Ollama, and pgvector—no cloud AI API costs, no data leaving your server, and fully customized for Indian full-stack devs.
Why Build This Yourself?
Before we dive into the code, let’s cover why this custom tool beats off-the-shelf options for Indian freelancers:
- Full DPDP Act compliance: All client data stays on your own server, no cross-border data transfers, so you can safely work with enterprise, government, and fintech clients with strict data requirements.
- Zero per-token costs: Unlike OpenAI or Anthropic APIs that charge per request, Ollama runs open-source LLMs like Llama 3 8B locally on your VPS or personal machine, so you only pay for your server hosting.
- Market-specific context: Generic AI tools don’t understand that most Indian clients expect 30% advance payments, or that MERN and Next.js are the most in-demand stacks for local projects. We’ll tailor the model’s prompts to output proposals that feel native to the Indian market.
- No vendor lock-in: You own the entire stack, so you can tweak the tool to add features like regional language support or WhatsApp integration whenever you want.
Tech Stack Overview
We’re using a lightweight, fully open-source stack optimized for self-hosted deployment:
- Elysia.js: A Bun-native edge framework with near-Node performance and built-in type safety, perfect for the low-latency API layer of our tool.
- Ollama: Runs open-source LLMs like Llama 3 8B locally, no cloud API dependencies or data leaks.
- pgvector: A Postgres extension for storing and querying vector embeddings, used to store past proposals and retrieve contextually similar ones for new generations.
- Bun: The fast JavaScript runtime that powers Elysia, with built-in package management and TypeScript support.
Prerequisites
Before we start, make sure you have the following set up:
- Bun installed on your local machine or VPS
- Postgres 14+ with the pgvector extension enabled (you can use managed services like Supabase, which has pgvector built-in)
- Ollama installed, with the
llama3:8b-instructmodel pulled (runollama pull llama3:8b-instructto download it) - Basic familiarity with TypeScript and SQL
Step 1: Initialize the Elysia Project
First, create a new project directory and initialize an Elysia app:
# Create project folder
mkdir proposal-generator && cd proposal-generator
# Initialize Elysia app
bun create elysia . --typescript
# Install dependencies
bun add pg elysia @elysiajs/cors @elysiajs/static pgvector
Next, create a .env file to store your database and Ollama configuration:
DATABASE_URL=postgres://your-username:your-password@localhost:5432/proposal_generator
OLLAMA_URL=http://localhost:11434
Step 2: Set Up pgvector for Proposal Storage
We’ll use pgvector to store embeddings of past successful proposals, so we can retrieve similar ones to use as context when generating new proposals. First, create a database connection utility and set up the proposals table:
// src/db.ts
import { Pool } from 'pg'
export const pool = new Pool({
connectionString: process.env.DATABASE_URL,
})
// Initialize table and extension on app start
export async function initDB() {
await pool.query('CREATE EXTENSION IF NOT EXISTS vector;')
await pool.query(`
CREATE TABLE IF NOT EXISTS proposals (
id SERIAL PRIMARY KEY,
client_name TEXT NOT NULL,
project_brief TEXT NOT NULL,
proposal_text TEXT NOT NULL,
embedding vector(768),
created_at TIMESTAMP DEFAULT NOW()
);
`)
}
The 768-dimensional vector matches the embedding output size of Llama 3 8B, which we’ll use for both text generation and embedding generation.
Step 3: Build Embedding and Retrieval Logic
We need two core functions: one to generate embeddings for text using Ollama’s local embedding endpoint, and another to retrieve similar past proposals from pgvector using cosine similarity:
// src/utils.ts
export async function generateEmbedding(text: string): Promise<number[]> {
const response = await fetch(`${process.env.OLLAMA_URL}/api/embeddings`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
model: 'llama3:8b-instruct',
prompt: text,
}),
})
const data = await response.json()
return data.embedding
}
export async function getSimilarProposals(brief: string, limit = 3) {
const embedding = await generateEmbedding(brief)
const result = await pool.query(
`SELECT proposal_text, client_name, 1 - (embedding <=> $1) AS similarity
FROM proposals
ORDER BY embedding <=> $1
LIMIT $2`,
[JSON.stringify(embedding), limit]
)
return result.rows
}
The <=> operator in pgvector calculates cosine similarity between vectors, so we can find the most semantically similar past proposals to use as reference for new generations.
Step 4: Build the Proposal Generation Endpoint
This is the core of our tool: an Elysia route that takes a client brief, retrieves similar past proposals, and generates a new custom proposal and SOW using the local Llama 3 model. We’ll tailor the prompt specifically for Indian full-stack devs to include local market context:
// src/index.ts
import Elysia from 'elysia'
import { initDB, pool } from './db'
import { generateEmbedding, getSimilarProposals } from './utils'
await initDB()
const app = new Elysia()
.use(static({ assets: './public', prefix: '/' })) // Serve frontend
.post('/generate-proposal', async ({ body }) => {
const { clientBrief, clientName, projectType, includeSOW } = body
// 1. Retrieve context from past proposals
const similarProposals = await getSimilarProposals(clientBrief)
const context = similarProposals
.map(
(p) =>
`Past proposal for ${p.client_name} (similarity score: ${p.similarity.toFixed(2)}):\n${p.proposal_text}`
)
.join('\n\n')
// 2. Build market-specific prompt
const prompt = `You are a senior Indian full-stack freelance developer with 5+ years of experience building projects for local startups, SMEs, and enterprise clients. Generate a professional, culturally tailored freelance proposal and scope of work for the following project:
Client Name: ${clientName}
Project Type: ${projectType}
Client Brief: ${clientBrief}
${context ? `Reference these past successful proposals for tone, structure, and terms:\n${context}` : ''}
Follow these strict rules for Indian market alignment:
- Mention standard payment terms: 30% advance, milestone-based payments, GST invoicing (18% for IT services)
- Reference popular local tech stacks (MERN, Next.js, PostgreSQL, AWS) where relevant
- Include standard Indian freelance clauses: IP transfer only after 100% payment, 2 rounds of complimentary revisions, 30-day post-launch support
- Keep the tone professional but approachable, avoiding overly formal Western jargon
- If includeSOW is true, add a separate Scope of Work section with timelines and deliverables
- Keep the total proposal length between 800-1200 words
`
// 3. Generate proposal with local LLM
const llmResponse = await fetch(`${process.env.OLLAMA_URL}/api/generate`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
model: 'llama3:8b-instruct',
prompt: prompt,
stream: false,
options: { temperature: 0.7, max_tokens: 2000 },
}),
})
const llmData = await llmResponse.json()
const generatedProposal = llmData.response
// 4. Save new proposal to database for future context
const embedding = await generateEmbedding(clientBrief)
await pool.query(
`INSERT INTO proposals (client_name, project_brief, proposal_text, embedding)
VALUES ($1, $2, $3, $4)`,
[clientName, clientBrief, generatedProposal, JSON.stringify(embedding)]
)
return { proposal: generatedProposal }
})
.post('/add-proposal', async ({ body }) => {
// Endpoint to manually add past successful proposals to the database
const { clientName, projectBrief, proposalText } = body
const embedding = await generateEmbedding(proposalText)
await pool.query(
`INSERT INTO proposals (client_name, project_brief, proposal_text, embedding)
VALUES ($1, $2, $3, $4)`,
[clientName, projectBrief, proposalText, JSON.stringify(embedding)]
)
return { success: true }
})
.listen(3000)
console.log('Proposal generator running on http://localhost:3000')
Step 5: Add a Simple Frontend
As a full-stack dev, you can build a custom frontend with React, Next.js, or even a simple static HTML form. We’ve already added static file serving to our Elysia app, so create a public/index.html file with this basic form:
<!-- public/index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Indian Freelance Proposal Generator</title>
<style>
body { max-width: 800px; margin: 2rem auto; padding: 0 1rem; font-family: system-ui; }
input, textarea { width: 100%; margin: 0.5rem 0 1rem; padding: 0.5rem; }
button { padding: 0.7rem 1.5rem; background: #2563eb; color: white; border: none; border-radius: 4px; cursor: pointer; }
#output { margin-top: 2rem; white-space: pre-wrap; line-height: 1.6; }
</style>
</head>
<body>
<h1>🤝 Freelance Proposal Generator for Indian Devs</h1>
<form id="proposalForm">
<div>
<label>Client Name / Company:</label>
<input type="text" id="clientName" required>
</div>
<div>
<label>Project Type (e.g. D2C E-commerce, SaaS MVP, School Management System):</label>
<input type="text" id="projectType" required>
</div>
<div>
<label>Client Brief:</label>
<textarea id="clientBrief" rows="6" required placeholder="Describe the client's requirements, timeline, and any specific asks..."></textarea>
</div>
<div>
<label>
<input type="checkbox" id="includeSOW"> Include detailed Scope of Work
</label>
</div>
<button type="submit">Generate Proposal</button>
</form>
<div id="output"></div>
<script>
document.getElementById("proposalForm").addEventListener("submit", async (e) => {
e.preventDefault()
const output = document.getElementById("output")
output.textContent = "Generating your custom proposal..."
try {
const res = await fetch("http://localhost:3000/generate-proposal", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
clientName: document.getElementById("clientName").value,
projectType: document.getElementById("projectType").value,
clientBrief: document.getElementById("clientBrief").value,
includeSOW: document.getElementById("includeSOW").checked
})
})
const data = await res.json()
output.textContent = data.proposal
} catch (err) {
output.textContent = "Error generating proposal: " + err.message
}
})
</script>
</body>
</html>
Step 6: Deploy and Secure Your Tool
For production use, deploy your stack on a low-cost VPS (starting at ₹200/month on DigitalOcean or AWS Lightsail) or use managed services:
- Use Supabase for Postgres + pgvector, which handles backups and scaling for you
- Run Ollama on the same VPS as your Elysia app, or a separate low-cost instance if you have high traffic
- Add API key authentication to your
/generate-proposalendpoint to prevent unauthorized use - Enable HTTPS with Let’s Encrypt to secure client data in transit
- Restrict Ollama’s API to localhost or your server’s IP only to prevent external access
Final Thoughts
Building this self-hosted proposal generator will cut the time you spend on administrative freelance work from hours to minutes, while keeping your client data fully compliant with Indian data laws. Unlike generic AI tools, it’s tailored to the specific needs of the Indian freelance ecosystem, so the proposals it generates feel authentic and aligned with what local clients expect.
You can extend this tool with even more features: add project timeline calculators, automated invoice generation, or client relationship management (CRM) basics to build a full freelance operations stack that’s 100% yours, with zero monthly fees.
Enjoyed this article?
Read More Articles