SteenBudgetSolution
A website inprogress for personal budgeting
Install / Use
/learn @lsteen89/SteenBudgetSolutionREADME
SteenBudgetSolution
SteenBudgetSolution is a comprehensive, full-stack personal finance management application designed to empower users to track income, expenses, and manage budgets effectively.
This project showcases the ability to design, build, deploy, and manage a complete application end-to-end, with a strong emphasis on secure development practices, modern technologies, robust infrastructure, and efficient CI/CD workflows, all self-hosted on a Raspberry Pi home lab.
Project Docs
Quick links:
- Security
- API
- WebSockets
- Roadmap
Architecture Overview
This project uses a modern, separated architecture to ensure security, stability, and maintainability.
Pi 4 (Production Host)
- Role: The dedicated, hardened server that runs the live application stack.
- Services: Runs the entire stack within Docker Compose: MariaDB, the .NET 8 backend API, and a Caddy web server.
- Security:
- Exposes only HTTP/HTTPS (80/443) to the web.
- SSH (port 2222) is only accessible from the Pi 3 runner's local IP address. All other SSH traffic is blocked by
ufw. - SSH is configured for key-only authentication; password login is disabled.
- Secrets: All application secrets (database credentials, JWT keys, etc.) are managed in a
.envfile in the project directory, which is excluded from Git.
Pi 3 (CI/CD Runner & Deploy Orchestrator)
- Role: A trusted, internal orchestrator that receives deployment jobs from GitHub and executes them securely.
- Services: Runs a single containerized GitHub Actions self-hosted runner.
- Security:
- Requires zero inbound ports from the internet. It only makes outbound connections to GitHub.
- It communicates with the Pi 4 over the local LAN using a dedicated, forced-command SSH key.
- Its sole purpose is to execute the deployment steps defined in the CI/CD workflow.
GitHub (Cloud Build Environment)
- Role: Acts as a powerful, disposable build factory. It handles all CPU-intensive compilation and packaging.
- Actions:
- On a push to
master, it builds a multi-architecture (linux/amd64,linux/arm64) backend Docker image and pushes it to the GitHub Container Registry (GHCR). - It builds the production-optimized React frontend (
distbundle) and uploads it as a workflow artifact. - It then triggers the
deployjob, which is picked up by the self-hosted runner on the Pi 3.
- On a push to
Cloudflare (DNS & TLS Helper)
- Role: Manages the
ebudget.seDNS records. - Actions: Used by Caddy to perform the ACME DNS-01 challenge. Caddy uses a scoped API token to create and delete temporary TXT records to prove domain ownership for issuing and renewing Let's Encrypt TLS certificates.
Key Features
- 🔐 Secure User Authentication: Robust JWT-based authentication featuring auto-refresh, periodic status checks, and WebSocket integration for immediate session termination.
- 🤖 ReCAPTCHA Integration: Protects user registration from bots using Google reCAPTCHA v3.
- 💰 Full CRUD Operations: Manage budgets, income, and expense transactions with complete Create, Read, Update, and Delete functionality.
- 📧 Email Notifications: Integrated SMTP client (using MailKit) for user email verification and essential notifications.
- 📱 Responsive Design: Modern, mobile-first UI built with Tailwind CSS ensures a great experience on any device.
- 🚀 Real-time Communication: Employs WebSockets for immediate server-driven events (like session termination) and uses a ping/pong mechanism to maintain connection health.
- 🛡️ Hardened Security: Multi-layered security approach including infrastructure hardening and application-level protections.
Tech Stack
Backend:
- Framework: .NET 8 (C#) with ASP.NET Core Web API
- Database: MariaDB (SQL-based relational database)
- Data Access: Dapper (Micro-ORM, chosen for performance and direct SQL control)
- Architecture: Clean Architecture principles for separation of concerns and testability.
- Real-time: ASP.NET Core WebSockets
- Email: MailKit
Frontend:
- Framework: React (TypeScript) for a robust and type-safe UI.
- Build Tool: Vite for fast development server and optimized builds.
- Styling: Tailwind CSS (Utility-first CSS framework).
- API Communication: Axios (with interceptor for token refresh)
Infrastructure & DevOps:
- Host: Self-hosted on Raspberry Pi 4 (Linux OS)
- Orchestrator: Raspberry Pi 3 (Linux OS)
- Containerization: Docker & Docker Compose
- Web Server / Reverse Proxy: Caddy (with automatic HTTPS)
- Security Tools: UFW (Firewall), Fail2Ban (Intrusion Prevention)
- CI/CD: GitHub Actions (Automated build, test, and deployment pipeline)
- Secrets Management: GitHub Actions Secrets &
.envfile on host. - Domain & Network: Custom Domain, DNS Management via Cloudflare
🚀 Getting Started (Local Development)
Recommended dev mode: MariaDB in Docker; Backend & Frontend run natively (hot-reload). Prod:
.env(only). Dev: backend uses user-secrets, frontend uses.env.local.
0) Prereqs (Mac/Linux)
- Docker Desktop (or Colima) running
- .NET 8 SDK
- Node 18+ / 20+
1) Dev database (Docker MariaDB)
Create a local file (DO NOT COMMIT): ./.env.dev
MARIADB_ROOT_PASSWORD=devrootpassword
MARIADB_DATABASE=steenbudgetDEV
MARIADB_USER=app
MARIADB_PASSWORD=apppwd
Note:
.envfiles do not expand${VARS}. Write literal values.
Start DB (run from repo root):
docker compose --env-file .env.dev -f docker-compose.dev.yml up -d db
docker compose --env-file .env.dev -f docker-compose.dev.yml ps
2) Backend setup (user-secrets)
cd Backend
dotnet user-secrets init
# DB (host → container via 127.0.0.1)
dotnet user-secrets set "DatabaseSettings:ConnectionString" \
"Server=127.0.0.1;Port=3306;Database=steenbudgetDEV;Uid=app;Pwd=apppwd;SslMode=None;GuidFormat=Binary16"
# JWT (dev)
dotnet user-secrets set "JwtSettings:SecretKey" "base64:REPLACE_ME"
dotnet user-secrets set "Jwt:Keys:dev" "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="
dotnet user-secrets set "Jwt:ActiveKid" "dev"
# WebSocket auth
dotnet user-secrets set "WEBSOCKET_SECRET" "dev-ws-secret"
# Turnstile (dev/test keys)
dotnet user-secrets set "Turnstile:VerifyUrl" "https://challenges.cloudflare.com/turnstile/v0/siteverify"
dotnet user-secrets set "Turnstile:SecretKey" "1x0000000000000000000000000000000AA"
dotnet user-secrets set "Turnstile:Enabled" "true"
Generate a JWT key:
openssl rand -base64 32
# then set JwtSettings:SecretKey = base64:<value>
Run backend (hot reload):
cd Backend
DOTNET_USE_POLLING_FILE_WATCHER=true dotnet watch run --urls http://localhost:5001
3) Frontend setup (Vite)
Create: Frontend/.env.local
VITE_APP_API_URL=http://localhost:5001
VITE_TURNSTILE_SITE_KEY=XYZ
VITE_USE_MOCK=false
Run frontend:
cd Frontend
npm install
npm run dev
If you edit
.env.local, restart Vite.
🧪 Dev seeding (users)
A small CLI (Backend.Tools) can seed a user in dev.
- Seeding is guarded by
ALLOW_SEEDING=true - TURNSTILE is bypassed for seeding
Option A: Seed via Docker (recommended)
Run from repo root:
docker compose --env-file .env.dev -f docker-compose.dev.yml --profile seed run --rm seed-users
Override values:
docker compose --env-file .env.dev -f docker-compose.dev.yml --profile seed run --rm \
-e SEED_EMAIL=jane@doe.se -e SEED_PASSWORD=ChangeMe123 \
-e SEED_FIRST=Jane -e SEED_LAST=Doe \
seed-users
Important: the seeder must use a Docker-internal connection string (Server=db;...).
If needed, set it in seed-users.environment:
DATABASESETTINGS__CONNECTIONSTRING: "Server=db;Port=3306;Database=steenbudgetDEV;Uid=app;Pwd=apppwd;GuidFormat=Binary16"
ALLOW_SEEDING: "true"
"
Option B: Seed locally (host)
Set connection string via env (or user-secrets):
cd Backend.Tools
ALLOW_SEEDING=true DATABASESETTINGS__CONNECTIONSTRING="Server=127.0.0.1;Port=3306;Database=steenbudgetDEV;Uid=app;Pwd=apppwd;SslMode=None;GuidFormat=Binary16" \
dotnet run -- --email jane@doe.se --password 'ChangeMe123' --first Jane --last Doe
🧰 DB access (GUI)
Recommended: TablePlus
- Host:
127.0.0.1 - Port:
3306 - User:
app - Password:
apppwd - Database:
steenbudgetDEV - SSL: off
Root login from the host is typically blocked in the MariaDB image. Use
app.
Requirements
- The seeder container uses a Docker-internal connection string (
Server=db;...) already set indocker-compose.dev.yml. - Service name:
seed-users.
High-Level System Flowchart
graph TD
%% == 1. Define ALL Nodes First ==
%% User Flow & Services
UserBrowser["User's Browser"]
Cloudflare["Cloudflare DNS"]
%% Production Host (Pi 4)
Pi4["Raspberry Pi 4 - Prod Host"]
UFW(UFW Firewall)
Caddy["Caddy Reverse Proxy"]
BackendAPI[".NET 8 Backend API"]
MariaDB[(MariaDB Database)]
%% Runner Host (Pi 3)
Pi3["Raspberry Pi 3 - CI/CD Host"]
Runner("Self-Hosted<br/>GitHub Runner")
%% Cloud & Git
Developer[Developer]
GitHubRepo(GitHub Repo)
GitHubActions["GitHub Actions"]
GHCR(GHCR - Container Regi
Related Skills
node-connect
349.7kDiagnose OpenClaw node connection and pairing failures for Android, iOS, and macOS companion apps
frontend-design
109.7kCreate distinctive, production-grade frontend interfaces with high design quality. Use this skill when the user asks to build web components, pages, or applications. Generates creative, polished code that avoids generic AI aesthetics.
openai-whisper-api
349.7kTranscribe audio via OpenAI Audio Transcriptions API (Whisper).
qqbot-media
349.7kQQBot 富媒体收发能力。使用 <qqmedia> 标签,系统根据文件扩展名自动识别类型(图片/语音/视频/文件)。
