Compare commits

...

11 Commits

Author SHA1 Message Date
Thomas Kaul 07cf3c670f
Feature/add blog post: Ghostfolio 3 (#6678) 12 hours ago
Thomas Kaul c7e029bc38
Bugfix/prisma client initialization in admin service (#6764) 13 hours ago
Thomas Kaul 2abba414b0 Merge branch 'main' into next 14 hours ago
Thomas Kaul 5a7667adef
Task/upgrade nestjs to version 11.1.19 (#6756) 14 hours ago
Thomas Kaul a75a5dd3cd
Task/refresh cryptocurrencies list (20260421) (#6758) 14 hours ago
Thomas Kaul 9eecad153a
Task/upgrade Nx to version 22.6.5 (#6757) 14 hours ago
Thomas Kaul fb982df929
Bugfix/release dates in CHANGELOG.md (#6763) 16 hours ago
Thomas Kaul 8b9c8e04f2
Task/upgrade jsonpath to version 1.3.0 (#6755) 2 days ago
Thomas Kaul ef7df25496
Task/refactor subscription types (#6735) 2 days ago
Thomas Kaul ccd81bde4b
Task/update OSS Friends 20260421 (#6754) 2 days ago
Thomas Kaul a463b5511b
Task/upgrade countup.js to version 2.10.0 (#6636) 3 days ago
  1. 22
      CHANGELOG.md
  2. 5
      apps/api/src/app/access/access.controller.ts
  3. 316
      apps/api/src/app/admin/admin.service.ts
  4. 3
      apps/api/src/app/endpoints/public/public.controller.ts
  5. 4
      apps/api/src/app/endpoints/sitemap/sitemap.service.ts
  6. 12
      apps/api/src/app/portfolio/portfolio.controller.ts
  7. 7
      apps/api/src/app/user/user.service.ts
  8. 109
      apps/api/src/assets/cryptocurrencies/cryptocurrencies.json
  9. 4
      apps/api/src/middlewares/html-template.middleware.ts
  10. 7
      apps/api/src/services/data-provider/data-provider.service.ts
  11. 7
      apps/client/src/app/components/admin-overview/admin-overview.component.ts
  12. 21
      apps/client/src/app/pages/blog/2026/04/ghostfolio-3/ghostfolio-3-page.component.ts
  13. 281
      apps/client/src/app/pages/blog/2026/04/ghostfolio-3/ghostfolio-3-page.html
  14. 26
      apps/client/src/app/pages/blog/blog-page.html
  15. 9
      apps/client/src/app/pages/blog/blog-page.routes.ts
  16. 5
      apps/client/src/app/pages/portfolio/fire/fire-page.component.ts
  17. 3
      apps/client/src/app/pages/portfolio/x-ray/x-ray-page.component.ts
  18. BIN
      apps/client/src/assets/images/blog/ghostfolio-3.jpg
  19. 7
      apps/client/src/assets/oss-friends.json
  20. 3698
      package-lock.json
  21. 42
      package.json

22
CHANGELOG.md

@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## Next
### Added
- Added a blog post: _Announcing Ghostfolio 3.0_
### Changed
- Migrated from _Material Design_ 2 to _Material Design_ 3
@ -16,7 +20,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- **Breaking Change**: The `sslmode=prefer` parameter in `DATABASE_URL` is no longer supported. Please update your environment variables (see `.env`) to use `sslmode=require` if _SSL_ is enabled or remove the `sslmode` parameter entirely if _SSL_ is not used.
## 2.255.0 - 2026-03-20
## Unreleased
### Changed
- Refreshed the cryptocurrencies list
- Upgraded `countup.js` from version `2.9.0` to `2.10.0`
- Upgraded `jsonpath` from version `1.2.1` to `1.3.0`
- Upgraded `nestjs` from version `11.1.14` to `11.1.19`
- Upgraded `Nx` from version `22.6.4` to `22.6.5`
## 2.255.0 - 2026-04-20
### Changed
@ -33,7 +47,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Fixed the missing value column of the accounts table component on mobile
## 2.254.0 - 2026-03-10
## 2.254.0 - 2026-04-10
### Added
@ -51,7 +65,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Improved the style of the activity type component
## 2.253.0 - 2026-03-06
## 2.253.0 - 2026-04-06
### Added
@ -71,7 +85,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Fixed the allocations by ETF provider chart on the allocations page in the _Presenter View_
- Fixed the allocations by platform chart on the allocations page in the _Presenter View_
## 2.252.0 - 2026-03-02
## 2.252.0 - 2026-04-02
### Added

5
apps/api/src/app/access/access.controller.ts

@ -2,6 +2,7 @@ import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorat
import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard';
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import { CreateAccessDto, UpdateAccessDto } from '@ghostfolio/common/dtos';
import { SubscriptionType } from '@ghostfolio/common/enums';
import { Access } from '@ghostfolio/common/interfaces';
import { permissions } from '@ghostfolio/common/permissions';
import type { RequestWithUser } from '@ghostfolio/common/types';
@ -75,7 +76,7 @@ export class AccessController {
): Promise<AccessModel> {
if (
this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION') &&
this.request.user.subscription.type === 'Basic'
this.request.user.subscription.type === SubscriptionType.Basic
) {
throw new HttpException(
getReasonPhrase(StatusCodes.FORBIDDEN),
@ -130,7 +131,7 @@ export class AccessController {
): Promise<AccessModel> {
if (
this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION') &&
this.request.user.subscription.type === 'Basic'
this.request.user.subscription.type === SubscriptionType.Basic
) {
throw new HttpException(
getReasonPhrase(StatusCodes.FORBIDDEN),

316
apps/api/src/app/admin/admin.service.ts

@ -36,7 +36,6 @@ import {
BadRequestException,
HttpException,
Injectable,
Logger,
NotFoundException
} from '@nestjs/common';
import {
@ -44,7 +43,6 @@ import {
AssetSubClass,
DataSource,
Prisma,
PrismaClient,
Property,
SymbolProfile
} from '@prisma/client';
@ -280,186 +278,178 @@ export class AdminService {
const extendedPrismaClient = this.getExtendedPrismaClient();
try {
const symbolProfileResult = await Promise.all([
extendedPrismaClient.symbolProfile.findMany({
skip,
take,
where,
orderBy: [...orderBy, { id: sortDirection }],
select: {
_count: {
select: {
activities: true,
watchedBy: true
}
},
activities: {
orderBy: [{ date: 'asc' }],
select: { date: true },
take: 1
},
assetClass: true,
assetSubClass: true,
comment: true,
countries: true,
currency: true,
dataSource: true,
id: true,
isActive: true,
isUsedByUsersWithSubscription: true,
name: true,
scraperConfiguration: true,
sectors: true,
symbol: true,
SymbolProfileOverrides: true
}
}),
this.prismaService.symbolProfile.count({ where })
]);
const assetProfiles = symbolProfileResult[0];
let count = symbolProfileResult[1];
const lastMarketPrices = await this.prismaService.marketData.findMany({
distinct: ['dataSource', 'symbol'],
orderBy: { date: 'desc' },
const symbolProfileResult = await Promise.all([
extendedPrismaClient.symbolProfile.findMany({
skip,
take,
where,
orderBy: [...orderBy, { id: sortDirection }],
select: {
_count: {
select: {
activities: true,
watchedBy: true
}
},
activities: {
orderBy: [{ date: 'asc' }],
select: { date: true },
take: 1
},
assetClass: true,
assetSubClass: true,
comment: true,
countries: true,
currency: true,
dataSource: true,
marketPrice: true,
symbol: true
id: true,
isActive: true,
isUsedByUsersWithSubscription: true,
name: true,
scraperConfiguration: true,
sectors: true,
symbol: true,
SymbolProfileOverrides: true
}
}),
this.prismaService.symbolProfile.count({ where })
]);
const assetProfiles = symbolProfileResult[0];
let count = symbolProfileResult[1];
const lastMarketPrices = await this.prismaService.marketData.findMany({
distinct: ['dataSource', 'symbol'],
orderBy: { date: 'desc' },
select: {
dataSource: true,
marketPrice: true,
symbol: true
},
where: {
dataSource: {
in: assetProfiles.map(({ dataSource }) => {
return dataSource;
})
},
where: {
dataSource: {
in: assetProfiles.map(({ dataSource }) => {
return dataSource;
})
},
symbol: {
in: assetProfiles.map(({ symbol }) => {
return symbol;
})
}
symbol: {
in: assetProfiles.map(({ symbol }) => {
return symbol;
})
}
});
}
});
const lastMarketPriceMap = new Map<string, number>();
const lastMarketPriceMap = new Map<string, number>();
for (const { dataSource, marketPrice, symbol } of lastMarketPrices) {
lastMarketPriceMap.set(
getAssetProfileIdentifier({ dataSource, symbol }),
marketPrice
);
}
for (const { dataSource, marketPrice, symbol } of lastMarketPrices) {
lastMarketPriceMap.set(
getAssetProfileIdentifier({ dataSource, symbol }),
marketPrice
);
}
let marketData: AdminMarketDataItem[] = await Promise.all(
assetProfiles.map(
async ({
_count,
activities,
assetClass,
assetSubClass,
comment,
countries,
currency,
dataSource,
id,
isActive,
isUsedByUsersWithSubscription,
name,
sectors,
symbol,
SymbolProfileOverrides
}) => {
let countriesCount = countries ? Object.keys(countries).length : 0;
const lastMarketPrice = lastMarketPriceMap.get(
getAssetProfileIdentifier({ dataSource, symbol })
);
const marketDataItemCount =
marketDataItems.find((marketDataItem) => {
return (
marketDataItem.dataSource === dataSource &&
marketDataItem.symbol === symbol
);
})?._count ?? 0;
let sectorsCount = sectors ? Object.keys(sectors).length : 0;
if (SymbolProfileOverrides) {
assetClass = SymbolProfileOverrides.assetClass ?? assetClass;
assetSubClass =
SymbolProfileOverrides.assetSubClass ?? assetSubClass;
if (
(SymbolProfileOverrides.countries as unknown as Prisma.JsonArray)
?.length > 0
) {
countriesCount = (
SymbolProfileOverrides.countries as unknown as Prisma.JsonArray
).length;
}
let marketData: AdminMarketDataItem[] = await Promise.all(
assetProfiles.map(
async ({
_count,
activities,
name = SymbolProfileOverrides.name ?? name;
if (
(SymbolProfileOverrides.sectors as unknown as Sector[])?.length >
0
) {
sectorsCount = (
SymbolProfileOverrides.sectors as unknown as Prisma.JsonArray
).length;
}
}
return {
assetClass,
assetSubClass,
comment,
countries,
countriesCount,
currency,
dataSource,
id,
isActive,
isUsedByUsersWithSubscription,
lastMarketPrice,
marketDataItemCount,
name,
sectors,
sectorsCount,
symbol,
SymbolProfileOverrides
}) => {
let countriesCount = countries ? Object.keys(countries).length : 0;
const lastMarketPrice = lastMarketPriceMap.get(
getAssetProfileIdentifier({ dataSource, symbol })
);
const marketDataItemCount =
marketDataItems.find((marketDataItem) => {
return (
marketDataItem.dataSource === dataSource &&
marketDataItem.symbol === symbol
);
})?._count ?? 0;
let sectorsCount = sectors ? Object.keys(sectors).length : 0;
if (SymbolProfileOverrides) {
assetClass = SymbolProfileOverrides.assetClass ?? assetClass;
assetSubClass =
SymbolProfileOverrides.assetSubClass ?? assetSubClass;
if (
(
SymbolProfileOverrides.countries as unknown as Prisma.JsonArray
)?.length > 0
) {
countriesCount = (
SymbolProfileOverrides.countries as unknown as Prisma.JsonArray
).length;
}
name = SymbolProfileOverrides.name ?? name;
if (
(SymbolProfileOverrides.sectors as unknown as Sector[])
?.length > 0
) {
sectorsCount = (
SymbolProfileOverrides.sectors as unknown as Prisma.JsonArray
).length;
}
}
return {
assetClass,
assetSubClass,
comment,
currency,
countriesCount,
dataSource,
id,
isActive,
lastMarketPrice,
name,
symbol,
marketDataItemCount,
sectorsCount,
activitiesCount: _count.activities,
date: activities?.[0]?.date,
isUsedByUsersWithSubscription:
await isUsedByUsersWithSubscription,
watchedByCount: _count.watchedBy
};
}
)
);
if (presetId) {
if (presetId === 'ETF_WITHOUT_COUNTRIES') {
marketData = marketData.filter(({ countriesCount }) => {
return countriesCount === 0;
});
} else if (presetId === 'ETF_WITHOUT_SECTORS') {
marketData = marketData.filter(({ sectorsCount }) => {
return sectorsCount === 0;
});
activitiesCount: _count.activities,
date: activities?.[0]?.date,
isUsedByUsersWithSubscription: await isUsedByUsersWithSubscription,
watchedByCount: _count.watchedBy
};
}
)
);
count = marketData.length;
if (presetId) {
if (presetId === 'ETF_WITHOUT_COUNTRIES') {
marketData = marketData.filter(({ countriesCount }) => {
return countriesCount === 0;
});
} else if (presetId === 'ETF_WITHOUT_SECTORS') {
marketData = marketData.filter(({ sectorsCount }) => {
return sectorsCount === 0;
});
}
return {
count,
marketData
};
} finally {
await extendedPrismaClient.$disconnect();
Logger.debug('Disconnect extended prisma client', 'AdminService');
count = marketData.length;
}
return {
count,
marketData
};
}
public async getMarketDataBySymbol({
@ -704,8 +694,6 @@ export class AdminService {
}
private getExtendedPrismaClient() {
Logger.debug('Connect extended prisma client', 'AdminService');
const symbolProfileExtension = Prisma.defineExtension((client) => {
return client.$extends({
result: {
@ -746,7 +734,7 @@ export class AdminService {
});
});
return new PrismaClient().$extends(symbolProfileExtension);
return this.prismaService.$extends(symbolProfileExtension);
}
private async getMarketDataForCurrencies(): Promise<AdminMarketData> {

3
apps/api/src/app/endpoints/public/public.controller.ts

@ -7,6 +7,7 @@ import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interc
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
import { DEFAULT_CURRENCY } from '@ghostfolio/common/config';
import { SubscriptionType } from '@ghostfolio/common/enums';
import { getSum } from '@ghostfolio/common/helper';
import { PublicPortfolioResponse } from '@ghostfolio/common/interfaces';
import type { RequestWithUser } from '@ghostfolio/common/types';
@ -58,7 +59,7 @@ export class PublicController {
});
if (this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION')) {
hasDetails = user.subscription.type === 'Premium';
hasDetails = user.subscription.type === SubscriptionType.Premium;
}
const [

4
apps/api/src/app/endpoints/sitemap/sitemap.service.ts

@ -120,6 +120,10 @@ export class SitemapService {
{
languageCode: 'en',
routerLink: ['2025', '11', 'black-weeks-2025']
},
{
languageCode: 'en',
routerLink: ['2026', '04', 'ghostfolio-3']
}
]
.map(({ languageCode, routerLink }) => {

12
apps/api/src/app/portfolio/portfolio.controller.ts

@ -17,6 +17,7 @@ import {
HEADER_KEY_IMPERSONATION,
UNKNOWN_KEY
} from '@ghostfolio/common/config';
import { SubscriptionType } from '@ghostfolio/common/enums';
import {
PortfolioDetails,
PortfolioDividendsResponse,
@ -92,7 +93,8 @@ export class PortfolioController {
let hasError = false;
if (this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION')) {
hasDetails = this.request.user.subscription.type === 'Premium';
hasDetails =
this.request.user.subscription.type === SubscriptionType.Premium;
}
const filters = this.apiService.buildFiltersFromQueryParams({
@ -356,7 +358,7 @@ export class PortfolioController {
if (
this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION') &&
this.request.user.subscription.type === 'Basic'
this.request.user.subscription.type === SubscriptionType.Basic
) {
dividends = dividends.map((item) => {
return nullifyValuesInObject(item, ['investment']);
@ -484,7 +486,7 @@ export class PortfolioController {
if (
this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION') &&
this.request.user.subscription.type === 'Basic'
this.request.user.subscription.type === SubscriptionType.Basic
) {
investments = investments.map((item) => {
return nullifyValuesInObject(item, ['investment']);
@ -596,7 +598,7 @@ export class PortfolioController {
if (
this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION') &&
this.request.user.subscription.type === 'Basic'
this.request.user.subscription.type === SubscriptionType.Basic
) {
performanceInformation.chart = performanceInformation.chart.map(
(item) => {
@ -624,7 +626,7 @@ export class PortfolioController {
if (
this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION') &&
this.request.user.subscription.type === 'Basic'
this.request.user.subscription.type === SubscriptionType.Basic
) {
for (const category of report.xRay.categories) {
category.rules = null;

7
apps/api/src/app/user/user.service.ts

@ -32,6 +32,7 @@ import {
TAG_ID_EXCLUDE_FROM_ANALYSIS,
locale as defaultLocale
} from '@ghostfolio/common/config';
import { SubscriptionType } from '@ghostfolio/common/enums';
import {
User as IUser,
SystemMessage,
@ -156,7 +157,7 @@ export class UserService {
if (
this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION') &&
subscription.type === 'Basic'
subscription.type === SubscriptionType.Basic
) {
tags = [];
}
@ -443,7 +444,7 @@ export class UserService {
createdAt: user.createdAt
});
if (user.subscription?.type === 'Basic') {
if (user.subscription?.type === SubscriptionType.Basic) {
const daysSinceRegistration = differenceInDays(
new Date(),
user.createdAt
@ -485,7 +486,7 @@ export class UserService {
// Reset holdings view mode
user.settings.settings.holdingsViewMode = undefined;
} else if (user.subscription?.type === 'Premium') {
} else if (user.subscription?.type === SubscriptionType.Premium) {
if (!hasRole(user, Role.DEMO)) {
currentPermissions.push(permissions.createApiKey);
currentPermissions.push(permissions.enableDataProviderGhostfolio);

109
apps/api/src/assets/cryptocurrencies/cryptocurrencies.json

@ -40,6 +40,7 @@
"0NE": "Stone",
"0X0": "0x0.ai",
"0X1": "0x1.tools: AI Multi-tool Plaform",
"0X63SPIKE": "Spike",
"0XBTC": "0xBitcoin",
"0XCOCO": "0xCoco",
"0XDEV": "DEVAI",
@ -773,7 +774,7 @@
"ANALY": "Analysoor",
"ANARCHISTS": "Anarchists Prime",
"ANAT": "Anatolia Token",
"ANB": "Angryb",
"ANB": "Ant BlockChain",
"ANC": "Anchor Protocol",
"ANCHOR": "AnchorSwap",
"ANCIENTKING": "Ancient Kingdom",
@ -798,6 +799,7 @@
"ANGLE": "ANGLE",
"ANGO": "Aureus Nummus Gold",
"ANGRYSLERF": "ANGRYSLERF",
"ANGRYTOKEN": "Angryb",
"ANI": "Ani Grok Companion (anicompanion.net)",
"ANIM": "Animalia",
"ANIMA": "Realm Anima",
@ -1047,6 +1049,7 @@
"ARTDRAW": "ArtDraw",
"ARTE": "Artemine",
"ARTEM": "Artem",
"ARTEMIS": "OFFICIAL ARTEMIS",
"ARTEON": "Arteon",
"ARTEQ": "artèQ",
"ARTEX": "Artex",
@ -1120,6 +1123,10 @@
"ASTA": "ASTA",
"ASTER": "Aster",
"ASTERINU": "Aster INU",
"ASTEROID": "ASTEROID",
"ASTEROIDBOT": "Asteroid Bot",
"ASTEROIDCOIN": "ASTEROID",
"ASTEROIDETH": "Asteroid",
"ASTHERUSUSDF": "Astherus USDF",
"ASTO": "Altered State Token",
"ASTON": "Aston",
@ -1564,6 +1571,7 @@
"BANANO": "Banano",
"BANC": "Babes and Nerds",
"BANCA": "BANCA",
"BANCORUSD": "USD Bancor",
"BAND": "Band Protocol",
"BANDEX": "Banana Index",
"BANDIT": "Bandit on Base",
@ -1602,11 +1610,12 @@
"BART": "BarterTrade",
"BARTKRC": "BART Token",
"BARY": "Bary",
"BAS": "BNB Attestation Service",
"BASEAI": "BaseAI",
"BASEBEAR": "BBQ",
"BASECAT": "BASE CAT",
"BASECOIN": "BASECOIN",
"BASED": "Based Money",
"BASED": "Based Token",
"BASEDAI": "BasedAI",
"BASEDALF": "Based Alf",
"BASEDB": "Based Bonk",
@ -1614,12 +1623,13 @@
"BASEDCOPE": "COPE",
"BASEDFINANCE": "Based",
"BASEDHOPPY": "Based Hoppy (basedhoppy.vip)",
"BASEDMONEY": "Based Money",
"BASEDMONEYV1": "Based Money v1",
"BASEDP": "Based Pepe",
"BASEDR": "Based Rabbit",
"BASEDS": "BasedSwap",
"BASEDSB": "Based Street Bets",
"BASEDTURBO": "Based Turbo",
"BASEDV1": "Based Money v1",
"BASEHEROES": "Baseheroes",
"BASEPROTOCOL": "Base Protocol",
"BASESWAPX": "BaseX",
@ -1637,6 +1647,7 @@
"BASISSHAREV2": "Basis Share",
"BASK": "BasketDAO",
"BAST": "Bast",
"BASTEROID": "BABY ASTEROID",
"BASTET": "Bastet Goddess",
"BAT": "Basic Attention Token",
"BATCH": "BATCH Token",
@ -2584,7 +2595,7 @@
"BOZO": "BOZO",
"BOZOH": "bozo Hybrid",
"BOZY": "Book of Crazy",
"BP": "BunnyPark",
"BP": "Backpack",
"BPAD": "BlokPad",
"BPADA": "Binance-Peg Cardano (Binance Bridge)",
"BPAVAX": "Binance-Peg Avalanche (Binance Bridge)",
@ -2650,6 +2661,7 @@
"BREAD": "Breadchain Cooperative",
"BREE": "CBDAO",
"BREED": "BreederDAO",
"BRENT": "Brent Crude",
"BREPE": "BREPE",
"BRETARDIO": "Bretardio",
"BRETT": "Brett Base",
@ -2965,6 +2977,7 @@
"BULLA": "BULLA",
"BULLBEAR": "BullBear AI",
"BULLC": "BuySell",
"BULLDOG": "BullDog Coin",
"BULLF": "BULL FINANCE",
"BULLGOD": "Bull God",
"BULLI": "Bullish On Ethereum",
@ -2997,6 +3010,7 @@
"BUNNY": "Pancake Bunny",
"BUNNYINU": "Bunny Inu",
"BUNNYM": "BUNNY MEV BOT",
"BUNNYP": "BunnyPark",
"BUNNYROCKET": "BunnyRocket",
"BURG": "Burger",
"BURGER": "Burger Swap",
@ -3207,7 +3221,8 @@
"CARTIER": "Cartier",
"CARV": "CARV",
"CAS": "Cashaa",
"CASH": "CashCoin",
"CASH": "CASH",
"CASHCOIN": "CashCoin",
"CASHIO": "Cashio Dollar",
"CASHLY": "Cashly",
"CASHT": "Cash Tech",
@ -3232,7 +3247,7 @@
"CATCEO": "CATCEO",
"CATCH": "SpaceCatch",
"CATCO": "CatCoin",
"CATCOIN": "CatCoin",
"CATCOINCASH": "CatCoin",
"CATCOINETH": "Catcoin",
"CATCOINIO": "Catcoin",
"CATCOINOFSOL": "Cat Coin",
@ -3488,7 +3503,7 @@
"CHARTIQ": "ChartIQ",
"CHAS": "Chasm",
"CHASH": "CleverHash",
"CHAT": "Solchat",
"CHAT": "OpenChat",
"CHATAI": "ChatAI Token",
"CHATGPT": "AI Dragon",
"CHATOSHI": "chAtoshI",
@ -3826,7 +3841,8 @@
"COFIX": "CoFIX",
"COFOUNDIT": "Cofound.it",
"COG": "Cognitio",
"COGE": "Cogecoin",
"COGE": "Copper Doge",
"COGECOIN": "Cogecoin",
"COGEN": "Cogenero",
"COGI": "COGI",
"COGS": "Cogmento",
@ -4350,6 +4366,7 @@
"CWR": "Cowrium",
"CWS": "Crowns",
"CWT": "CrossWallet",
"CWU": "Commonwealth",
"CWV": "CryptoWave",
"CWX": "Crypto-X",
"CWXT": "CryptoWorldXToken",
@ -4673,6 +4690,7 @@
"DEFIK": "DeFi Kingdoms JADE",
"DEFIL": "DeFIL",
"DEFILAB": "Defi",
"DEFINITIVE": "Edge",
"DEFISCALE": "DeFiScale",
"DEFISSI": "DEFI.ssi",
"DEFIT": "Digital Fitness",
@ -5343,7 +5361,8 @@
"DTV": "DraperTV",
"DTX": "DataBroker DAO",
"DUA": "Brillion",
"DUAL": "Dual Finance",
"DUAL": "DUAL",
"DUALDAOTOKEN": "Dual Finance",
"DUB": "DubCoin",
"DUBAICAT": "Dubai Cat",
"DUBBZ": "Dubbz",
@ -5487,6 +5506,7 @@
"EARTH": "Earth Token",
"EARTHCOIN": "EarthCoin",
"EARTHM": "Earthmeta",
"EARTHY": "Little Earth Buddy",
"EASY": "EASY",
"EASYF": "EasyFeedback",
"EASYMINE": "EasyMine",
@ -5566,7 +5586,7 @@
"EDEXA": "edeXa Security Token",
"EDFI": "EdFi",
"EDG": "Edgeless",
"EDGE": "Definitive",
"EDGE": "edgeX",
"EDGEACTIVITY": "EDGE Activity Token",
"EDGEAI": "EdgeAI",
"EDGEN": "LayerEdge",
@ -5650,6 +5670,7 @@
"EIGENP": "Eigenpie",
"EIM": "Expert Infra",
"EIQT": "IQ Prediction",
"EITHER": "Eitherway",
"EJAC": "EJA Coin",
"EJS": "Enjinstarter",
"EKG": "Ekon Gold",
@ -5765,6 +5786,7 @@
"EML": "EML Protocol",
"EMMM": "emmm",
"EMN.CUR": "Eastman Chemical",
"EMOGINETWORK": "EMOGI Network",
"EMOJI": "MOMOJI",
"EMON": "Ethermon",
"EMONEYEUR": "e-Money EUR",
@ -6063,10 +6085,11 @@
"EUSX": "eUSX",
"EUT": "EarnUp Token",
"EUTBL": "Spiko EU T-Bills Money Market Fund",
"EV": "EVAI",
"EV": "Everything",
"EVA": "Evadore",
"EVAA": "EVAA Protocol",
"EVAI": "EVA Intelligence",
"EVAIIO": "EVAI",
"EVAL": "Chromia's EVAL by Virtuals",
"EVAN": "Evanesco Network",
"EVAULT": "EthereumVault",
@ -7101,6 +7124,7 @@
"GENIESWAP": "GenieSwap",
"GENIESWAPV1": "GenieSwap v1",
"GENIFYART": "Genify ART",
"GENIUS": "Genius",
"GENIX": "Genix",
"GENO": "GenomeFi",
"GENOME": "GenomesDao",
@ -7740,6 +7764,7 @@
"HACHIK": "Hachiko",
"HACHIKO": "Hachiko Inu Token",
"HACHIKOINU": "Hachiko Inu",
"HACHIKOTOKEN": "Hachiko",
"HACHIONB": "Hachi On Base",
"HACHITOKEN": "Hachi",
"HACK": "HACK",
@ -7747,6 +7772,7 @@
"HAEDAL": "Haedal Protocol",
"HAGGIS": "New Born Haggis Pygmy Hippo",
"HAHA": "Hasaki",
"HAHAYESRIZO": "Haha Yes Hedgehog",
"HAI": "Hacken Token",
"HAIO": "HAiO",
"HAIR": " HairDAO",
@ -7776,6 +7802,7 @@
"HANACOIN": "Hanacoin",
"HANAETH": "Hana",
"HANAETHCTO": "HANA",
"HANC": "OddHanc",
"HAND": "ShowHand",
"HANDY": "Handy",
"HANK": "Hank",
@ -7806,6 +7833,7 @@
"HASBIK": "Hasbulla",
"HASH": "Provenance Blockchain",
"HASHAI": "HashAI",
"HASHCOIN": "HASH Coin",
"HASHNET": "HashNet BitEco",
"HASHT": "HASH Token",
"HASUI": "Haedal",
@ -8411,6 +8439,7 @@
"IG": "IG Token ",
"IGCH": "IG-Crypto Holding",
"IGG": "IG Gold",
"IGGT": "The Invincible Game Token",
"IGI": "Igi",
"IGNIS": "Ignis",
"IGT": "Infinitar",
@ -8526,7 +8555,8 @@
"INNBC": "Innovative Bioresearch Coin",
"INNOU": "Innou",
"INNOVAMINEX": "InnovaMinex",
"INO": "Ino Coin",
"INO": "InoAi",
"INOCOIN": "Ino Coin",
"INOVAI": "INOVAI",
"INP": "Ionic Pocket Token",
"INRT": "INRToken",
@ -8709,6 +8739,7 @@
"IVY": "IvyKoin",
"IVZ": "InvisibleCoin",
"IW": "iWallet",
"IWC": "IWC",
"IWFT": "İstanbul Wild Cats",
"IWMON": "iShares Russell 2000 ETF (Ondo Tokenized)",
"IWT": "IwToken",
@ -9382,6 +9413,7 @@
"KRD": "Krypton DAO",
"KREDS": "KREDS",
"KREST": "krest Network",
"KRGN": "Kerrigan Network",
"KRIDA": "KridaFans",
"KRIPTO": "Kripto",
"KRL": "Kryll",
@ -9897,7 +9929,7 @@
"LOKA": "League of Kingdoms",
"LOKR": "Polkalokr",
"LOKY": "Loky by Virtuals",
"LOL": "EMOGI Network",
"LOL": "LOL",
"LOLA": "Lola",
"LOLATHECAT": "Lola",
"LOLC": "LOL Coin",
@ -9907,9 +9939,11 @@
"LOLO": "Lolo",
"LOLONBSC": "LOL",
"LON": "Tokenlon",
"LONG": "Longdrink Finance",
"LONG": "LONG",
"LONGDRINK": "Longdrink Finance",
"LONGEVITY": "longevity",
"LONGFU": "LONGFU",
"LONGFUN": "Long",
"LONGM": "Long Mao",
"LONGSHINE": "LongShine",
"LOOBY": "Looby by Stephen Bliss",
@ -10360,6 +10394,7 @@
"MAXL": "Maxi protocol",
"MAXR": "Max Revive",
"MAXX": "MAXX Finance",
"MAXXING": "Maxxing",
"MAY": "Mayflower",
"MAYA": "Maya",
"MAYACOIN": "MayaCoin",
@ -10667,6 +10702,8 @@
"MEXC": "MEXC Token",
"MEXP": "MOJI Experience Points",
"MEY": "Mey Network",
"MEZO": "MEZO",
"MEZOUSD": "Mezo USD",
"MEZZ": "MEZZ Token",
"MF": "Moonwalk Fitness",
"MF1": "Meta Finance",
@ -10713,7 +10750,8 @@
"MHT": "Mouse Haunt",
"MHUNT": "MetaShooter",
"MI": "XiaoMiCoin",
"MIA": "MiamiCoin",
"MIA": "MIA",
"MIAMICOIN": "MiamiCoin",
"MIAO": "MIAOCoin",
"MIB": "Mobile Integrated Blockchain",
"MIBO": "miBoodle",
@ -11034,7 +11072,7 @@
"MOLO": "MOLO CHAIN",
"MOLT": "Moltbook",
"MOLTID": "MoltID",
"MOM": "Mother of Memes",
"MOM": "MOM",
"MOMA": "Mochi Market",
"MOMIJI": "MAGA Momiji",
"MOMO": "Momo",
@ -11120,6 +11158,7 @@
"MOONEY": "Moon DAO",
"MOONI": "MOON INU",
"MOONION": "Moonions",
"MOONKIN": "MOONKIN",
"MOONKIZE": "MoonKize",
"MOONLIGHT": "Moonlight Token",
"MOONPIG": "Moonpig",
@ -11156,6 +11195,7 @@
"MOTG": "MetaOctagon",
"MOTH": "MOTH",
"MOTHER": "Mother Iggy",
"MOTHEROFMEMES": "Mother of Memes",
"MOTI": "Motion",
"MOTION": "motion",
"MOTIONCOIN": "Motion",
@ -12140,7 +12180,7 @@
"OEX": "OEX",
"OF": "OFCOIN",
"OFBC": "OneFinBank Coin",
"OFC": "$OFC Coin",
"OFC": "OneFootball Club",
"OFCR": "CryptoPolice",
"OFE": "Ofero",
"OFF": "BlastOff",
@ -12278,6 +12318,7 @@
"ONLINE": "Onlinebase",
"ONLY": "OnlyCam",
"ONLYCUMIES": "OnlyCumies",
"ONLYFANSCOINS": "$OFC Coin",
"ONNO": "Onno Vault",
"ONOMY": "Onomy Protocol",
"ONOT": "ONO",
@ -12506,6 +12547,7 @@
"OXY2": "Cryptoxygen",
"OXYC": "Oxycoin",
"OYS": "Oyster Platform",
"OYSTERPEARL": "Oyster Pearl",
"OZG": "Ozagold",
"OZK": "OrdiZK",
"OZMPC": "Ozempic",
@ -13021,7 +13063,6 @@
"PINMO": "Pinmo",
"PINO": "Pinocchu",
"PINS": "PINs Network Token",
"PINU": "Piccolo Inu",
"PINU100X": "Pi INU 100x",
"PIO": "Pioneershares",
"PIP": "Pip",
@ -13412,7 +13453,7 @@
"PRISMA": "Prisma Finance",
"PRIVIX": "Privix",
"PRIX": "Privatix",
"PRL": "Oyster Pearl",
"PRL": "Perle",
"PRM": "PrismChain",
"PRMX": "PREMA",
"PRNT": "Prime Numbers",
@ -13442,6 +13483,7 @@
"PROTEO": "Proteo DeFi",
"PROTO": "Protocon",
"PROTOCOLZ": "Protocol Zero",
"PROTOKEN": "Pro Token",
"PROTON": "Proton",
"PROUD": "PROUD Money",
"PROVE": "Succinct",
@ -13540,6 +13582,7 @@
"PUMPB": "Pump",
"PUMPBTC": "pumpBTC",
"PUMPBTCXYZ": "PumpBTC",
"PUMPCADE": "PUMPCADE",
"PUMPFUNBAN": "Pump Fun Ban",
"PUMPIT": "BOGDANOFF",
"PUMPTRUMP": "PUMP TRUMP",
@ -14058,6 +14101,7 @@
"RETSA": "Retsa Coin",
"REU": "REUCOIN",
"REUNI": "Reunit Wallet",
"REUR": "Royal Euro",
"REUSDC": "Relend USDC",
"REV": "Revain",
"REV3L": "REV3AL",
@ -14159,6 +14203,7 @@
"RIPTO": "RiptoBuX",
"RIS": "Riser",
"RISE": "EverRise",
"RISECOIN": "Rise coin",
"RISEP": "Rise Protocol",
"RISEVISION": "Rise",
"RISITA": "Risitas",
@ -14166,6 +14211,7 @@
"RITE": "ritestream",
"RITO": "Ritocoin",
"RITZ": "Ritz.Game",
"RIV": "RIV Coin",
"RIVER": "River",
"RIVERPTS": "River Point Reward Token",
"RIVUS": "RivusDAO",
@ -14602,7 +14648,7 @@
"SBCH": "Smart Bitcoin Cash",
"SBE": "Sombe",
"SBEFE": "BEFE",
"SBET": "SBET",
"SBET": "Sports Bet",
"SBF": "SBF In Jail",
"SBGO": "Bingo Share",
"SBIO": "Vector Space Biosciences, Inc.",
@ -15410,6 +15456,7 @@
"SOLCAT": "CatSolHat",
"SOLCATMEME": "SOLCAT",
"SOLCEX": "SolCex",
"SOLCHAT": "Solchat",
"SOLCHICKSSHARDS": "SolChicks Shards",
"SOLE": "SoleCoin",
"SOLER": "Solerium",
@ -15420,6 +15467,7 @@
"SOLFUN": "SolFun",
"SOLGOAT": "SOLGOAT",
"SOLGUN": "Solgun",
"SOLIB": "Solitaire Blossom",
"SOLIC": "Solice",
"SOLID": "Solidified",
"SOLIDSEX": "SOLIDsex: Tokenized veSOLID",
@ -15583,7 +15631,11 @@
"SPIDER": "Spider Man",
"SPIDERMAN": "SPIDERMAN BITCOIN",
"SPIDEY": "Spidey",
"SPIK": "Spike",
"SPIKE": "Spiking",
"SPIKE1984": "Spike 1984",
"SPIKECOIN": "SPIKE",
"SPIKEFURIE": "SPIKE",
"SPILLWAYS": "SpillWays",
"SPIN": "SPIN Protocol",
"SPINT": "Spintria",
@ -15591,6 +15643,7 @@
"SPITT": "Hawk Ttuuaahh",
"SPIZ": "SPACE-iZ",
"SPK": "Spark",
"SPKI": "SPIKE INU",
"SPKL": "SpokLottery",
"SPKTR": "Ghost Coin",
"SPKY": "GhostyCash",
@ -15612,6 +15665,7 @@
"SPOOL": "Spool DAO Token",
"SPORE": "Spore",
"SPORT": "SportsCoin",
"SPORTBET": "SBET",
"SPORTFUN": "Sport.fun",
"SPORTS": "ZenSports",
"SPORTSFIX": "SportsFix",
@ -16571,6 +16625,7 @@
"TKX": "Tokenize Xchange",
"TKY": "THEKEY Token",
"TLC": "Trillioner",
"TLF": "Tradeleaf",
"TLM": "Alien Worlds",
"TLN": "Trustlines Network",
"TLOS": "Telos",
@ -17075,6 +17130,7 @@
"UBQ": "Ubiq",
"UBT": "UniBright",
"UBTC": "UnitedBitcoin",
"UBU": "UBU",
"UBX": "UBIX Network",
"UBXN": "UpBots Token",
"UBXS": "UBXS",
@ -17169,6 +17225,7 @@
"UNBNK": "Unbanked",
"UNBREAKABLE": "UnbreakableCoin",
"UNC": "UnCoin",
"UNCEROID": "unc asteroid",
"UNCL": "UNCL",
"UNCN": "Unseen",
"UNCOMMONGOODS": "UNCOMMON•GOODS",
@ -17241,6 +17298,7 @@
"UNRC": "UniversalRoyalCoin",
"UNS": "UNS TOKEN",
"UNSHETH": "unshETH Ether",
"UNT": "Uni Token",
"UNW": "UniWorld",
"UOP": "Utopia Genesis Foundation",
"UOS": "UOS",
@ -17296,7 +17354,7 @@
"USDAI": "USDai",
"USDAP": "Bond Appetite USD",
"USDAVALON": "USDa",
"USDB": "USD Bancor",
"USDB": "Blynex USD",
"USDBC": "Bridged USDC",
"USDBLAST": "USDB Blast",
"USDC": "USD Coin",
@ -17412,13 +17470,14 @@
"UTNP": "Universa",
"UTON": "uTON",
"UTOPIA": "Utopia",
"UTOPIAUSD": "Utopia USD",
"UTT": "uTrade",
"UTU": "UTU Protocol",
"UTX": "UTIX",
"UTYA": "Utya",
"UTYAB": "Utya Black",
"UUC": "USA Unity Coin",
"UUSD": "Utopia USD",
"UUSD": "Unity USD",
"UUU": "U Network",
"UVT": "UvToken",
"UW3S": "Utility Web3Shot",
@ -17621,7 +17680,8 @@
"VIKKY": "VikkyToken",
"VILADY": "Vitalik Milady",
"VIM": "VicMove",
"VIN": "VinChain",
"VIN": "VulgarTycoon",
"VINCHAIN": "VinChain",
"VINCI": "VINCI",
"VINE": "Vine Coin",
"VINU": "Vita Inu",
@ -17716,6 +17776,7 @@
"VOL": "Volume Network",
"VOLBOOST": "VolBoost",
"VOLLAR": "Vollar",
"VOLM": "VOLM",
"VOLR": "Volare Network",
"VOLT": "Volt Inu",
"VOLTA": "Volta Club",
@ -18488,6 +18549,7 @@
"XCG": "Xchange",
"XCH": "Chia",
"XCHAT": "XChat",
"XCHATSOL": "XChat",
"XCHF": "CryptoFranc",
"XCHNG": "Chainge Finance",
"XCI": "Cannabis Industry Coin",
@ -18543,6 +18605,7 @@
"XENOVERSE": "Xenoverse",
"XEP": "Electra Protocol",
"XERA": "XERA",
"XERO": "XERO",
"XERS": "X Project",
"XES": "Proxeus",
"XET": "Xfinite Entertainment Token",

4
apps/api/src/middlewares/html-template.middleware.ts

@ -83,6 +83,10 @@ const locales = {
'/en/blog/2025/11/black-weeks-2025': {
featureGraphicPath: 'assets/images/blog/black-weeks-2025.jpg',
title: `Black Weeks 2025 - ${title}`
},
'/en/blog/2026/04/ghostfolio-3': {
featureGraphicPath: 'assets/images/blog/ghostfolio-3.jpg',
title: `Announcing Ghostfolio 3.0 - ${title}`
}
};

7
apps/api/src/services/data-provider/data-provider.service.ts

@ -12,6 +12,7 @@ import {
PROPERTY_DATA_SOURCE_MAPPING
} from '@ghostfolio/common/config';
import { CreateOrderDto } from '@ghostfolio/common/dtos';
import { SubscriptionType } from '@ghostfolio/common/enums';
import {
DATE_FORMAT,
getAssetProfileIdentifier,
@ -227,7 +228,7 @@ export class DataProviderService implements OnModuleInit {
if (
this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION') &&
user.subscription.type === 'Basic'
user.subscription.type === SubscriptionType.Basic
) {
const dataProvider = this.getDataProvider(DataSource[dataSource]);
@ -591,7 +592,7 @@ export class DataProviderService implements OnModuleInit {
} else if (
dataProvider.getDataProviderInfo().isPremium &&
this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION') &&
user?.subscription.type === 'Basic'
user?.subscription.type === SubscriptionType.Basic
) {
// Skip symbols of Premium data providers for users without subscription
return false;
@ -780,7 +781,7 @@ export class DataProviderService implements OnModuleInit {
})
.map((lookupItem) => {
if (this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION')) {
if (user.subscription.type === 'Premium') {
if (user.subscription.type === SubscriptionType.Premium) {
lookupItem.dataProviderInfo.isPremium = false;
}

7
apps/client/src/app/components/admin-overview/admin-overview.component.ts

@ -8,7 +8,10 @@ import {
PROPERTY_SYSTEM_MESSAGE,
ghostfolioPrefix
} from '@ghostfolio/common/config';
import { ConfirmationDialogType } from '@ghostfolio/common/enums';
import {
ConfirmationDialogType,
SubscriptionType
} from '@ghostfolio/common/enums';
import { getDateFnsLocale } from '@ghostfolio/common/helper';
import {
Coupon,
@ -255,7 +258,7 @@ export class GfAdminOverviewComponent implements OnInit {
this.systemMessage ??
({
message: '⚒️ Scheduled maintenance in progress...',
targetGroups: ['Basic', 'Premium']
targetGroups: [SubscriptionType.Basic, SubscriptionType.Premium]
} as SystemMessage)
)
);

21
apps/client/src/app/pages/blog/2026/04/ghostfolio-3/ghostfolio-3-page.component.ts

@ -0,0 +1,21 @@
import { publicRoutes } from '@ghostfolio/common/routes/routes';
import { Component } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { RouterModule } from '@angular/router';
@Component({
host: { class: 'page' },
imports: [MatButtonModule, RouterModule],
selector: 'gf-ghostfolio-3-page',
templateUrl: './ghostfolio-3-page.html'
})
export class Ghostfolio3PageComponent {
public pricingUrl = `https://ghostfol.io/${document.documentElement.lang}/${publicRoutes.pricing.path}`;
public routerLinkAbout = publicRoutes.about.routerLink;
public routerLinkAboutChangelog =
publicRoutes.about.subRoutes.changelog.routerLink;
public routerLinkBlog = publicRoutes.blog.routerLink;
public routerLinkFeatures = publicRoutes.features.routerLink;
public routerLinkMarkets = publicRoutes.markets.routerLink;
}

281
apps/client/src/app/pages/blog/2026/04/ghostfolio-3/ghostfolio-3-page.html

@ -0,0 +1,281 @@
<div class="blog container">
<div class="row">
<div class="col-md-8 offset-md-2">
<article>
<div class="mb-4 text-center">
<h1 class="mb-1">Ghostfolio 3.0</h1>
<div class="mb-3 text-muted"><small>2026-04-22</small></div>
<img
alt="Ghostfolio 3.0 Teaser"
class="border rounded w-100"
src="../assets/images/blog/ghostfolio-3.jpg"
title="Announcing Ghostfolio 3.0"
/>
</div>
<section class="mb-4">
<p>
Since the
<a href="../en/blog/2023/09/ghostfolio-2">last major version</a>
of <a [routerLink]="routerLinkAbout">Ghostfolio</a>, we have shipped
over 250 releases. The project now counts 275+ contributors from
around the globe and has surpassed 2’300’000 pulls on Docker Hub.
These milestones reflect steady adoption and our focus on
simplifying investment tracking while prioritizing user privacy.
</p>
<p>
Today’s release marks the next major version in our Open Source
Software (OSS) journey.
</p>
</section>
<section class="mb-4">
<h2 class="h4">Introducing Ghostfolio 3.0</h2>
<p>
Ghostfolio 3.0 is the evolution of our
<a [routerLink]="routerLinkFeatures"
>open source wealth management software</a
>, with meaningful improvements for both users and developers. We
have refreshed the user interface, expanded analytics, improved
stability, added more languages, and updated the technology stack to
support these changes. Here is a closer look at a selection of what
you can expect from this
<a [routerLink]="routerLinkAboutChangelog">release</a>, alongside
many smaller additions and enhancements.
</p>
<h3 class="h5">Refreshed User Interface</h3>
<p>
Ghostfolio 3.0 comes with a refreshed user interface that modernizes
the visual appearance of the application. The updated design is
cleaner, with refined components and improved consistency across the
platform.
</p>
<h3 class="h5">Comprehensive Analytics</h3>
<p>
This release provides a broader set of tools to help you understand
your portfolio. Ghostfolio X-ray uses static analysis to highlight
potential issues and risks, with rules that can now be customized to
match your investment strategy.
</p>
<h3 class="h5">Extended Multilanguage Support</h3>
<p>
Largely driven by contributions from the community, Ghostfolio now
supports more languages than ever. The application is now available
in a growing number of languages, making it accessible to a broader
audience of investors around the world.
</p>
<h3 class="h5">Reliable Stability</h3>
<p>
A wealth management platform needs to be reliable. With Ghostfolio
3.0, we have further strengthened the robustness of our
architecture, so you can rely on Ghostfolio across different
<a [routerLink]="routerLinkMarkets">market conditions</a>.
</p>
<h3 class="h5">Empowering Self-Hosting</h3>
<p>
This release simplifies and extends the self-hosting experience. A
major addition is that self-hosters can now fully benefit from
<a target="_blank" [href]="pricingUrl">Ghostfolio Premium</a> to
make use of a professional data provider. This gives you full
control over your infrastructure while still giving you access to
high-quality market data for portfolio analytics.
</p>
<h3 class="h5">Updated Technology Stack</h3>
<p>
Under the hood, Ghostfolio 3.0 has been upgraded to
<a href="https://angular.io" target="_blank">Angular 21</a>,
<a href="https://nestjs.com" target="_blank">Nest.js 11</a>,
<a href="https://www.prisma.io" target="_blank">Prisma 7</a>, and
<a href="https://nx.dev" target="_blank">Nx 22</a>. Keeping the
technology stack up to date helps us provide a solid foundation for
users and developers.
</p>
</section>
<section class="mb-4">
<h2 class="h4">Thriving Ghostfolio Community</h2>
<p>
Ghostfolio is built in public, and its community plays a central
role in shaping the open source project. Here are some highlights of
the community growth:
</p>
<ul>
<li>
Ghostfolio has accumulated <strong>8’000+ stars</strong> on
<a
href="https://github.com/ghostfolio/ghostfolio"
target="_blank"
title="Find Ghostfolio on GitHub"
>GitHub</a
>, reflecting the growing interest and trust in the project.
</li>
<li>
The
<a
href="https://join.slack.com/t/ghostfolio/shared_invite/zt-vsaan64h-F_I0fEo5M0P88lP9ibCxFg"
target="_blank"
title="Join the Ghostfolio Slack community"
>Slack</a
>
community has grown to over <strong>1’250 members</strong>, where
investors exchange ideas and help each other.
</li>
<li>
Over
<strong>700 investors and personal finance enthusiasts</strong>
follow Ghostfolio on
<a href="https://x.com/ghostfolio_" target="_blank">X</a>
(formerly Twitter) for updates and discussions.
</li>
</ul>
<p>
There is much more to come. If you are not part of the community
yet, we would love to have you on board.
</p>
<p>
<strong>Join our Slack community</strong>: Connect with fellow
investors, share insights, and stay updated by joining our
<a
href="https://join.slack.com/t/ghostfolio/shared_invite/zt-vsaan64h-F_I0fEo5M0P88lP9ibCxFg"
target="_blank"
title="Join the Ghostfolio Slack community"
>Slack</a
>
community.
</p>
<p>
<strong>Follow us on X</strong>: For release updates and market
insights, follow
<a href="https://x.com/ghostfolio_" target="_blank"
>Ghostfolio on X</a
>
to stay informed.
</p>
<p>
<strong>Give us a Star</strong>: If Ghostfolio has been useful to
you, please consider giving us a star on
<a
href="https://github.com/ghostfolio/ghostfolio"
target="_blank"
title="Find Ghostfolio on GitHub"
>GitHub</a
>. Your support helps us continue improving Ghostfolio.
</p>
<p>
<strong>Become a contributor</strong>: Interested in getting
involved? We welcome contributions from developers who are
passionate about open source and personal finance.
<a href="https://github.com/ghostfolio/ghostfolio" target="_blank"
>Join our developer community</a
>
and help shape the future of Ghostfolio.
</p>
</section>
<section>
<p>
Ghostfolio 3.0 is the result of countless contributions, feedback,
and shared passion for open source and personal finance. Whether you
have been with us from the start or are just discovering the
project, thank you for being part of this community.
</p>
<p>Thomas from Ghostfolio</p>
</section>
<section class="mb-4">
<ul class="list-inline">
<li class="list-inline-item">
<span class="badge badge-light">Angular</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Announcement</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Collaboration</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Community</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Contribution</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Evolution</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Fintech</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Ghostfolio</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Ghostfolio 3.0</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Ghostfolio Premium</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Internationalization</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Investment</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Nest.js</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Nx</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Open Source</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">OSS</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Personal Finance</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Platform</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Portfolio</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Prisma</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Privacy</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Release</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Self-Hosting</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Software</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Stack</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Technology</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Wealth Management</span>
</li>
</ul>
</section>
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item">
<a i18n [routerLink]="routerLinkBlog">Blog</a>
</li>
<li
aria-current="page"
class="active breadcrumb-item text-truncate"
>
Announcing Ghostfolio 3.0
</li>
</ol>
</nav>
</article>
</div>
</div>
</div>

26
apps/client/src/app/pages/blog/blog-page.html

@ -8,6 +8,32 @@
finance</small
>
</h1>
<mat-card appearance="outlined" class="mb-3">
<mat-card-content class="p-0">
<div class="container p-0">
<div class="flex-nowrap no-gutters row">
<a
class="d-flex overflow-hidden p-3 w-100"
href="../en/blog/2026/04/ghostfolio-3"
>
<div class="flex-grow-1 overflow-hidden">
<div class="h6 m-0 text-truncate">
Announcing Ghostfolio 3.0
</div>
<div class="d-flex text-muted">2026-04-03</div>
</div>
<div class="align-items-center d-flex">
<ion-icon
class="chevron text-muted"
name="chevron-forward-outline"
size="small"
/>
</div>
</a>
</div>
</div>
</mat-card-content>
</mat-card>
@if (hasPermissionForSubscription) {
<mat-card appearance="outlined" class="mb-3">
<mat-card-content class="p-0">

9
apps/client/src/app/pages/blog/blog-page.routes.ts

@ -218,5 +218,14 @@ export const routes: Routes = [
(c) => c.BlackWeeks2025PageComponent
),
title: 'Black Weeks 2025'
},
{
canActivate: [AuthGuard],
path: '2026/04/ghostfolio-3',
loadComponent: () =>
import('./2026/04/ghostfolio-3/ghostfolio-3-page.component').then(
(c) => c.Ghostfolio3PageComponent
),
title: 'Ghostfolio 3.0'
}
];

5
apps/client/src/app/pages/portfolio/fire/fire-page.component.ts

@ -1,5 +1,6 @@
import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service';
import { UserService } from '@ghostfolio/client/services/user/user.service';
import { SubscriptionType } from '@ghostfolio/common/enums';
import {
FireCalculationCompleteEvent,
FireWealth,
@ -80,7 +81,7 @@ export class GfFirePageComponent implements OnInit {
: 0
}
};
if (this.user.subscription?.type === 'Basic') {
if (this.user.subscription?.type === SubscriptionType.Basic) {
this.fireWealth = {
today: {
valueInBaseCurrency: 10000
@ -113,7 +114,7 @@ export class GfFirePageComponent implements OnInit {
this.user = state.user;
this.hasPermissionToUpdateUserSettings =
this.user.subscription?.type === 'Basic'
this.user.subscription?.type === SubscriptionType.Basic
? false
: hasPermission(
this.user.permissions,

3
apps/client/src/app/pages/portfolio/x-ray/x-ray-page.component.ts

@ -2,6 +2,7 @@ import { GfRulesComponent } from '@ghostfolio/client/components/rules/rules.comp
import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service';
import { UserService } from '@ghostfolio/client/services/user/user.service';
import { UpdateUserSettingDto } from '@ghostfolio/common/dtos';
import { SubscriptionType } from '@ghostfolio/common/enums';
import {
PortfolioReportResponse,
PortfolioReportRule
@ -73,7 +74,7 @@ export class GfXRayPageComponent {
this.user = state.user;
this.hasPermissionToUpdateUserSettings =
this.user.subscription?.type === 'Basic'
this.user.subscription?.type === SubscriptionType.Basic
? false
: hasPermission(
this.user.permissions,

BIN
apps/client/src/assets/images/blog/ghostfolio-3.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 219 KiB

7
apps/client/src/assets/oss-friends.json

@ -1,5 +1,5 @@
{
"createdAt": "2025-12-08T00:00:00.000Z",
"createdAt": "2026-04-21T00:00:00.000Z",
"data": [
{
"name": "Activepieces",
@ -21,11 +21,6 @@
"description": "Fastest LLM gateway with adaptive load balancer, cluster mode, guardrails, 1000+ models support & <100 µs overhead at 5k RPS.",
"href": "https://www.getmaxim.ai/bifrost"
},
{
"name": "Cal.com",
"description": "Cal.com is a scheduling tool that helps you schedule meetings without the back-and-forth emails.",
"href": "https://cal.com"
},
{
"name": "Cap",
"description": "Cap is the open source alternative to Loom. Lightweight, powerful, and cross-platform. Record and share securely in seconds.",

3698
package-lock.json

File diff suppressed because it is too large

42
package.json

@ -76,15 +76,15 @@
"@keyv/redis": "4.4.0",
"@nestjs/bull": "11.0.4",
"@nestjs/cache-manager": "3.1.0",
"@nestjs/common": "11.1.14",
"@nestjs/config": "4.0.3",
"@nestjs/core": "11.1.14",
"@nestjs/common": "11.1.19",
"@nestjs/config": "4.0.4",
"@nestjs/core": "11.1.19",
"@nestjs/event-emitter": "3.0.1",
"@nestjs/jwt": "11.0.2",
"@nestjs/passport": "11.0.5",
"@nestjs/platform-express": "11.1.14",
"@nestjs/schedule": "6.1.1",
"@nestjs/serve-static": "5.0.4",
"@nestjs/platform-express": "11.1.19",
"@nestjs/schedule": "6.1.3",
"@nestjs/serve-static": "5.0.5",
"@openrouter/ai-sdk-provider": "0.7.2",
"@prisma/adapter-pg": "7.7.0",
"@prisma/client": "7.7.0",
@ -107,7 +107,7 @@
"cookie-parser": "1.4.7",
"countries-and-timezones": "3.8.0",
"countries-list": "3.3.0",
"countup.js": "2.9.0",
"countup.js": "2.10.0",
"date-fns": "4.1.0",
"dotenv": "17.2.3",
"dotenv-expand": "12.0.3",
@ -118,7 +118,7 @@
"helmet": "7.0.0",
"http-status-codes": "2.3.0",
"ionicons": "8.0.13",
"jsonpath": "1.2.1",
"jsonpath": "1.3.0",
"lodash": "4.18.1",
"marked": "17.0.2",
"ms": "3.0.0-canary.1",
@ -156,18 +156,18 @@
"@angular/pwa": "21.2.6",
"@eslint/eslintrc": "3.3.1",
"@eslint/js": "9.35.0",
"@nestjs/schematics": "11.0.9",
"@nestjs/testing": "11.1.14",
"@nx/angular": "22.6.4",
"@nx/eslint-plugin": "22.6.4",
"@nx/jest": "22.6.4",
"@nx/js": "22.6.4",
"@nx/module-federation": "22.6.4",
"@nx/nest": "22.6.4",
"@nx/node": "22.6.4",
"@nx/storybook": "22.6.4",
"@nx/web": "22.6.4",
"@nx/workspace": "22.6.4",
"@nestjs/schematics": "11.1.0",
"@nestjs/testing": "11.1.19",
"@nx/angular": "22.6.5",
"@nx/eslint-plugin": "22.6.5",
"@nx/jest": "22.6.5",
"@nx/js": "22.6.5",
"@nx/module-federation": "22.6.5",
"@nx/nest": "22.6.5",
"@nx/node": "22.6.5",
"@nx/storybook": "22.6.5",
"@nx/web": "22.6.5",
"@nx/workspace": "22.6.5",
"@schematics/angular": "21.2.6",
"@storybook/addon-docs": "10.1.10",
"@storybook/angular": "10.1.10",
@ -193,7 +193,7 @@
"jest": "30.2.0",
"jest-environment-jsdom": "30.2.0",
"jest-preset-angular": "16.0.0",
"nx": "22.6.4",
"nx": "22.6.5",
"prettier": "3.8.2",
"prettier-plugin-organize-attributes": "1.0.0",
"prisma": "7.7.0",

Loading…
Cancel
Save