Skip to content
Webitops

Work

Portflossio: a portfolio tracker that stores no balances

A Casablanca Stock Exchange portfolio tracker built on an append-only event ledger where every balance is derived rather than stored, kept fast by two-dimensional cache versioning. Laravel, Inertia, Vue.

Live at
portflossio.com ↗
Status
Live, free
Timeline
2026 – present
Our role
Product design, architecture, implementation, operations
Stack
  • Laravel
  • PHP 8.4
  • Inertia 2
  • Vue 3
  • TypeScript
  • Fortify
  • SQLite
  • Chart.js
  • Pest

Portflossio tracks a private investor’s holdings on the Casablanca Stock Exchange: what they own, what it cost, what it is worth, and what they owe in capital gains tax at the end of the year.

Most trackers in this category store your position. This one refuses to.

The constraint

Portfolio accounting has a property that makes the obvious design wrong: every number a user cares about is a consequence of something else.

Your position in a stock is a consequence of your trades. Your average cost is a consequence of those trades and their fees. Your cash balance is a consequence of deposits, withdrawals, dividends and the cash legs of every trade. Your realised gain is a consequence of the order the sells happened in.

Store any of those, and you have created a second source of truth that can drift from the first. Drift in a portfolio tracker is not cosmetic. A user who corrects a trade they entered wrong three months ago, and whose average cost does not move to match, now has a tax report that is quietly false.

The decision

Only facts are stored. Everything else is computed on read.

There are two kinds of fact — capital events (deposit, withdrawal, dividend) and trade events (a signed quantity, plus fees and taxes). The log is append-only: a mistake is corrected by recording a new event, never by editing or deleting an old one. Positions, cash, weighted average cost, realised and unrealised P&L and total equity are all derived by replaying that log.

The invariants are stated up front and enforced in tests rather than left implicit:

  • You cannot sell more than you hold.
  • Cash must reconcile exactly.
  • No number appears anywhere in the UI without a traceable origin in the event log.

Two things fall out of this design almost for free, which is usually the sign it was the right one:

  • Time travel. “What did my portfolio look like last March?” is not a feature requiring stored snapshots; it is the same computation with the event stream filtered by date.
  • Inferred reinvestment. How much fresh money you put in, versus how much came from trading gains, is answerable from the log alone. Nothing has to be tagged at entry time — which matters, because users do not reliably tag anything.

What it cost

Recompute latency. Every page view replays the ledger. That is the bill for the guarantee, and it is paid with caching — which then creates the actual hard problem, because a cache over derived data has to be invalidated by everything that could change the derivation.

Rather than track which keys depend on which inputs, every cache key carries two version counters: one for the user, one for the market.

portfolio_summary_u{userId}_v{userVersion}_m{marketVersion}

Writing an event increments that user’s version. A price update increments the global market version. Either one invalidates every dependent entry — summary, tax report, chart data, asset list, sentiment — atomically, with a single increment and no key enumeration. Stale entries are never deleted; they simply become unreachable and expire on their own.

The property that makes it safe is that it is impossible to forget to invalidate something. Adding a new derived value means adding a new cache key in the same namespace, and it inherits correct invalidation by construction. Key-by-key busting has the opposite property: every new derived value is a new opportunity to miss one.

The rest of the product

  • A market data pipeline that ingests and normalises exchange prices on a schedule, with retry and freshness handling, feeding both the portfolio valuations and a public price-history record.
  • Capital gains reporting for the local regime, including loss carry-forward, exportable at year end.
  • Bilingual English and French with translated URL slugs, hreflang alternates and a generated bilingual sitemap — the marketing and public market pages are server-rendered for crawlability while the application itself is a single-page app behind authentication.
  • Authentication with optional TOTP two-factor, enforced email verification, and per-model ownership policies.
  • A zero-signup demo seeded with plausible history against real prices, so the product can be evaluated without handing over a trade ledger. It deletes itself after 48 hours.

Portflossio is live and free. The demo account needs no signup and disappears on its own — the fastest way to see the ledger model in practice.

Visit portflossio.com ↗

Engineering notes from this build