Kryptofolio is an open-source crypto portfolio tracker built with Vue 3 and strict Hexagonal Architecture (Ports and Adapters). It serves as a visual presentation layer that displays transaction and tax information computed by the backend, utilizing a centralized backend (
apps/backend) to bridge the UI with the data sources.
β οΈ Note: This project was born as a learning endeavor and is in continuous development in its early stages.
- π Institutional Analytics & Time Series Engine: DuckDB-powered OLAP queries for daily valuation, 30d annualized volatility, ATH drawdowns, and risk metrics (Sharpe Ratio, Alpha, Beta, Win Rate, Best/Worst performing asset).
- ποΈ Strict Domain Isolation & Precise Financial Precision: Pure domain architecture using
PreciseAmountbranded string value objects (string & { __brand: 'PreciseAmount' }), isolating domain logic from external math libraries and guaranteeing zero decimal truncation. - π Synchronized Portfolio Materializer & Dynamic Base Currency: Real-time FIFO recalculation and materialization across SQLite transaction ledgers and DuckDB analytical views, with dynamic base currency configuration from user settings.
- βοΈ Global FIFO Tax Engine with Custody Traceability: Global per-asset FIFO for taxation (legally required for Spanish IRPF) is computed entirely separately from a double-entry custody ledger that tracks which account physically holds each lot. Wallet-to-wallet transfers never generate a taxable event and never reorder the FIFO queue. See FIFO Tax Engine & Custody Ledger.
- π§Ή Data Ingestion Wizard with Source Format Profiles: A multi-step interface to upload CSV/XLSX files, detect the source's own fee/format conventions (Kraken, Bitvavo, Bit2Me, Bitunix, Tangem, or a generic fallback) from its header row, perform manual adjustments with alphabetically sorted options, validate Spot vs. Futures constraints, and push valid data to the backend through the same pure ingestion pipeline used for re-ingestion.
- ποΈ Fiscal & Tax Compliance: A dedicated Tax Report view to inspect transaction logs, identify gaps (missing cost bases or negative balances), and present clean data for AEAT-compliant reporting.
- π‘ Real-Time Market Data Providers: Seamlessly orchestrates live price streaming (Server-Sent Events) and REST endpoints using a hot-swappable provider architecture. Supports Kraken, Binance, Coinbase, Bit2Me, and CoinGecko with automated caching via DuckDB and InMemory layers.
- π€ AI Agent Ready (Future Feature): The frontend is technically prepared for future AI Agent integration (e.g., Vercel AI SDK or Mastra). Since Use Cases and DTOs are isolated and validated, they can be directly exposed as LLM Tools (function calling) for natural language querying without rewriting validations.
- π‘οΈ Privacy First: Fully self-hosted. The system operates locally, ensuring API credentials and transactions are kept secure. The backend can integrate with any local database or external data source securely.
- π Local Secrets Vault: AES-256-GCM encrypted local vault for securely storing API keys. RAM memory scrubbing ensures keys are erased after use. Integrations can be dynamically enabled or disabled.
- ποΈ Hexagonal Architecture (Frontend Separation): Strict separation of concerns (Ports & Adapters). The frontend UI layer is decoupled from network protocols and local storage mechanisms, enabling absolute testability and contract safety via Zod validation schemas.
- Framework: Vue 3 (Composition API +
<script setup>) - State Management: Pinia + Pinia Colada
- Styling: TailwindCSS 4
- Charts: Lightweight Charts (TradingView) & vue-chartjs (Chart.js)
- Testing: Vitest
- Workspace: pnpm workspaces (Monorepo)
The repository is structured as a PNPM Workspaces Monorepo to cleanly decouple domains and scale efficiently:
apps/frontend/: The main Vue 3 application (UI, Pinia stores).apps/backend/: The Hono backend service β handles API routes, encrypted secrets vault, and the dual-database analytical engine. Cleanly separated intoapp.ts(routing) andindex.ts(orchestration). Exposes anAppTypefor end-to-end Hono RPC type safety.packages/core-domain/: Pure business logic (e.g., Services, Use Cases, Normalizers). Completely framework-agnostic.packages/shared-types/: Zod schemas, DTOs, and type definitions shared across the entire monorepo.packages/database/: Database abstraction layer β defines the genericIDatabasePortinterface and SQL schema files. It encapsulates the core architecture: a local-first SQLite Ledger (kryptofolio_ledger.db) for OLTP persistence, and an ephemeral DuckDB Engine for high-performance OLAP federated queries.docs/: Technical documentation covering:
We use PNPM Catalogs to maintain a single source of truth for common dependencies across all workspace packages (e.g., TypeScript, Zod, Hono).
- To update a shared dependency, edit the
catalog:block inpnpm-workspace.yamlat the root and runpnpm install. - When adding a shared dependency to a package, use
"dependency-name": "catalog:"in itspackage.json.
Turborepo orchestrates all build, test, lint, and typecheck tasks across the monorepo with automatic caching.
pnpm buildβturbo run build(respects^builddependency order)pnpm testβturbo run test --concurrency=1(cached; one package at a time, because each package's own Vitest config already caps its workers and the DuckDB suites report starvation as a timeout)pnpm typecheckβturbo run typecheck
Kryptofolio implements a strict Institutional Light design system (Tailwind v4). You can read the full specifications in DESIGN.md.
Key Rules & Usage:
- Strict Light Mode: The interface is exclusively light mode to maintain a high-contrast, institutional appearance. Do not use
dark:classes. - Tabular Data: All numerical data (prices, percentages, dates, IDs) MUST use the
.numutility class (which appliesfont-monofrom JetBrains Mono andtabular-nums) to ensure perfect vertical alignment in tables and widgets. - Semantic Coloring: We do not use generic Tailwind colors (
blue-500,slate-100). Use semantic tokens:- Surfaces:
bg-surface,bg-surface-2,bg-surface-3 - Text:
text-fg,text-muted,text-muted-2 - Financial:
text-profit,text-loss,text-warning,text-info - Interactions:
--color-accentis a deep institutional indigo. For subtle hovers in ghost buttons or selects, ALWAYS usehover:bg-accent-soft hover:text-accent-2.
- Surfaces:
# For development
cp .env.example .env
# For production
cp .env.production.example .env.productionKey Variables:
VITE_API_URL: URL ofapps/backendfrom the frontend's perspective (default:http://localhost:3001).VITE_APP_LANG: The language for the interface. Valid options are currentlyesoren.LEDGER_DB_PATH: (Backend) Path to the primary SQLite ledger database file for transactions, encrypted credentials vault, and settings (kryptofolio_ledger.db).HISTORICAL_DATA_PATH: (Backend) Path to the folder containing Hive-partitioned Parquet files for historical pricing data.MOCK_MODE: (Backend) Set totrueto use an in-memory SQLite DB (development). Default:false.
Kryptofolio uses a zero-dependency, environment-based translation system.
To choose a language:
Set VITE_APP_LANG=en (English) or VITE_APP_LANG=es (Spanish) in your .env file and restart the development server. If the variable is missing or invalid, it defaults to English.
To add a new language (e.g., French fr):
- Create a new file
src/i18n/dictionaries/fr.ts. - Copy the structure from
en.tsand translate the values. Ensure the object satisfies theI18nDictionaryinterface. - Open
src/core/infrastructure/i18n/EnvI18nAdapter.ts. - Import the new dictionary:
import { fr } from '@/i18n/dictionaries/fr' - Add it to the
dictionariesmap inside the adapter:const dictionaries: Record<string, I18nDictionary> = { en, es, fr }
- Set
VITE_APP_LANG=frin your.envfile.
Ensure you have pnpm installed.
# 1. Clone the repository
git clone https://github.com/nelomr/kryptofolio.git
cd kryptofolio
# 2. Install dependencies at the workspace root
pnpm install
# 3. Start the development environment
# Frontend only (requires apps/backend running separately)
pnpm dev
# Backend only (serves mock data on :3001)
pnpm dev:backend
# Full stack: frontend + backend simultaneously
pnpm dev:fullNote:
dev:fullconcurrently spins up the Vite frontend and the Hono backend (apps/backend), which serves type-safe mock data via Hono RPC. SetVITE_API_URLinapps/frontend/.envto point to your own backend if needed (BYOB).
We apply strict quality controls (Clean Architecture and TDD). Run these commands at the project root to validate your changes locally:
| Command | Description |
|---|---|
pnpm dev |
Starts the local frontend development server (-F @kryptofolio/frontend). |
pnpm dev:full |
Orchestrates simultaneous frontend and backend startup via Turborepo. |
pnpm test |
Runs the complete test suite across the workspace via Turborepo, one package at a time. |
pnpm typecheck |
Statically runs Vue-TSC and type checking across all packages. |
pnpm lint |
Analyzes code with ESLint across the workspace. |
pnpm build |
Compiles and bundles the project using Turborepo's caching. |
This project strictly adheres to Hexagonal Architecture (Ports and Adapters) in the frontend. It is important to note that the frontend does not execute core financial business logic (such as FIFO cost-basis allocation or realized/unrealized PnL calculation). Instead:
- Calculation Engine: The heavy lifting is delegated to the backend layer.
- Frontend Ports & Adapters: Designed purely to decouple the UI components and presentation states from network protocols, API contracts, local storage vaults, i18n configurations, and validation formats.
-
Domain Layer (
src/core/domain/) The heart of the application's client-side logic. Total Isolation: It has absolutely zero external framework dependencies (no Vue, Axios, or Zod imports).- Entities & Value Objects (
models/): Defined using pure TypeScript interfaces. We heavily utilize Branded Types (e.g.AssetIdorLotId) to guarantee type-safety across identifiers. Financial figures are strictly encapsulated in aMoneyValue Object usingdecimal.js, entirely eradicating primitive obsession and IEEE-754 floating-point errors. - Ports (
ports/): Interfaces defining the contract for data operations. The domain dictates what the client needs, not how to get it. Note: There is NOrepositoriesfolder; repository interfaces are outgoing ports.
- Entities & Value Objects (
-
Application Layer (
src/core/application/)- Use Cases (
use-cases/): Pure TypeScript classes that coordinate the Domain Ports. They contain frontend-specific orchestration logic (e.g.SaveVaultKeyUseCase,UpdateLanguageUseCase,ImportTransactionsUseCase) without any Vue reactivity or framework imports. All state mutations MUST pass through a Use Case.
- Use Cases (
-
Infrastructure Layer (
src/core/infrastructure/) The outer edge that communicates with the real world and protects the domain.- Adapters (
adapters/): Concrete implementations of the Domain Ports (e.g.RestCryptoAdapter). Must be suffixed withAdapter. Note: Mocks and API routing are managed exclusively at the backend layer. - DTOs & Anti-Corruption Layer (
dtos/): Strict Zod validation schemas (ExternalTaxSchemas.ts). These map raw API data to pure Entities and validate payload integrity before it ever touches the domain. - Dependency Injection (
di/): The "Composition Root". It instantiates the REST adapters and wires them into Vue (via provide/inject using strict symbols likeVAULT_PORT_KEY).
- Adapters (
-
Application & UI Layer (
src/composables/&src/views/)- We utilize
@pinia/coladainside specificcomposables/queriesto declaratively manage asynchronous server state fetching. - Structural Note: In this project, there is no global
src/stores/folder and Vue components NEVER importbffClient. Components consumeuse*Queries(which delegate to injected Ports) anduse*Mutations(which delegate to Use Cases). - Feature-Sliced Design (Colocation): Components specific to a single view/feature (e.g.,
MetricsRow) must live inside the view's dedicatedcomponents/directory (e.g.,src/views/Portfolio/components/). Only strictly generic, reusable UI primitives (like buttons or modals) are placed in the globalsrc/components/folder.
- We utilize
- No
anyPolicy: The production source code is 100% statically typed, with no exceptions. It is rigorously compiled usingvue-tsc --noEmit. - Global Error Bus: If a Zod schema in the Anti-Corruption Layer fails, a controlled error is emitted to the
errorBus, preventing silent runtime crashes and allowing the UI to react gracefully. - Single-User Local First: The domain model has strictly eradicated multi-tenancy. There are no
user_idortenant_idfields, guaranteeing a localized and pure architecture for individual portfolios. - Financial Precision Boundaries: All financial data crossing the ACL MUST be strings and parsed by strict Zod regex rules (e.g.,
preciseAmountSchema) before entering the Domain to prevent floating-point precision loss.
This monorepo uses Changesets for independent package versioning, ensuring changes in one package do not artificially bump unrelated packages.
However, we follow a "Frontend is King" philosophy:
- The
@kryptofolio/frontendversion acts as the de facto global application version. - During early development, developers should strongly prefer
patchbumps overminorbumps for non-critical features to ensure version numbers grow slowly and deliberately.
Releases are fully automated via our Continuous Delivery pipeline.
When a pull request with a changeset is merged to main:
- The
.github/workflows/release.ymlGitHub Action automatically runspnpm changeset version. - It bumps the
package.jsonfiles and creates a direct commit tomainbypassing PR reviews. - Packages are published automatically.
Developer Workflow:
Before opening a PR to main that modifies package code, you must run:
pnpm changesetFollow the prompts to declare your intent (patch/minor/major) and write a brief description. A .changeset/*.md file will be generated which you must commit. No changeset, no release.
This project is open-source under the AGPL-3.0 License.
