尚未检测到更新。GitHub 发布版本后会显示在这里。
个人 GitHub 账号下公开仓库的所有者可以登录并验证所有权,自行管理介绍,无需管理员批准。
登录并生成验证文件,在 30 分钟内将其提交到此仓库的默认分支。其他分支或分叉仓库中的文件无效。然后在下方检查文件,即可无需管理员批准直接管理项目。请保留文件以维持管理权限。
如需登录,将前往登录页面。登录后请回到此处,再次点击所选的验证按钮。
在项目文件夹中运行 /nomorevibe verify,即可验证所有权并自行管理介绍。
一分钟完成验证制作者的介绍仅有上面的一句话。 更多详情请阅读仓库 README。 仓库 README ↗
正在加载完整 README。
查看完整 README ↗A full-stack crypto portfolio tracker for recording crypto BUY and SELL transactions, viewing current holdings, tracking market value, and calculating realised and unrealised profit/loss.
The app is transaction-driven. The backend replays active transaction revisions to rebuild stored holdings for open positions, preserves transaction revisions as an audit log, and records portfolio snapshots for performance history.
Project status and next work: repository audit and roadmap, updated 2026-10-25. It records confirmed calculation/frontend issues, dependency advisory triage, and the recommended portfolio-demo milestones. The working backlog distinguishes implemented features from remaining work.
crypto-portfolio-app-hazel.vercel.app · API health: /actuator/health
| Part | Hosting |
|---|---|
| Next.js frontend | Vercel |
| Spring Boot API (Docker) | Render |
| PostgreSQL | Neon (TLS verify-full) |
Runs on free tiers: the API sleeps after ~15 minutes idle, so the first request can take about a minute while it wakes. Register a throwaway account and use fictional data only. Setup and verification steps: free-tier deployment guide.
| Layer | Technology |
|---|---|
| Backend | Java 25 LTS, Spring Boot 4.1.2 |
| API | Spring MVC REST controllers |
| Security | Spring Security, JWT, BCrypt |
| Database | PostgreSQL, Spring Data JPA, Hibernate, Flyway |
| Market data | CoinGecko-compatible API via RestTemplate |
| Backend tests | JUnit Jupiter 6, Spring Boot Test, MockMvc, H2, PostgreSQL Testcontainers |
| Frontend | Next.js 16.4.3, React 19.2.8, TypeScript |
| Styling | Tailwind CSS |
| Frontend tests | Vitest, Testing Library, Playwright config |
| Local services | Docker Compose, PostgreSQL, pgAdmin |
401 (not ordinary permission-denied 403).BUY and SELL transactions..
├── README.md
├── .env.example
├── .github/workflows/ci.yml
├── backend/
│ ├── Dockerfile
│ ├── docker-compose.yml
│ ├── pom.xml
│ └── src/
│ ├── main/java/com/kevinas/crypto_portfolio_backend/
│ ├── main/resources/application.yaml
│ ├── main/resources/db/migration/
│ └── test/
├── docs/
│ ├── AGENTS.md
│ ├── REPO_OVERVIEW.md
│ └── tasklist.MD
└── frontend/
├── .env.example
├── package.json
└── src/
├── app/
├── components/
├── hooks/
└── lib/
Frontend:
cd frontend
cp .env.example .env.local
Optional backend shell env from the root example:
cp .env.example .env.local
set -a
source .env.local
set +a
Spring Boot also has local defaults in backend/src/main/resources/application.yaml, so the backend can run without sourcing a .env file when using the default Docker database.
cd backend
docker compose up -d
| Service | URL/Port | Default Login |
|---|---|---|
| PostgreSQL | localhost:5432 | postgres / postgres, database cryptodb |
| pgAdmin | http://localhost:5050 | [email protected] / admin123 |
cd backend
./mvnw spring-boot:run
Backend default URL: http://localhost:8080
cd frontend
npm install
npm run dev
Frontend default URL: http://localhost:3000
| Area | Command |
|---|---|
| Start database | cd backend && docker compose up -d |
| Stop database | cd backend && docker compose down |
| Run backend | cd backend && ./mvnw spring-boot:run |
| Backend tests | cd backend && ./mvnw test |
| Backend build | cd backend && ./mvnw package |
| Run frontend | cd frontend && npm run dev |
| Frontend tests | cd frontend && npm test |
| Frontend lint | cd frontend && npm run lint |
| Frontend build | cd frontend && npm run build |
| Playwright browsers | cd frontend && npx playwright install chromium |
| E2E test script | cd frontend && npm run test:e2e |
| Variable | Default | Purpose |
|---|---|---|
DB_URL | jdbc:postgresql://localhost:5432/cryptodb | JDBC database URL |
DB_USERNAME | postgres | Database username |
DB_PASSWORD | postgres | Database password |
PORT | — | Hosting-provider HTTP port; takes precedence over SERVER_PORT |
SERVER_PORT | 8080 | Local backend HTTP port fallback |
JWT_SECRET | local development default | JWT HMAC signing secret; required in the prod profile, must be at least 32 bytes, and is validated at startup |
JWT_EXPIRATION_SECONDS | 3600 | JWT lifetime |
APP_CORS_ALLOWED_ORIGINS | http://localhost:3000 | Comma-separated frontend origins allowed to call the API; required in the prod profile |
AUTH_RATE_LIMIT_MAX_REQUESTS | 10 | Maximum login or registration attempts from one client address per auth window |
AUTH_RATE_LIMIT_WINDOW_SECONDS | 60 | Auth rate-limit window length in seconds |
MARKET_RATE_LIMIT_MAX_REQUESTS | 60 | Maximum market-data requests from one client address per market window |
MARKET_RATE_LIMIT_WINDOW_SECONDS | 60 | Market-data rate-limit window length in seconds |
CRYPTO_API_BASE_URL | https://api.coingecko.com/api/v3 | CoinGecko-compatible market API base URL |
SPRING_JPA_SHOW_SQL | true | SQL logging toggle |
PORTFOLIO_SNAPSHOT_FIXED_DELAY_MS | 3600000 | Scheduled snapshot interval |
PORTFOLIO_SNAPSHOT_INITIAL_DELAY_MS |
Rate limits apply to POST /api/auth/login, POST /api/auth/register, and GET /api/market/**. Exceeded requests receive 429 Too Many Requests with a Retry-After header. The limiter is in-memory and per application instance; use a shared store or edge rate limiting before scaling the API horizontally.
| Variable | Example | Purpose |
|---|---|---|
NEXT_PUBLIC_API_BASE_URL | http://localhost:8080/api | Browser-visible backend API base URL |
The repository includes a basic deployment path using a Vercel frontend, Dockerized API, and managed PostgreSQL database.
Build and run the backend image from the backend directory:
docker build -t crypto-portfolio-api .
docker run --rm -p 8080:8080 \
-e SPRING_PROFILES_ACTIVE=prod \
--mount type=bind,src=/absolute/path/to/provider-ca.crt,dst=/certs/provider-ca.crt,readonly \
-e DB_URL='jdbc:postgresql://<host>/<database>?sslmode=verify-full&sslrootcert=/certs/provider-ca.crt' \
-e DB_USERNAME='<database-user>' \
-e DB_PASSWORD='<database-password>' \
-e JWT_SECRET='<at-least-32-byte-secret>' \
-e APP_CORS_ALLOWED_ORIGINS='https://<your-vercel-app>.vercel.app' \
crypto-portfolio-api
The image runs as a non-root user and defaults to prod. Production requires a unique signing key, explicit database credentials, verified PostgreSQL TLS (install the provider's trusted certificate), and exact HTTPS CORS origins. The hosting platform should provide PORT. The public application check is GET /api/health. Readiness and liveness probes are public at /actuator/health/readiness and /actuator/health/liveness; /actuator/metrics requires the ADMIN role, not just a registered account. Follow the release and restore runbook before deployment.
Deploy frontend/ as a Next.js project and set this production environment variable:
NEXT_PUBLIC_API_BASE_URL=https://<your-api-domain>/api
Set the backend's APP_CORS_ALLOWED_ORIGINS to the exact production frontend origin. Never commit real database credentials or JWT secrets.
Protected endpoints require:
Authorization: Bearer <accessToken>
| Method | Endpoint | Auth | Request | Response |
|---|---|---|---|---|
GET | /api/health | No | None | { "status": "ok" } |
GET | /actuator/health/readiness | No | None | Actuator readiness status |
GET | /actuator/health/liveness | No | None | Actuator liveness status |
GET | /actuator/metrics | Yes | None | Available Micrometer metric names |
POST | /api/auth/register | No | { "email": string, "password": string } | { "accessToken": string } |
POST | /api/auth/login | No | { "email": string, "password": string } | { "accessToken": string } |
GET | /api/market/prices?symbols=BTC,ETH | No | Query symbols list | { "BTC": 60000.00 } |
GET | /api/market/supported-coins | No | None | [{ "symbol": "BTC", "name": "Bitcoin", "coinGeckoId": "bitcoin" }] |
GET | /api/portfolio/holdings | Yes | None | HoldingResponse[] |
GET | /api/portfolio/summary | Yes | None | PortfolioSummaryResponse |
GET | /api/portfolio/performance/history?range=30d | Yes | Query range: 7d, 30d, |
{
"symbol": "BTC",
"name": "Bitcoin",
"type": "BUY",
"quantity": 0.5,
"priceUsd": 45000
}
type must be BUY or SELL. quantity must be greater than 0; priceUsd must be 0 or greater. A SELL that exceeds the stored holding quantity returns a conflict response.
Edits preserve the transaction's historical ledger position by voiding the current audit row and returning a replacement row with a new ID. Edits and deletes replay the active ledger atomically; a correction that would make any later SELL historically invalid returns 409 Conflict without changing transactions, holdings, or snapshots.
{
"id": 1,
"symbol": "BTC",
"name": "Bitcoin",
"quantity": 0.5,
"averageBuyPriceUsd": 45000.00,
"currentPriceUsd": 60000.00,
"investedValueUsd": 22500.00,
"currentValueUsd": 30000.00,
"profitLossUsd": 7500.00,
"marketPriceAvailable": true
}
{
"totalInvestedUsd": 22500.00,
"totalCurrentValueUsd": 30000.00,
"totalUnrealisedProfitLossUsd": 7500.00,
"totalRealisedProfitLossUsd": 0.00,
"totalProfitLossUsd": 7500.00,
"hasUnsupportedMarketData": false,
"holdings": [
{
"symbol": "BTC",
"name": "Bitcoin",
"quantity": 0.5,
"averageBuyPriceUsd": 45000.00,
"currentPriceUsd": 60000.00,
"investedValueUsd": 22500.00,
"currentValueUsd": 30000.00,
"unrealisedProfitLossUsd": 7500.00,
"marketPriceAvailable": true
}
]
}
{
"content": [
{
"id": 1,
"symbol": "BTC",
"name": "Bitcoin",
"type": "BUY",
"quantity": 0.5,
"priceUsd": 45000.00,
"totalValueUsd": 22500.00,
"realisedProfitUsd": 0.00,
"createdAt": "2026-06-19T20:00:00Z"
}
],
"pageNumber": 0,
"pageSize": 20,
"totalElements": 1,
"totalPages": 1,
"hasNext": false,
"hasPrevious": false
}
Supported market symbols are served by GET /api/market/supported-coins.
Current backend-supported symbols:
BTC, ETH, SOL, LINK, DOGE, ADA, XRP, DOT, MATIC, AVAX, UNI, ATOM, XLM, LTC, TRX, BCH, ALGO, ICP, NEAR, VET, BNB
Unsupported symbols can still be recorded manually, but market-value fields are flagged with marketPriceAvailable=false and portfolio totals set hasUnsupportedMarketData=true. /api/market/quotes adds fetchedAt and FRESH/STALE/UNAVAILABLE status; the legacy prices map remains compatible. Fresh quotes last 60 seconds, fallback expires after five minutes, and network waits are bounded (2s connect, 3s read). Stale totals are labelled. Snapshots with missing/stale data and legacy snapshots have valuationComplete=false and are excluded from chart lines.
Trade quantities support eight decimal places; unit prices support twelve. Cost basis and realised profit retain intermediate precision and round to cents only at USD response/summary boundaries. Displayed rounded row amounts or realised/unrealised components can differ by a cent from correctly aggregated totals; totals use underlying exact amounts, not sums of rounded displays. V5–V7 preserve audit revisions and rebuild legacy cost basis using retained prices; input discarded by old cent-rounding cannot be recovered automatically. Page size is limited to 100; CSV/legacy full-history output is limited to 10,000 records. Exports neutralize formula-leading text. Ledger replay remains linear in active transaction count.
| Problem | Check |
|---|---|
| Backend cannot connect to DB | Confirm docker compose up -d is running in backend, port 5432 is free, and DB_URL, DB_USERNAME, DB_PASSWORD match Docker defaults. |
| Flyway or schema startup errors | Confirm the database is the expected local cryptodb. V2__transaction_constraints.sql is intentionally a no-op and should remain versioned. |
Port 8080 already in use | Stop the other process or run backend with SERVER_PORT=8081; then update NEXT_PUBLIC_API_BASE_URL. |
Port 3000 already in use | Run npm run dev -- --port 3001, then set APP_CORS_ALLOWED_ORIGINS=http://localhost:3001 for the backend. |
| Registration/login fails with JWT key errors | Use a JWT_SECRET at least 32 bytes long. Very short local secrets fail JJWT HMAC validation. |
| Prices are missing or zero | Check CoinGecko availability, rate limits, and whether the symbol exists in /api/market/supported-coins. Unsupported symbols are intentionally marked unavailable. |
| Frontend cannot reach backend | Confirm NEXT_PUBLIC_API_BASE_URL=http://localhost:8080/api, backend is running, and browser requests are not blocked by CORS. |
| E2E tests cannot launch browser | Run cd frontend && npx playwright install chromium. |
/api/market/supported-coins.V2__transaction_constraints.sql is intentionally a no-op placeholder to keep existing Flyway version history stable./Users/kevinasramoska/Desktop/crypto-portfolio-app before committing.300000 |
| Scheduled snapshot startup delay |
90d{ "range": "30d", "history": PerformancePoint[] } |
POST | /api/transactions | Yes | TransactionRequest | TransactionResponse |
PUT | /api/transactions/{id} | Yes | TransactionRequest | Replacement TransactionResponse |
DELETE | /api/transactions/{id} | Yes | None | 204 No Content |
GET | /api/transactions | Yes | None | TransactionResponse[] |
GET | /api/transactions/paginated?page=0&size=20 | Yes | Query page, size | PaginatedTransactionsResponse |
GET | /api/transactions/summary | Yes | None | { "totalBuyVolumeUsd": number, "totalSellVolumeUsd": number, "totalRealisedProfitUsd": number } |