Build a Self-Hosted Codebase Q&A API with Bun.js, PostgreSQL pgvector, and Ollama: 2026 Step-by-Step Guide for Developers
In 2026, self-hosted AI tools are a must for developers who want full control over their codebase data and eliminate cloud AI costs. This step-by-step guide walks you through building a production-ready codebase Q&A API using Bun.js, PostgreSQL pgvector, and Ollama, no third-party cloud dependencies required.
Agentic Academy Labs
Author
Self-hosted codebase Q&A tools have become a non-negotiable standard for development teams in 2026, as concerns over proprietary code leakage, cloud AI costs, and latency drive teams to bring AI inference and data storage in-house. Unlike generic cloud Q&A tools that send your code to third-party servers, a self-hosted solution gives you full control over your data, customization options for your specific tech stack, and zero per-query fees.
In this guide, we’ll walk you through building a production-ready codebase Q&A API using three modern, developer-friendly tools: Bun.js for a high-performance runtime, PostgreSQL with pgvector for embedding storage and similarity search, and Ollama for local large language model (LLM) inference. No cloud AI dependencies, no surprise bills, and full ownership of your codebase data.
Why This Stack in 2026?
Each component of this stack is purpose-built for the use case, with major 2026 updates that make this combination far more efficient than older alternatives:
- Bun.js: The 2026 stable release includes built-in TypeScript support, a zero-config package manager, native test runner, and a built-in HTTP server that is 3-5x faster than Node.js for I/O-heavy workloads like API serving. You won’t need to configure Express or other middleware for this project.
- PostgreSQL pgvector: pgvector is now a first-class extension in all major managed Postgres providers (Supabase, Neon, AWS RDS), with optimized HNSW indexing for sub-millisecond similarity search on code embeddings. You won’t need a separate vector database, reducing operational overhead.
- Ollama: The 2026 release of Ollama supports quantized 7B-8B parameter models that run smoothly on consumer hardware, with native support for code-specific models like Code Llama and Llama 3. No API keys, no data leaving your server, and zero inference costs.
Prerequisites
Before you start, make sure you have the following installed:
- Bun 1.1 or later
- PostgreSQL 16+ with the pgvector extension enabled
- Ollama 0.2 or later, with
codellama:7b-instructandnomic-embed-textpulled viaollama pull codellama:7b-instructandollama pull nomic-embed-text - Basic familiarity with TypeScript and SQL
Step 1: Initialize Your Bun.js Project
First, create a new Bun project and install the required dependencies:
bun init codebase-qa-api
cd codebase-qa-api
bun add pg ollama dotenv
bun add -d @types/pg typescript
Create a .env file in your project root to store configuration:
DATABASE_URL=postgres://your_user:your_password@localhost:5432/codebase_qa
OLLAMA_HOST=http://localhost:11434
EMBEDDING_MODEL=nomic-embed-text
LLM_MODEL=codellama:7b-instruct
We use nomic-embed-text for embeddings because it is optimized for code and technical text, delivering higher similarity accuracy for codebase search than general-purpose embedding models.
Step 2: Configure PostgreSQL with pgvector
First, enable the pgvector extension in your Postgres database:
CREATE EXTENSION IF NOT EXISTS vector;
Next, create a table to store your code chunks and their embeddings. We use 768 dimensions to match the output size of nomic-embed-text:
CREATE TABLE code_chunks (
id SERIAL PRIMARY KEY,
file_path TEXT NOT NULL,
content TEXT NOT NULL,
embedding vector(768) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
To speed up similarity search, create an HNSW index optimized for cosine similarity (the standard metric for comparing embedding quality):
CREATE INDEX ON code_chunks USING hnsw (embedding vector_cosine_ops);
HNSW is far faster than the older IVFFlat index for small to medium codebases (up to 1M chunks), with minimal accuracy tradeoffs.
Step 3: Build the Embedding and Indexing Pipeline
We need two core utilities: a code chunker to split code files into searchable pieces, and an embedding pipeline to generate and store embeddings for those chunks.
Create src/embeddings.ts:
import { Ollama } from 'ollama';
import { Client } from 'pg';
const ollama = new Ollama({ host: process.env.OLLAMA_HOST });
const db = new Client({ connectionString: process.env.DATABASE_URL });
// Simple sliding window chunker (replace with a code-aware splitter for production)
function chunkCode(content: string, filePath: string) {
const chunks = [];
const chunkSize = 1000;
const overlap = 200;
let start = 0;
while (start < content.length) {
chunks.push({ content: content.slice(start, start + chunkSize), filePath });
start += chunkSize - overlap;
}
return chunks;
}
// Generate embedding for a text chunk
async function generateEmbedding(text: string) {
const res = await ollama.embeddings({
model: process.env.EMBEDDING_MODEL!,
prompt: text,
});
return res.embedding;
}
// Index a code file into Postgres
export async function indexCodeFile(filePath: string, content: string) {
await db.connect();
const chunks = chunkCode(content, filePath);
for (const chunk of chunks) {
const embedding = await generateEmbedding(chunk.content);
await db.query(
'INSERT INTO code_chunks (file_path, content, embedding) VALUES ($1, $2, $3)',
[chunk.filePath, chunk.content, embedding]
);
}
await db.end();
console.log(`Indexed ${chunks.length} chunks from ${filePath}`);
}
Pro tip for production: Replace the simple sliding window chunker with a code-aware splitter like LangChain’s
CodeSplitterto avoid splitting functions, classes, or imports mid-definition, which will drastically improve search accuracy.
Step 4: Build the Q&A API
We’ll use Bun’s built-in Bun.serve API for a lightweight, high-performance server with no external dependencies. Create src/index.ts:
import { Ollama } from 'ollama';
import { Client } from 'pg';
import dotenv from 'dotenv';
dotenv.config();
const ollama = new Ollama({ host: process.env.OLLAMA_HOST });
const db = new Client({ connectionString: process.env.DATABASE_URL });
db.connect().then(() => console.log('Connected to PostgreSQL'));
const server = Bun.serve({
port: 3000,
async fetch(req) {
const url = new URL(req.url);
// Health check
if (url.pathname === '/health') return new Response('OK', { status: 200 });
// Q&A endpoint
if (url.pathname === '/api/qa' && req.method === 'POST') {
try {
const { question } = await req.json();
if (!question) return new Response(JSON.stringify({ error: 'Missing question' }), { status: 400 });
// Embed the user's question
const questionEmbedding = await ollama.embeddings({
model: process.env.EMBEDDING_MODEL!,
prompt: question,
});
// Find top 3 most similar code chunks
const similarChunks = await db.query(
`SELECT file_path, content, 1 - (embedding <=> $1) AS similarity
FROM code_chunks
ORDER BY embedding <=> $1
LIMIT 3`,
[JSON.stringify(questionEmbedding.embedding)]
);
// Build context for the LLM
const context = similarChunks.rows
.map(row => `File: ${row.file_path}\n${row.content}\n---`)
.join('\n');
// Generate answer with local LLM
const llmRes = await ollama.generate({
model: process.env.LLM_MODEL!,
prompt: `Answer the question using ONLY the provided code context. If the answer is not present, say "I don't have enough information to answer that from the codebase."\n\nContext:\n${context}\n\nQuestion: ${question}`,
});
return new Response(
JSON.stringify({
answer: llmRes.response,
sources: similarChunks.rows.map(r => r.file_path),
scores: similarChunks.rows.map(r => r.similarity),
}),
{ headers: { 'Content-Type': 'application/json' } }
);
} catch (err) {
console.error(err);
return new Response(JSON.stringify({ error: 'Server error' }), { status: 500 });
}
}
// Index endpoint for adding code files
if (url.pathname === '/api/index' && req.method === 'POST') {
try {
const { filePath, content } = await req.json();
if (!filePath || !content) return new Response(JSON.stringify({ error: 'Missing fields' }), { status: 400 });
const { indexCodeFile } = await import('./embeddings.js');
await indexCodeFile(filePath, content);
return new Response(JSON.stringify({ message: `Indexed ${filePath}` }), { status: 200 });
} catch (err) {
console.error(err);
return new Response(JSON.stringify({ error: 'Server error' }), { status: 500 });
}
}
return new Response('Not Found', { status: 404 });
},
});
console.log(`Server running at http://localhost:${server.port}`);
The API flow is simple:
- A user submits a question about the codebase
- We generate an embedding for the question using Ollama
- We search Postgres for the 3 most similar code chunks using cosine similarity
- We feed those chunks as context to the local Code Llama model to generate an accurate, code-specific answer
- We return the answer alongside the source files and similarity scores so users can verify the result
Step 5: Test Your API
First, start your Postgres and Ollama services, then launch the Bun server:
bun run src/index.ts
First, index a sample code file:
curl -X POST http://localhost:3000/api/index \
-H "Content-Type: application/json" \
-d '{
"filePath": "src/auth/token.ts",
"content": "import jwt from 'jsonwebtoken';\nimport bcrypt from 'bcrypt';\n\nexport function validateJWT(token: string): boolean {\n try {\n const decoded = jwt.verify(token, process.env.JWT_SECRET as string);\n return !!decoded;\n } catch {\n return false;\n }\n}\n\nexport function hashPassword(password: string): string {\n return bcrypt.hashSync(password, 10);\n}"
}'
Then test the Q&A endpoint:
curl -X POST http://localhost:3000/api/qa \
-H "Content-Type: application/json" \
-d '{
"question": "How do I validate a JWT token in this codebase?"
}'
You’ll get a response like this:
{
"answer": "To validate a JWT token, use the `validateJWT` function from `src/auth/token.ts`. It takes a token string as input, verifies it using the JWT_SECRET environment variable, and returns a boolean indicating if the token is valid.",
"sources": ["src/auth/token.ts"],
"scores": [0.92]
}
Production Considerations for 2026
This basic implementation is ready for small to medium codebases, but for production use at scale, keep these tips in mind:
- Use a code-aware chunker: Replace the simple sliding window chunker with a syntax-aware splitter to avoid breaking code context, which will improve search accuracy by 30-40% for most codebases.
- Optimize your model selection: For large codebases, use a 13B parameter code LLM like
codellama:13b-instructfor better answer accuracy, or fine-tune a small model on your internal codebase for even better performance. - Add authentication: Secure your API with JWT or API key authentication to prevent unauthorized access to your proprietary code.
- Cache frequent queries: Use Redis to cache common Q&A pairs to reduce LLM inference load and cut response latency by 50%+ for repeated questions.
- Scale your Postgres instance: For codebases with 100k+ files, use a managed Postgres provider with auto-scaling and adjust your HNSW index parameters for faster search.
Conclusion
This stack gives you a fully self-hosted, low-cost, and private codebase Q&A solution that is far more customizable than cloud alternatives. With Bun’s speed, pgvector’s optimized search, and Ollama’s local inference, you can build a tool that scales with your team’s needs, without sacrificing data privacy or blowing your budget. In 2026, self-hosted AI tools are no longer a niche experiment—they’re the smart choice for development teams that want full control over their workflow.
Enjoyed this article?
Read More Articles