Portfolio Project
My CRM
A single-user personal CRM that turns relationship maintenance into a short daily workflow. I built it on Next.js and Supabase, with a deterministic follow-up engine, Gmail and LinkedIn messaging, and AI drafts that never send without review.
Sep 2026
Overview
My CRM is a production, single-user workspace for the part of professional networking that's easy to neglect: staying in touch after the meeting, project, or job ends. It isn't a sales pipeline or a mass-outreach tool. It keeps contacts, conversation history, and the next best action in one place, and turns them into a short, prioritized daily queue. Email and LinkedIn messages go out from inside the workspace only after I review them, and replies land back in the same contact history. It's live and running in production.
Problem
Professional relationships rarely end because someone decided to let them go. They fade because the context is scattered (the last email is in an inbox, the LinkedIn thread is somewhere else, a promised follow-up lives in a note) and nothing makes the next outreach feel timely or manageable. Traditional CRMs are built for deals and teams, not for keeping former colleagues, clients, and collaborators warm over years. The design problem wasn't storing contacts. It was making relationship maintenance a short, repeatable habit without handing judgment over to automation.
Approach
I built the product around a relationship loop rather than a funnel. Each contact has a cadence tier (Inner circle, Close, Wider ring, Long tail) and a current state: Due, Reached Out, Conversation, Follow-Through, or Resting. The engine derives who's due, when an unanswered message should resurface, and how to rank the work — outstanding promises first, then scheduled follow-ups, then overdue relationships weighted by closeness — so every item in the Action Queue has a stated reason for being there. The daily queue is capped so it reads as a to-do list rather than a backlog, and a Board view shows the whole loop at once.

Calls, meetings, and other off-platform interactions can be logged, and every interaction becomes a Touch in a single newest-first history, so the next message starts from context rather than memory. AI is a writing assistant, not an automation layer: it offers two or three context-aware drafts with an angle, a confidence score, and the signals behind them, and nothing sends without an explicit click.

The app runs on Next.js 16, React 19, TypeScript, Tailwind, and shadcn UI, deployed on Vercel, with Supabase handling Postgres, Google OAuth, row-level security, and secret storage. The relationship engine is pure TypeScript and computes state at read time rather than through a nightly job, which keeps it deterministic and testable. Messaging runs through channel adapters: Gmail through the Gmail API, with a Railway worker polling for replies, and LinkedIn through a ConnectSafely adapter with signed inbound webhooks. Outbound messages are recorded only after the provider accepts them, and unknown inbound senders are held for manual review rather than silently becoming contacts. OpenAI drafting runs server-side only, uses structured output, and caches drafts until new history arrives.
Outcome
My CRM is live and working. The full loop has been verified in production: sign in with Google, import contacts, send a reviewed Gmail message, receive the reply as a Touch, and watch the relationship state update. LinkedIn has been validated the same way for both an outbound message and a signed inbound reply, and a Metrics view tracks relationship health across the network. The result is a broad professional network converted into a short, explainable list of next actions, with the context needed to make each message personal.
The system is deliberately conservative: no mass outreach, no auto-replies, no auto-send, and no contacts created without review. It's also single-user by design, built for my own network rather than as a multi-tenant product.
