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.
Set up the database
Create a free Supabase project and run this prompt to get your schema and security set up.
"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."
Scaffold the app
This gives you the React app with routing and Supabase connected.
"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."
Build the heatmap
The signature feature — a visual grid of all your portfolio activity.
"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."
Add overdue detection
Automatically flag companies that haven't sent updates on schedule.
"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."
Wire up auth
Magic link login. Your LPs only see companies they're invested in.
"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.
# 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:
- Go to Cloudflare Dashboard → Workers & Pages → Create → Pages
- Connect your GitHub repo
- Set build command to
npm run buildand output directory todist - Add environment variables:
VITE_SUPABASE_URLandVITE_SUPABASE_ANON_KEY - 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
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.
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
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.
-- 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
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:
- Start with the database schema. Get the tables and RLS right first. Everything else flows from having the data model correct.
- Use generous overdue thresholds. If your monthly cadence fires an alert on day 31, you'll have constant noise. We settled on 45 days for monthly after testing.
- Magic links beat passwords. Your investors log in once a quarter at most. Don't make them remember another password.
- RLS is worth the setup. It feels like extra work upfront, but it means your entire frontend is permission-aware without a single
ifstatement checking user roles. - The heatmap sells it. Investors love the visual. It immediately shows which companies are active and which have gone quiet. Build it first and iterate from there.
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.