13 KiB
Venture Fund CRM — Project Context for Claude
You are continuing development on a self-hosted CRM system for a venture fund. Below is everything you need to know about what has been built, how it works, and what comes next.
Business Context
- Fund: ~$200M AUM, currently fundraising for Fund II
- Users: Team of 5 people, accessing via browser on local network or remotely via Tailscale VPN
- Current LPs: 150 investors
- Prospects: 250+ being tracked
- Migrating from: Airtable (CSV exports available)
- Core goals:
- Eliminate sensitive LP/prospect data from third-party servers (Airtable, CRMs)
- Stop paying monthly subscription costs
- Purpose-built tool for fundraising workflow: managing existing investors, tracking new prospects, raising capital
- User: Grant (grant@ten31.xyz)
What Has Been Built (Sprint 1 — Complete)
A fully functional prototype with backend API, frontend UI, demo data, and utility scripts. Everything runs locally with zero external dependencies beyond two Python packages.
Tech Stack (Actual — differs from original plan)
The original plan called for FastAPI + SQLAlchemy + separate React build, but the build environment lacked pip/npm access. The stack was adapted to:
- Backend: Python 3 stdlib HTTP server +
sqlite3+bcrypt+PyJWT— single file, no framework - Database: SQLite with WAL mode (concurrent reads, serialized writes — fine for 5 users)
- Frontend: Single self-contained HTML file loading React 18 + Babel from CDN (unpkg)
- Deployment: Run
python3 backend/server.py— serves both API and frontend on port 8080 - Remote access: Tailscale mesh VPN (each device gets a private IP, peer-to-peer encrypted)
Project Structure
venture-crm/
├── backend/
│ ├── server.py # Complete API server (1,873 lines)
│ └── requirements.txt # bcrypt, PyJWT (for reference)
├── frontend/
│ └── index.html # Complete React SPA (2,982 lines)
├── data/
│ └── crm.db # SQLite database (created on first run)
├── scripts/
│ ├── create_user.py # CLI tool to add users
│ ├── reset_password.py # CLI tool to reset passwords
│ └── backup.sh # Database backup with 30-day retention
└── start.sh # Launch script
Database Schema
All tables use TEXT primary keys (8-char UUIDs). The database is at data/crm.db.
Tables:
users— id, username, email, password_hash, full_name, role (admin/manager/member), is_activecontacts— id, first_name, last_name, email, phone, mobile, title, organization_id (FK), contact_type (investor/prospect/advisor/other), status, source, tags (JSON), notes, linkedin_url, preferred_contact, created_by (FK)organizations— id, name, type, industry, website, phone, email, address, city, state, country, description, tags (JSON), created_by (FK)opportunities— id, name, contact_id (FK), organization_id (FK), stage (lead/outreach/meeting/due_diligence/committed/funded), commitment_amount, expected_amount, probability, expected_close_date, fund_name, description, next_step, owner_id (FK), priority (low/medium/high), lost_reasoncommunications— id, contact_id (FK), opportunity_id (FK), type (email/call/meeting/note/text), subject, body, communication_date, duration_minutes, outcome, next_action, next_action_date, attendees (JSON), created_by (FK)lp_profiles— id, contact_id (FK, unique), commitment_amount, funded_amount, commitment_date, fund_name, investor_type, accredited, legal_docs_signed, signed_date, wire_received, wire_date, k1_sent, preferred_communication, notescustom_fields— id, name, entity_type, field_type, options (JSON), required, display_ordercustom_field_values— id, custom_field_id (FK), entity_id, entity_type, valueaudit_log— id, user_id (FK), entity_type, entity_id, action, changes (JSON), created_attags— id, name (unique), color
Key indexes: contacts(contact_type, status, organization_id), opportunities(stage, owner_id, contact_id), communications(contact_id, communication_date), audit_log(entity_type, entity_id), lp_profiles(contact_id)
API Endpoints
All endpoints except auth require Authorization: Bearer <jwt_token> header. Server runs at http://0.0.0.0:8080.
Auth:
POST /api/auth/login— body: {username, password} → {token, user}POST /api/auth/register— body: {username, password, email, full_name} → {token, user}
Contacts:
GET /api/contacts?type=&status=&search=&sort=&order=&limit=&offset=&organization_id=&tag=→ {data[], total, limit, offset}GET /api/contacts/:id→ {data: {contact + communications[], opportunities[], lp_profile}}POST /api/contacts— full CRUDPUT /api/contacts/:idDELETE /api/contacts/:id
Organizations:
GET /api/organizations?search=&type=&limit=&offset=→ {data[], total}GET /api/organizations/:id→ {data: {org + contacts[], opportunities[]}}POST /api/organizations— full CRUDPUT /api/organizations/:idDELETE /api/organizations/:id
Opportunities (Pipeline):
GET /api/opportunities?stage=&owner_id=&search=&priority=&fund_name=&limit=&offset=→ {data[], total}GET /api/opportunities/:id→ {data: {opp + communications[], stage_history[]}}POST /api/opportunitiesPUT /api/opportunities/:idPATCH /api/opportunities/:id/stage— body: {stage} (logs stage change in audit)DELETE /api/opportunities/:id
Communications:
GET /api/communications?contact_id=&type=&search=&limit=&offset=→ {data[], total}GET /api/contacts/:id/communications→ same as above, scoped to contactPOST /api/communicationsPUT /api/communications/:idDELETE /api/communications/:id
LP Profiles:
GET /api/lp-profiles?fund_name=&search=→ {data[], total}GET /api/lp-profiles/:id→ {data}POST /api/lp-profiles— also sets contact type to 'investor'PUT /api/lp-profiles/:id
Reports:
GET /api/reports/dashboard→ {metrics, pipeline_stages[], recent_communications[], upcoming_actions[], recent_stage_changes[]}GET /api/reports/pipeline→ {by_stage[], by_owner[], by_priority[]}GET /api/reports/lp-breakdown→ {lps[], summary, by_type[]}GET /api/reports/activity?days=30→ {by_user[], by_day[]}
Import/Export:
POST /api/import/csv— body: {data: [...objects], entity_type, mapping: {csv_col: crm_field}, dry_run: bool}GET /api/export/contacts→ {data[]}
Other:
GET /api/tags/POST /api/tagsGET /api/usersGET /api/audit-log?entity_type=&entity_id=GET /api/health
Frontend Pages
The frontend is a single HTML file with inline CSS (dark theme) and React via CDN. Pages:
- Login — username/password form, registration option
- Dashboard — KPI cards (Total LPs, Committed $, Pipeline Value, Active Opportunities, Prospects, Monthly Comms), pipeline stage visualization, recent communications, upcoming actions, recent stage changes
- Contacts — tabbed (All/Investors/Prospects), searchable sortable table, slide-over detail panel with communications timeline and opportunities, add/edit modal
- Pipeline — Kanban-style board (Lead → Outreach → Meeting → DD → Committed → Funded), stage summary bar with $ per stage, opportunity cards with stage selector, add/edit modal
- Communications — chronological list, filter by type/contact, log new communication form
- LP Tracker — summary cards (Total Committed, Funded, Avg Check, LP Count), table with status indicators (checkmarks) for docs/wire/K1
- Import — CSV paste/upload, preview table, field mapping interface, dry-run validation, execute import
- Settings — user profile, tag management
Demo Data (Seeded Automatically)
On first run, the server seeds:
- 2 users:
admin/admin123(admin role),grant/password(admin role) - 8 organizations (Sovereign Wealth Holdings, Pacific Capital Partners, Northeast Pension Fund, Redwood Endowment, Atlas Family Office, Summit Insurance Group, Cascade Wealth Management, Blue Harbor Foundation)
- 12 contacts (6 investors, 6 prospects)
- 6 LP profiles totaling $83M committed (all Fund I, all fully funded)
- 6 pipeline opportunities totaling $40M expected (Fund II prospects at various stages)
- 8 communication records (emails, calls, meetings)
- 6 tags (High Priority, Fund I LP, Fund II Prospect, Family Office, Institutional, Re-up Target)
How to Run
pip3 install bcrypt PyJWT
cd venture-crm
python3 backend/server.py
# Open http://localhost:8080
# Login: grant / password
What Has Been Tested
All API endpoints have been verified via curl:
- Auth (login, register)
- Contact CRUD + search
- Organization CRUD
- Opportunity CRUD + stage changes
- Communication CRUD
- LP profile CRUD
- Dashboard, pipeline, LP breakdown reports
- CSV import with dry-run and field mapping
- Frontend serves correctly from the backend
What Has NOT Been Built Yet (Remaining Sprints)
Sprint 2 items still needed:
- Custom fields UI (backend schema exists but not wired to frontend forms)
- Drag-and-drop on pipeline board (currently uses dropdown stage selector)
Sprint 3: Airtable Migration + Custom Fields
- Custom field definition admin UI
- Display custom fields on contact/opportunity forms
- Actual Airtable data migration (import wizard exists but hasn't been used with real data)
Sprint 4: Reporting + Polish
- Pipeline analytics (deal velocity, conversion rates between stages)
- User activity report page
- CSV export buttons on all reports
- Bulk actions on contact list (tag multiple, assign, bulk export)
- Automated daily backup via cron
- Team setup documentation
Future Enhancements (discussed but not planned):
- Email integration (auto-log emails via IMAP)
- Calendar sync
- Task assignments linked to opportunities
- Bulk email with templates
- Two-factor authentication
- Advanced saved search filters
- Audit trail UI page
Architecture Decisions & Constraints
-
Single-file backend: The Python server is one file (
server.py) using stdlibhttp.server. No framework. This keeps deployment dead simple but means no middleware pattern, no auto-docs, no async. If the codebase grows significantly, consider migrating to FastAPI. -
Single-file frontend: The React app is one HTML file loading from CDN. No build step. This means no TypeScript, no tree-shaking, no code splitting. Babel compiles JSX in the browser. If the UI grows significantly, consider splitting into a proper Vite/React project.
-
SQLite WAL mode: Handles 5 concurrent readers + 1 writer. Fine for this team size. If the team grows past 10-15, migrate to PostgreSQL.
-
No localStorage: JWT token stored in React state only (memory). Page refresh = re-login. This is intentional for security.
-
8-char UUIDs: Generated via
uuid.uuid4()[:8]. Collision probability is negligible at this data scale. -
Tailscale for remote access: Server binds to 0.0.0.0. Tailscale gives each device a 100.x.x.x IP. No port forwarding, no public exposure.
Key Files to Read
When making changes, these are the files:
-
backend/server.py(1,873 lines) — ALL backend logic: database schema, auth, every API endpoint, seed data, server startup. Search for handler method names likehandle_list_contacts,handle_create_opportunity, etc. -
frontend/index.html(2,982 lines) — ALL frontend logic: CSS styles, React components, API client, every page. Search for component names likeDashboard,ContactsPage,PipelinePage, etc. -
scripts/create_user.py— CLI to add team members -
scripts/backup.sh— Database backup with rotation -
start.sh— Launch script
Common Modification Patterns
Adding a new field to contacts:
- In
server.py: add column to CREATE TABLE, add to INSERT/UPDATE in handler methods - In
index.html: add field to the contact form component and detail view - Delete
data/crm.dbto recreate schema (or use sqlite3 ALTER TABLE)
Adding a new pipeline stage:
- In
server.py: add toPIPELINE_STAGESlist - In
index.html: add to the stages array in the Pipeline component
Changing the color scheme:
- In
index.html: modify the CSS variables in the<style>tag (search for hex colors like#0f172a,#1e293b,#6366f1)
Adding a new API endpoint:
- In
server.py: add route matching indo_GET/do_POST/etc., then add handler method
Adding a new page:
- In
index.html: create a new component, add it to the navigation sidebar and the page router