Helm Portfolio for Dummies
Syllabus Next →
Module 02 of 10

Architecture

Map the moving parts — two services, three databases, three API surfaces, and the data flows that connect them.

20 minutes Completed Module 1

The Big Picture

Portfolio looks like one application but it's actually two services sharing a database, fronted by a React UI, with several companion services wired in. Here's everything laid out:

Delio Core
Laravel / PHP
Delio Frontend
React / TypeScript / Ant Design
Portfolio API
Laravel 11 / PHP 8.3
Calc Service
Go / gRPC / port 50051
Both services read/write to the same database ↓
Landlord DB
PostgreSQL
Tenant registry
Tenant Schemas
PostgreSQL (1 schema per client)
Assets, Investors, Cash Flows
Service Rates
PostgreSQL
FX exchange rates
Redis
Cache
Calculation results

Let's walk through each piece.

Service 1: The Portfolio API (Laravel)

This is the main application — a Laravel 11 app running PHP 8.3. It's responsible for:

Key insight

The Laravel app owns all writes (creating and modifying data) and handles read orchestration (deciding what to fetch, caching results). But it delegates the actual number-crunching to the Go service.

The Action Pattern

Business logic in Portfolio follows a strict pattern: Controller → DTO → Action. Controllers don't contain business logic — they validate the request, build a Data Transfer Object, and hand it to an Action class.

For example, creating a capital call:

  1. CapitalCallController validates the request via a Form Request
  2. Builds a CreateCapitalCallDto with the validated data
  3. Calls CreateCapitalCallAction::execute($dto)
  4. The Action creates the Transaction, CashFlows, updates unit holdings, and fires events
  5. Event listeners clear caches and trigger NAV recalculations

There are Action classes for every operation: CreateDrawdownAction, CreateRedemptionTransactionAction, CreateFeeAction, ReverseImportAction, and dozens more.

Service 2: The Calculation Service (Go)

This is a dedicated Go microservice that performs all financial calculations. It communicates exclusively via gRPC on port 50051.

Why does this exist?

Financial calculations like XIRR require numerical methods (Newton's method, Secant method) with high-precision decimal arithmetic. These are computationally expensive and benefit from Go's concurrency model — the service fires parallel goroutines for different calculation types and uses worker pools for large datasets. Keeping this separate from PHP means neither blocks the other.

What It Computes (27 endpoints)

The calc service exposes 27 gRPC methods. They fall into these categories:

EndpointWhat It Returns
FundsOverviewCommitted, called, uncalled, paid-in, distributions, NAV, DPI, TVPI, capital gains, XIRR — the full dashboard
FundsOverviewPerAssetSame metrics broken out per asset
FundsOverviewPerInvestorSame metrics broken out per investor
CommittedCapitalCommitted capital for specific scope
PaidInPerInvestorPaid-in per investor (used for pro-rata distribution %)
FundsInvestedOverviewSub-position investment overview (weighted/unweighted)
CalculatedValuationPerAssetNAV per asset computed from units x VPU
EndpointWhat It Returns
DirectOverviewInvestment totals, valuation, income, yield, return on capital, multiple
DirectOverviewPerAssetSame metrics per direct asset
DirectOverviewPerInvestorSame metrics per investor
AggregatesOverviewCombined funds + directs overview
EndpointWhat It Returns
ValuationBridgeNAV waterfall: start → paid-in → distributions → valuation change → end
AllocationByGeographyPortfolio allocation by geography (pie chart data)
AllocationBySectorPortfolio allocation by sector
AllocationByVintagePortfolio allocation by vintage year
AllocationByStylePortfolio allocation by investment style
DistributionTimelineCumulative distributions over time by type
ValuationTimelineValuation changes over a date range
NetPositionNet cash position by quarter
EndpointWhat It Returns
CapitalCallDocumentPer-investor capital call calculations for notice PDFs
DistributionNoticeCalcsPer-investor distribution metrics for notice PDFs
RedemptionCalcsRedemption values: total, PPU, return of capital, realised gains
AssetTreeTransferCalcsTransfer calculations across asset hierarchy

How It Works Under the Hood

Every request to the calc service includes:

The service then:

  1. Opens a connection to the tenant's PostgreSQL schema
  2. Fires parallel goroutines for each core calculation (committed, called, paid-in, distributions, etc.)
  3. Aggregates raw results into derived metrics (DPI = distributions / paid-in, etc.)
  4. Converts currencies using the rates database if needed
  5. Returns the computed protobuf response
The calc service reads the SAME database

Both the Laravel app and the Go calc service query the same PostgreSQL tables. The Laravel app writes data; the Go service reads it to compute metrics. There's no separate analytics database or ETL pipeline. This is simple but means the calc service's queries can compete with the app's writes for database resources.

The Three API Surfaces

Portfolio exposes three distinct APIs, each serving a different consumer. This is a critical architectural detail — data enters the system through different doors.

1. REST API — The Main Door
Bearer token auth (JWT) • /api/*

This is what the React frontend calls. It handles everything users do in the UI: browsing assets, recording transactions, uploading imports, viewing dashboards. Permissions are enforced via middleware gates (can-access-portfolio, can-add-investor-transaction, etc.).

~120+ endpoints across investors, assets, transactions, valuations, tables, widgets, graphs, reports, and bulk import.

2. Machine API — System-to-System
Header auth (DELIOTENANT / DELIOSECRET) • /machine/*

Used by Delio Core to programmatically create investors, push subscriptions, investments, and capital calls into Portfolio. This is how the main platform syncs data without human intervention.

Limited surface: investors (create, update, reconcile), subscriptions, investments, capital calls, asset show — about 7 endpoints.

3. MCP API — AI Integration
Same machine auth • /mcp/portfolio

A Model Context Protocol server that exposes Portfolio as a tool for AI agents. Used by delio-ai (chat) and Morph (document-to-data transformation). Provides 12 read tools and 8 import tools.

Entity resolution by client_reference. All imports use JSON arrays, validated first, then processed through the same Actions as the XLSX pipeline.

Gotcha: The Machine API is limited

The Machine API only covers ~7 of the 17+ transaction types. Distributions, fees, fund cash movements, asset transfers, and valuations cannot be created via Machine API — they must go through the REST API (UI) or the XLSX bulk import. This is a significant gap that affects onboarding automation.

Multi-Tenant Data Isolation

Portfolio uses Spatie Multi-Tenancy with a schema-per-tenant pattern in PostgreSQL:

Landlord Database

One database, shared across all tenants. Contains only:

  • tenants table — id, name, database/schema, status
  • Tenant CRUD is managed via the Landlord API

Tenant Schemas

Each tenant gets an isolated PostgreSQL schema with the full table set:

  • investors, assets, transactions, cash_flows
  • asset_valuations, fees, taxes, etc.
  • ~25 tables per tenant

Tenant resolution happens via middleware. For the REST API, the tenant is determined from the request domain/subdomain. For the Machine API, it's from the DELIOTENANT header. The Go calc service receives the schema name explicitly with every gRPC request.

The Cache Layer

All calculation results from the Go service are cached in Redis by the Laravel app. This is critical for performance — without caching, every page load would trigger expensive gRPC computation.

AspectHow It Works
Cache key structureTagged by tenant + asset ID(s) + investor ID(s) for surgical invalidation
When caches are clearedOn any data mutation — transaction creation, valuation update, deletion. Events fire ClearAllCaches or targeted cache tags.
Cache warmingcache:clear-and-warm --tenant={id} artisan command pre-computes common dashboard calculations
Go-side cachingThe Go service also caches exchange rates in-memory (bigcache) with 24h TTL

Companion Services

Horizon is Delio's identity and permissions platform. Portfolio delegates authentication and user management to Horizon. When a new investor is added to Portfolio, it may auto-create a Horizon user so the investor can log into the platform.

Portfolio's permission constants (~80 of them) are defined in the HorizonPermissions enum and checked via gate middleware.

A separate PostgreSQL database that stores daily exchange rates. Both the Laravel app and the Go calc service connect to this database independently.

Two conversion strategies:

  • By cash flow date — for paid-in, distributions, capital calls (converts at the rate on the settlement date)
  • By cutoff date — for committed capital, NAV, investments (converts at the reporting date rate)

XLSX import files are stored on S3 before processing. Generated PDF reports (QPRs, capital call notices, distribution notices) are also stored on S3 and linked to assets/investors via attachment records in the database.

delio-ai is a Python/FastAPI chat application that uses Portfolio's MCP tools to answer investor questions in natural language.

Morph is an AI-powered data transformation tool that reads unstructured documents (fund admin statements, cap tables) and pushes structured data into Portfolio via the MCP import tools.

Request Lifecycle: End to End

Let's trace what happens when an internal user views the Portfolio dashboard:

  1. Browser → Frontend: User navigates to the Portfolio overview page in delio-frontend
  2. Frontend → REST API: React fires parallel calls: GET /widgets/capital-overview, GET /graphs/overview-bridge, GET /tables/investors
  3. REST API → Middleware: Tenant middleware resolves the client from the request domain. Permission gates verify can-access-portfolio.
  4. Laravel → Redis: Check cache for this tenant + scope. Cache hit? Return immediately.
  5. Laravel → gRPC: Cache miss? Build a protobuf request with the schema name, investor/asset scope, and preferred currency. Send to the Go service on port 50051.
  6. Go service → PostgreSQL: Opens connection to the tenant schema. Fires parallel goroutines for each core calculation (committed, called, paid-in, distributions, etc.).
  7. Go service → Rates DB: If currency conversion is needed, fetches exchange rates (from cache or rates database).
  8. Go service → Laravel: Returns computed metrics via protobuf response.
  9. Laravel → Redis: Cache the result tagged by tenant + scope.
  10. Laravel → Frontend: Return JSON response. React renders widgets, charts, and tables.

Knowledge Check

Question 1

A developer needs to add a new metric to the Portfolio dashboard. Where does the calculation logic go?

In the React frontend, computed from the data the API already returns
In the Laravel app's PHP code, in a new Action class
In the Go calculation service — add the formula to the derived calculations, expose a new gRPC endpoint or extend an existing one, then have Laravel call and cache it
Correct! The Go service owns all financial calculations. You'd add the formula to the derived calculations in Go, expose it via gRPC, then have the Laravel app call it and cache the result. The frontend just renders what the API returns.
Not quite. All financial metric calculations live in the Go gRPC service. The Laravel app handles data storage and cache orchestration. The React frontend renders results. New metrics need to be added to the Go service first.
Question 2

Delio Core needs to automatically create an investor in Portfolio when a new LP is onboarded. Which API surface should it use?

The REST API with a Bearer token
The Machine API with DELIOTENANT/DELIOSECRET headers
The MCP API
Correct! System-to-system calls between Delio Core and Portfolio use the Machine API, authenticated via DELIOTENANT/DELIOSECRET headers. The REST API is for user-facing interactions; the MCP API is for AI agents.
Not quite. Delio Core communicates with Portfolio via the Machine API (/machine/* routes), authenticated with DELIOTENANT/DELIOSECRET headers. The REST API requires JWT user tokens, and the MCP API is designed for AI agents.

What's Next

You now understand the system's moving parts and how they connect. Next, we'll zoom in on the data model — the entities stored in those tenant schemas, how they relate to each other, and the dependency chain you need to follow when setting up a new client.