See it in action

This is a working portfolio tracker with sample data — the same kind of dashboard running in production at Raspberry Ventures. Click around, switch to the Diary view, explore the data.

This isn't a mockup — it's a live app. ↓ Here's how to build your own.

Build it with Claude

Five prompts. That's all it took to go from nothing to a working portfolio tracker. Copy these into Claude, follow the steps, and you'll have your own version running in an afternoon.

1

Set up the database

Create a free Supabase project and run this prompt to get your schema and security set up.

Prompt for Claude

"I'm building a portfolio tracker for my investment syndicate. I need a Supabase database with these tables: portfolio_companies, diary_entries, company_diary_settings, investors, investor_companies, deals, and deal_carry. Set up Row Level Security so the syndicate lead sees everything and individual investors only see companies they've invested in. Use magic link auth. Give me the full SQL migration."

2

Scaffold the app

This gives you the React app with routing and Supabase connected.

Prompt for Claude

"Create a React + Vite app with React Router for my portfolio tracker. I need three pages: a Dashboard with a heatmap grid (companies as rows, 15 months as columns, colored dots for entries), a CompanyDiary page with a timeline view, and a Login page using Supabase magic links. Use the Supabase JS client for data fetching. No component library — just CSS custom properties for theming."

3

Build the heatmap

The signature feature — a visual grid of all your portfolio activity.

Prompt for Claude

"Build the heatmap component for the Dashboard. Fetch all companies and diary entries, render a grid with company names as a sticky left column and months as scrollable columns. Each cell gets a colored dot based on entry type (blue=update, green=funding, purple=closed_round, red=warning). Add hover tooltips showing the entry title and a stats bar above with active companies, overdue count, total entries, and recent activity."

4

Add overdue detection

Automatically flag companies that haven't sent updates on schedule.

Prompt for Claude

"Add overdue detection to the dashboard. Each company has a cadence (monthly, quarterly, semi_annual, annual) and a last_update_date. Use generous thresholds: 45 days for monthly, 120 for quarterly, 210 for semi-annual, 400 for annual. Flag overdue companies with a red indicator and add an 'overdue only' filter toggle."

5

Wire up auth

Magic link login. Your LPs only see companies they're invested in.

Prompt for Claude

"Add Supabase Auth with magic link login. When a user logs in, their auth.uid() should match an investor record. The syndicate lead (is_syndicate_lead = true) sees all companies and internal notes. Regular investors only see companies in their investor_companies records. Hide founder_email, founder_name, and internal_notes from the investor view."

Deploy it

The tracker is a static React build — no server needed. Build it, push to GitHub, and let Cloudflare Pages handle the rest.

Terminal # Build the production bundle npm run build # Initialize git and push git init git add . git commit -m "Initial portfolio tracker" git remote add origin https://github.com/YOU/portfolio-tracker.git git push -u origin main

Then connect the repo to Cloudflare Pages:

  1. Go to Cloudflare Dashboard → Workers & Pages → Create → Pages
  2. Connect your GitHub repo
  3. Set build command to npm run build and output directory to dist
  4. Add environment variables: VITE_SUPABASE_URL and VITE_SUPABASE_ANON_KEY
  5. Deploy — every push to main auto-deploys

On env variables

Never hardcode your Supabase URL and anon key. Use Vite's import.meta.env pattern with a .env file locally, and set the same values in Cloudflare Pages settings. The anon key is safe to expose (RLS protects the data), but keeping it in env vars is cleaner.

Under the hood

Everything above gets you a working tracker. The sections below explain how it works — useful if you want to customise it or understand what Claude built for you.

Architecture

Architecture

React + Vite frontend
Supabase REST API
PostgreSQL + RLS

Static SPA hosted on Cloudflare Pages. All data via Supabase. No server to manage.

Tech stack

Frontend: React 19 + Vite + React Router. No component library — just clean CSS with custom properties. Backend: Supabase (PostgreSQL, Auth, REST API). Row Level Security handles who sees what. Hosting: Cloudflare Pages (static build, auto-deploy from GitHub). Cost: $0 on free tiers.

Database schema

Everything starts with seven tables in Supabase. The two core ones are portfolio_companies (your companies) and diary_entries (timestamped updates about each one). The rest handle investors, deals, and carry allocations.

Schema portfolio_companies id, name, status, category, investment_year, website, one_liner, logo_url diary_entries id, company_id, entry_date, entry_type, title, body -- entry_type: update | funding | closed_round | warning company_diary_settings company_id, cadence, last_update_date, founder_name, founder_email, internal_notes -- cadence: monthly | quarterly | semi_annual | annual investors id, name, email, auth_user_id, is_syndicate_lead investor_companies investor_id, company_id, amount, currency, shares deals id, company_id, round_name, close_date, carry_split deal_carry deal_id, entity_name, carry_percentage

The company_diary_settings table is where the magic happens for overdue detection. Each company has an expected update cadence, and a trigger automatically updates last_update_date whenever a new diary entry is created.

Overdue logic

JavaScript const CADENCE_DAYS = { monthly: 45, // generous — founders are never on time quarterly: 120, semi_annual: 210, annual: 400 } function isOverdue(lastUpdateDate, cadence) { const daysSince = (Date.now() - new Date(lastUpdateDate)) / 86400000 return daysSince > CADENCE_DAYS[cadence] }

Row Level Security

This is what makes it work for syndicates. You (the syndicate lead) see everything. Your investors only see companies they have a position in. Supabase handles this automatically — no application-level filtering needed.

Key database functions -- Check if the current user is the syndicate lead CREATE FUNCTION is_syndicate_lead() RETURNS boolean AS $$ SELECT EXISTS ( SELECT 1 FROM investors WHERE auth_user_id = auth.uid() AND is_syndicate_lead = true ); $$ LANGUAGE sql SECURITY DEFINER; -- Get company IDs the current user has invested in CREATE FUNCTION my_company_ids() RETURNS SETOF uuid AS $$ SELECT DISTINCT company_id FROM investor_companies WHERE investor_id = ( SELECT id FROM investors WHERE auth_user_id = auth.uid() ); $$ LANGUAGE sql SECURITY DEFINER;

With these two functions, every RLS policy becomes simple: if is_syndicate_lead(), show everything. Otherwise, filter to my_company_ids().

Entry type colors

Design tokens update → #1565C0 (blue) — regular portfolio updates funding → #2E7D32 (green) — new funding rounds closed_round → #7B1FA2 (purple) — rounds that closed warning → #C62828 (red) — issues needing attention

Lessons from building this

A few things we learned building the Raspberry Ventures version:

What it costs

Supabase free tier covers most syndicates easily — 500MB database, 50K monthly auth users, unlimited API requests. Cloudflare Pages is free for unlimited sites. Total: $0 unless you outgrow the free tiers, which is unlikely for a syndicate portfolio tracker.