Building a Lead and CRM System Into Your Own Portfolio Site
A portfolio that captures leads but has no system for managing them is a leaky bucket. Here is how to build a CRM directly into a Next.js portfolio site using Supabase.
Table of contents
Most developer portfolio contact forms do the same thing: they send you an email, and then what happens to the lead depends entirely on whether you check your inbox at the right time.
That is a leaky bucket. Inquiries come in, some get followed up promptly, others slip through because the email got buried. There is no pipeline view, no follow-up tracking, no history.
The fix is a CRM built directly into the admin panel.
What the CRM Tracks
The CRM on santoshpaudel.me has four core concepts:
Clients — One record per person or company. Fields: name, email, company, pipeline stage, notes, source (which page or channel they came from).
Pipeline stages — lead → prospect → proposal → active → won | lost | paused. Each stage has a visual indicator in the admin. The pipeline view shows all clients grouped by stage.
Tasks — Attached to a client record. Fields: title, due date, completed, notes. Tasks show up in the dashboard when they are due in the next 48 hours.
Interactions — A log of every touchpoint: emails sent, calls made, proposals submitted. Each interaction has a date, type, and notes field. This gives you a complete history without relying on email threads.
The Database Schema
CREATE TABLE clients (
id BIGSERIAL PRIMARY KEY,
name TEXT NOT NULL,
email TEXT,
company TEXT,
pipeline_stage TEXT DEFAULT 'lead'
CHECK (pipeline_stage IN ('lead','prospect','proposal','active','won','lost','paused')),
source TEXT,
notes TEXT,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE TABLE client_tasks (
id BIGSERIAL PRIMARY KEY,
client_id BIGINT NOT NULL REFERENCES clients(id) ON DELETE CASCADE,
title TEXT NOT NULL,
due_date DATE,
completed BOOLEAN DEFAULT FALSE,
notes TEXT,
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE TABLE client_interactions (
id BIGSERIAL PRIMARY KEY,
client_id BIGINT NOT NULL REFERENCES clients(id) ON DELETE CASCADE,
interaction_type TEXT NOT NULL,
notes TEXT NOT NULL,
interaction_date TIMESTAMPTZ DEFAULT NOW()
);
The ON DELETE CASCADE on both child tables means deleting a client removes their tasks and interactions. No orphaned records.
The Lead Inbox
The contact form on the public portfolio submits to an API route that does two things: inserts into contact_submissions (for record-keeping) and optionally inserts into clients (with stage lead) if the submission includes enough information.
// app/api/contact/route.ts
export async function POST(req: Request) {
const body = await req.json();
// Save the raw submission
await supabase.from('contact_submissions').insert({
name: body.name,
email: body.email,
message: body.message,
source: body.source,
});
// Create a CRM lead
await supabase.from('clients').insert({
name: body.name,
email: body.email,
pipeline_stage: 'lead',
source: body.source,
notes: body.message,
});
return Response.json({ success: true });
}
In the admin, the lead inbox shows all recent submissions. One click promotes a submission to the active pipeline. Inquiries that are not a fit are dismissed (soft-deleted or moved to stage lost).
The Simple Version to Build First
If a full CRM feels like too much, start with just the clients table and the lead inbox. That alone — a persistent, queryable list of every inquiry that came in, with the message and source — is 10x better than relying on email.
Add pipeline stages when you find yourself wanting to track where each conversation is. Add tasks when you find yourself forgetting to follow up. Add interaction history when you want to remember what you said last time.
The CRM grows with your actual needs. Start minimal.
Resources
- —Supabase Row Level Security — how to make user data visible only to the authenticated admin, with no extra API layer
- —Next.js API routes — how the contact form POST lands in the database without a separate backend
Related Posts
- —Case Study: santoshpaudel.me — A Portfolio That Runs Like a Business — the full platform the CRM module lives inside
- —How I Structure a Supabase + Vercel Admin Panel From Scratch — the admin panel architecture that houses the CRM
- —Your Portfolio Should Not Just Show Work — It Should Run Your Business — why the CRM matters beyond just storing contacts
Want a portfolio that captures and tracks leads instead of losing them to email? This is one of the first things I build into any client site. See my services or get in touch.
Get the free AI Prompt Pack + weekly frameworks
30+ tested prompts for images, captions, scripts & keywords, delivered instantly. Plus real insights on AI + marketing — no generic tips.
Want to implement this with guidance?
Santosh helps founders turn insights like this into real systems.
External Resources
Further Reading & Tools
Content Marketing Institute
Annual content marketing benchmarks — budgets, channels, and outcomes
HubSpot Marketing Blog
Data-driven marketing research, inbound strategy, and content guides
Semrush Blog
SEO, content, and digital marketing research and strategy guides
Marketing Week
UK's leading marketing news, strategy insight, and industry research