← Back to Blog
TechnicalCRMportfolioNext.js

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.

SPSantosh Paudel· May 23, 2026· 8 min read· 2 views
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 stageslead → 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


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.

No spam. Unsubscribe anytime.

Want to implement this with guidance?

Santosh helps founders turn insights like this into real systems.

AI Content Systems

External Resources

Further Reading & Tools

Related Posts

01
2 min
Next.jsSupabase
6d agoProgrammatic SEO

The Engine Room: Architecting a 200+ Page Programmatic Content System on Next.js 15 & Supabase

Managing a handful of blogs is easy. Scaling to 200+ high-quality, vertical-specific content assets is an engineering challenge. Here is the exact programmatic database schema, static generation routing, and automated pipeline behind a high-velocity content engine.

Read article
02
9 min
Next.jsSupabase
20d agoDev Sprint

How I Shipped 4 Full-Stack Platforms in 25 Days

Four production-ready sites. One developer. Twenty-five days. Here is the exact stack, timeline, and workflow — including what broke and what made it possible.

Read article
03
8 min
portfolioadmin panel
23d agoProject Deep-Dive

Case Study: santoshpaudel.me — A Portfolio That Runs Like a Business

Most developer portfolios are digital brochures. Mine has a CRM, lead management, AI content agents, and a full admin panel. Here is what I built and why.

Read article