BorsVy is a SaaS-style market intelligence app for stock and crypto research. It has a React/Vite frontend, a Spring Boot backend, GitHub Actions verification, and automated deployment to a Hetzner VPS behind Nginx and HTTPS.
Live site: https://borsvy.abenezeranglo.uk/
- Market dashboard with clickable overview cards.
- Popular instruments table for quick research entry.
- Watchlist/favorites with mobile-specific card layout.
- Stock and crypto analysis pages.
- Lazy-loaded AI analysis and news panels.
- Portfolio holdings stored through the backend with local fallback behavior.
- Price alerts stored through the backend with local fallback behavior.
- Authentication endpoints for email/password and Google sign-in support.
- Light and dark theme with mobile e2e coverage.
- Responsive layout tested on desktop and mobile Chrome.
- Automated GitHub Actions deploy to Hetzner.
Frontend:
- React 18
- Vite
- Ant Design
- Tailwind CSS
- React Router
- Chart.js, Recharts, lightweight-charts
- Playwright and Vitest
Backend:
- Java 17
- Spring Boot 3.2.3
- Spring Web, Security, Data JPA, Validation, Actuator
- H2 by default for the single-VPS deployment
- Optional PostgreSQL via production environment overrides
- Flyway migrations are present but disabled by default in the current single-VPS H2 setup
External data and AI providers:
- Finnhub for stock quotes/company data
- Twelve Data for historical data and crypto quotes when configured
- CoinGecko fallback for crypto quotes when Twelve Data is not configured
- NewsData for news when configured
- RapidAPI Yahoo Finance fallback for news
- Groq for AI analysis when configured
Desktop dashboard:
Desktop analysis workspace:
Mobile dashboard:
Mobile portfolio:
Dark-mode dashboard:
This project was built as a production-style full-stack portfolio project rather than a regulated investment product. The goal was to demonstrate end-to-end product engineering: a usable React SaaS interface, a Java backend, third-party API integration, mobile quality checks, CI/CD, and a real VPS deployment.
Key engineering decisions:
- Same-origin production API: The frontend calls
/api/...in production and Nginx proxies requests to Spring Boot. This avoids browser CORS complexity in production. - Lazy analysis loading: Expensive AI and news requests are loaded only when the user opens those tabs, reducing initial dashboard latency and API cost.
- Provider fallbacks: Crypto quotes can fall back to CoinGecko when Twelve Data is not configured. News can fall back to RapidAPI when NewsData is unavailable.
- Mobile-first verification: Playwright runs both desktop and mobile projects, including route overflow checks, mobile table clipping checks, and theme-toggle checks.
- Simple VPS deployment: GitHub Actions builds/tests the app, uploads frontend and backend artifacts to Hetzner, restarts systemd, and reloads Nginx.
Important product boundary:
- BorsVy is framed as market information and company/news briefing software. It is not intended to provide investment, financial, tax, or legal advice.
flowchart TD
User[Browser] --> Cloudflare[Cloudflare / DNS]
Cloudflare --> Nginx[Nginx on Hetzner]
Nginx --> Frontend[React static files<br/>/var/www/borsvy-frontend]
Nginx -->|/api/*| Backend[Spring Boot<br/>borsvy.service :8080]
Backend --> DB[(H2 file DB<br/>/var/lib/borsvy/borsvy)]
Backend --> Finnhub[Finnhub]
Backend --> TwelveData[Twelve Data]
Backend --> CoinGecko[CoinGecko fallback]
Backend --> NewsData[NewsData]
Backend --> RapidAPI[RapidAPI Yahoo Finance]
Backend --> Groq[Groq AI]
GitHub[GitHub Actions] -->|build/test/deploy| Hetzner[Hetzner VPS]
Hetzner --> Nginx
Hetzner --> Backend
The diagram reflects the current single-VPS production setup. The app can be moved to PostgreSQL later, but production currently defaults to file-based H2 unless environment variables override it.
.
|-- .github/workflows/deploy-hetzner.yml
|-- backend
| |-- .env.example
| |-- pom.xml
| |-- mvnw / mvnw.cmd
| `-- src/main
| |-- java/com/borsvy
| | |-- client
| | |-- controller
| | |-- model
| | |-- repository
| | |-- security
| | `-- service
| `-- resources
| |-- application.properties
| |-- application-prod.properties
| `-- db/migration
|-- frontend
| |-- .env.example
| |-- package.json
| |-- playwright.config.js
| |-- src
| `-- tests/e2e
|-- screenshots
`-- README.md
Requirements:
- Node.js 18
- Java 17
- Maven wrapper from the repo
Start the backend:
cd backend
cp .env.example .env
./mvnw spring-boot:runOn Windows PowerShell:
cd backend
Copy-Item .env.example .env
.\mvnw.cmd spring-boot:runStart the frontend:
cd frontend
npm ci
cp .env.example .env.local
npm run devFrontend dev server:
http://localhost:3001
Backend API:
http://localhost:8080
The frontend uses http://localhost:8080 as the API base URL in dev. In production it uses same-origin /api/... behind Nginx.
The backend can boot with safe defaults, but real market/news/AI functionality depends on API keys.
Common backend variables:
FINNHUB_API_KEY
TWELVEDATA_API_KEY
TWELVEDATA_API_URL
NEWSDATA_API_KEY
NEWSDATA_API_URL
RAPIDAPI_API_KEY
RAPIDAPI_API_HOST
GROQ_API_KEY
JWT_SECRET
MAIL_HOST
MAIL_PORT
MAIL_USERNAME
MAIL_PASSWORD
STRIPE_SECRET_KEY
STRIPE_WEBHOOK_SECRET
STRIPE_PRO_PRICE_ID
ALLOWED_ORIGINS
Database overrides for PostgreSQL:
DATABASE_URL
DB_DRIVER
DB_DIALECT
DB_USERNAME
DB_PASSWORD
JPA_DDL_AUTO
FLYWAY_ENABLED
Current production defaults in application-prod.properties use file-based H2:
jdbc:h2:file:/var/lib/borsvy/borsvy
That is acceptable for the current small single-server deployment. For heavier use, move to PostgreSQL and enable migrations deliberately.
Frontend production variable:
VITE_API_URL
For the Hetzner/Nginx deployment this is usually left empty so the frontend calls same-origin /api/....
Main routes include:
GET /api/health
GET /api/stocks/market-overview
GET /api/stocks/{symbol}
GET /api/stocks/search?query=...
GET /api/analysis/{symbol}/price-history
GET /api/analysis/{symbol}/ai
GET /api/analysis/{symbol}/news?limit=5
GET /api/favorites
POST /api/favorites
DELETE /api/favorites/{symbol}
GET /api/portfolio/holdings
POST /api/portfolio/holdings
DELETE /api/portfolio/holdings/{id}
GET /api/alerts
POST /api/alerts
PATCH /api/alerts/{id}/toggle
PATCH /api/alerts/{id}/triggered
DELETE /api/alerts/{id}
GET /api/auth/me
POST /api/auth/login
POST /api/auth/register
POST /api/auth/google
POST /api/auth/logout
GET /api/auth/me returns 401 when the visitor is logged out. That is expected behavior.
GET /api/health returns a lightweight status payload for deployment checks and uptime monitoring.
Frontend unit tests:
cd frontend
npm testPlaywright e2e tests:
cd frontend
npm run test:e2eThe Playwright suite runs:
- Desktop Chrome journey tests.
- Mobile Chrome journey tests.
- Mobile no-horizontal-overflow checks.
- Mobile dashboard table clipping checks.
- Mobile theme toggle checks.
Frontend production build:
cd frontend
npm run buildBackend compile:
cd backend
./mvnw -q -DskipTests compileBackend package:
cd backend
./mvnw clean package -DskipTestsProduction is currently deployed to a Hetzner VPS:
Frontend static root: /var/www/borsvy-frontend
Backend jar path: /root/backend-0.0.1-SNAPSHOT.jar
Backend service: borsvy.service
Backend port: 8080
Nginx API proxy: /api/ -> http://localhost:8080/api/
Nginx serves the frontend and proxies API requests to the Spring Boot service.
Operational work covered by this project includes:
- GitHub Actions deployment to a single Hetzner VPS.
- systemd-managed Spring Boot backend runtime.
- Nginx static frontend hosting and API reverse proxying.
- Production secret rotation workflow for third-party API credentials.
- Health-check based backend restart verification.
- A documented secret-rotation runbook for repeatable production maintenance.
Useful server commands:
systemctl status borsvy.service --no-pager
journalctl -u borsvy.service -n 120 --no-pager
nginx -t
systemctl reload nginxProduction backend secrets are currently loaded from:
/root/application-prod.properties
The systemd service starts the backend through:
/root/start-borsvy.sh
and passes:
--spring.config.additional-location=file:/root/application-prod.properties
When an API key or auth secret is exposed, treat it as compromised even if it has since been removed from the current branch. Git history, build logs, screenshots, or copied terminal output may still contain the old value.
Rotate the affected provider keys in the provider dashboards first, then update the Hetzner file:
sudo nano /root/application-prod.propertiesExpected production properties include:
finnhub.api.key=...
serpapi.api.key=...
polygon.api.key=...
rapidapi.api.key=...
rapidapi.api.host=finance-news22.p.rapidapi.com
groq.api.key=...
groq.model.id=llama-3.3-70b-versatile
allowed.origins=*
jwt.secret=...Generate a new JWT signing secret when rotating auth:
openssl rand -base64 48jwt.secret must be a single line. If the value wraps onto a second standalone line, delete both JWT lines and paste a freshly generated value as one line:
jwt.secret=single-line-secret-valueIn nano:
Ctrl+O save
Enter confirm filename
Ctrl+X exit
Restart and verify:
sudo systemctl restart borsvy.service
sudo systemctl status borsvy.service --no-pager
curl -fsS http://127.0.0.1:8080/api/healthUse this command to confirm secret fields exist without printing values:
sudo awk -F= '
BEGIN { IGNORECASE=1 }
$1 ~ /(api\.key|secret|password|token)/ { print $1"=<redacted>"; next }
{ print }
' /root/application-prod.propertiesThe output should include jwt.secret=<redacted> and should not include any random-looking standalone line after it.
After production is healthy, revoke or delete the old exposed keys in each provider dashboard. For this project, check:
Finnhub
SerpAPI
Polygon
RapidAPI
Groq
The deploy workflow is .github/workflows/deploy-hetzner.yml.
It runs on every push to main and can also be started manually from GitHub Actions.
Verify job:
- Checks out the repo.
- Installs frontend dependencies.
- Installs Playwright Chromium.
- Builds the frontend.
- Runs Playwright e2e tests.
- Uploads the frontend
distartifact. - Sets up Java 17.
- Packages the backend jar.
- Uploads the backend jar artifact.
Deploy job:
- Downloads the frontend artifact.
- Downloads the backend jar artifact.
- Uploads both artifacts to the VPS.
- Copies the jar to
/root/backend-0.0.1-SNAPSHOT.jar. - Restarts
borsvy.service. - Waits for
/api/healthbefore continuing. - Syncs frontend files to
/var/www/borsvy-frontend. - Validates and reloads Nginx.
Required GitHub repository secrets:
HETZNER_HOST
HETZNER_USER
HETZNER_SSH_KEY
HETZNER_SSH_PORT
Current values are expected to be:
HETZNER_HOST=37.27.31.97
HETZNER_USER=root
HETZNER_SSH_PORT=22
Do not commit the private SSH key. Store it only in GitHub Secrets.
Optional GitHub repository variables:
HETZNER_FRONTEND_DIR
HETZNER_BACKEND_SERVICE
Defaults:
HETZNER_FRONTEND_DIR=/var/www/borsvy-frontend
HETZNER_BACKEND_SERVICE=borsvy.service
- Cloudflare may cache old frontend assets. Purge Cloudflare cache if the deployed UI appears stale.
- The backend may return
401for/api/auth/mewhen logged out; that is normal. - Java startup takes roughly 10-15 seconds on the current VPS. The deploy workflow waits for
/api/healthafter restart. - The app currently uses H2 storage by default in production. Use PostgreSQL before expecting multi-user durability at scale.
- Some browser console messages such as
A listener indicated an asynchronous response...can come from browser extensions. Incognito testing confirmed this is not from the app.
- This is a portfolio project, not a regulated financial-advice product.
- Market data depends on third-party providers and may be delayed, unavailable, or rate-limited.
- Free/personal API plans may not be appropriate for a commercial SaaS launch.
- H2 is the default production database for this prototype; PostgreSQL is recommended for real users.
- The deploy currently restarts the backend in place, so brief API downtime can happen during deployment.
- AI summaries should be treated as informational summaries, not trading recommendations.
This is suitable for a small SaaS prototype on a single VPS. Before serious production usage:
- Move from H2 to PostgreSQL.
- Put backend secrets in a systemd environment file or a secrets manager.
- Reduce restart downtime further with blue/green or rolling deploys.
- Add caching/rate limiting around external market data APIs.
- Add structured logging and monitoring.
- Add database backups.
- Avoid root SSH deployment long term; use a restricted deploy user.
- Split frontend and backend deployment steps if deploy downtime becomes visible.
MIT




