C3: Budget allocation now verifies project belongs to company M4: Expense approve/reject scoped by company via project join H2: OIDC cookies get secure flag on HTTPS H3: OIDC auto-link only when email_verified by provider H4: Content-Security-Policy + X-Content-Type-Options in hooks M7: SSRF favicon redirect depth capped at 3 M2: File downloads use attachment disposition (not inline) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Buildfor Life Budget
A self-hosted, multi-company budget and project tracking tool built for internal use at Buildfor Life (B4L). Designed as a replacement for Actual Budget, with support for multiple companies, role-based access, project-level budget allocation, and an expense approval workflow.
Features
- Multi-company — manage multiple companies from a single instance; each with its own members, budget, and data
- Role-based access control — per-company roles (admin, manager, user, viewer) with fine-grained permissions
- Project budget allocation — allocate funds from the company pool to individual projects
- Expense submission and approval — users submit expenses; managers/admins approve or reject
- Categories and tags — organise expenses with categories and free-form tags
- Reports — spending by category, spending by project, monthly trend, and budget vs actual
- CSV import — migrate historical data from Actual Budget via CSV upload
- Audit log — full company-scoped changelog of all significant actions
- Company and user management — soft-delete companies, disable or permanently delete users (system admin only)
- Local authentication — email + password with Argon2 password hashing
- OIDC SSO — optional generic OIDC provider (Keycloak, Authentik, etc.) alongside local auth
- Light / dark mode — toggle persisted to
localStorage
Tech Stack
- Framework: SvelteKit 5 with
adapter-node - Database: PostgreSQL 14+ via
pgdriver - ORM / migrations: Drizzle ORM + Drizzle Kit
- Styling: Tailwind CSS v4
- Auth:
@node-rs/argon2(password hashing),@oslojs/crypto(session tokens), generic OIDC - Charts: Chart.js
- CSV parsing: PapaParse
Prerequisites
- Node.js 20 or later
- PostgreSQL 14 or later
- A dedicated database user with full privileges on the
publicschema
Setup
1. Clone the repository
git clone https://git.b4l.co.th/B4L/buildfor_life_budget.git
cd buildfor_life_budget
2. Install dependencies
npm install
3. Create the database
CREATE USER budget_app WITH PASSWORD 'your_password';
CREATE DATABASE buildfor_life_budget OWNER budget_app;
\c buildfor_life_budget
GRANT ALL ON SCHEMA public TO budget_app;
4. Configure environment variables
cp .env.example .env
Edit .env and fill in the required values (see Environment Variables below).
5. Push the database schema
npm run db:push
Running the Dev Server
npm run dev
The app is available at http://localhost:5173.
First-Time Admin Bootstrap
The first user to register receives a regular user account with no company access. To grant system-admin privileges, run the following SQL against the database:
UPDATE users
SET is_system_admin = true
WHERE email = 'you@example.com';
Only system admins can create companies. All other users must be invited to a company by an admin or manager.
Available Scripts
| Script | Description |
|---|---|
npm run dev |
Start the Vite development server (port 5173) |
npm run build |
Compile for production into ./build/ |
npm run preview |
Preview the production build locally |
npm run check |
Run svelte-check type checking |
npm run db:push |
Push schema changes directly to the database (dev) |
npm run db:generate |
Generate a new Drizzle migration file |
npm run db:migrate |
Apply pending migrations |
npm run db:studio |
Open Drizzle Studio in the browser |
Building for Production
npm run build
node build
The build output lives in ./build/. The app is started with node build and reads configuration from environment variables or .env.
Environment Variables
| Variable | Required | Description |
|---|---|---|
PORT |
No | Port the Node server listens on (default: 3000) |
HOST |
No | Bind address (default: 127.0.0.1) |
ORIGIN |
Yes | Public-facing URL — used by SvelteKit for CSRF protection (e.g. https://budget.b4l.co.th) |
DATABASE_URL |
Yes | PostgreSQL connection string, e.g. postgresql://user:pass@localhost:5432/dbname |
OIDC_ISSUER_URL |
No | OIDC provider issuer URL. Leave blank to disable SSO. |
OIDC_CLIENT_ID |
No | OIDC client ID |
OIDC_CLIENT_SECRET |
No | OIDC client secret |
OIDC_REDIRECT_URI |
No | Callback URL registered with the OIDC provider (e.g. https://budget.b4l.co.th/oidc/callback) |
Deployment
Ready-to-use deployment files are in the deploy/ directory:
| File | Purpose |
|---|---|
buildfor-life-budget.service |
systemd unit file — runs node build as a hardened service under the budget-app user |
nginx.conf |
nginx reverse proxy — TLS termination, HTTP→HTTPS redirect, static asset caching, WebSocket upgrade headers |
setup.sh |
Server provisioning helper script |
The app is served at https://budget.b4l.co.th behind nginx, which proxies to the Node process on 127.0.0.1:3000.
Project Structure
buildfor_life_budget/
├── src/
│ ├── app.html # HTML shell
│ ├── app.css # Global styles (Tailwind entry point)
│ ├── hooks.server.ts # Session validation, auth middleware
│ ├── lib/
│ │ ├── components/ # Shared Svelte components
│ │ ├── server/
│ │ │ ├── auth/ # Session management, OIDC helpers
│ │ │ ├── db/
│ │ │ │ ├── schema.ts # Drizzle table definitions
│ │ │ │ └── index.ts # Database client
│ │ │ ├── audit.ts # Audit log helpers
│ │ │ └── authorization.ts # Role/permission checks
│ │ ├── stores/ # Svelte stores (e.g. theme)
│ │ ├── types/ # Shared TypeScript types
│ │ └── utils/ # Misc utility functions
│ └── routes/
│ ├── (auth)/ # Login, register, OIDC callback
│ └── (app)/ # Authenticated area
│ ├── dashboard/ # Home dashboard
│ ├── companies/
│ │ └── [companyId]/
│ │ ├── projects/ # Project list and detail
│ │ ├── expenses/ # Expense submission and approval
│ │ ├── budget/ # Budget allocation
│ │ ├── categories/ # Category management
│ │ ├── reports/ # Spending reports and charts
│ │ ├── import/ # CSV import (Actual Budget migration)
│ │ └── settings/ # Company settings
│ └── admin/ # System admin panel (users, companies)
├── deploy/ # systemd unit, nginx config, setup script
├── drizzle.config.ts # Drizzle Kit configuration
├── svelte.config.js # SvelteKit / adapter-node config
├── vite.config.ts # Vite / Tailwind plugin config
└── .env.example # Environment variable template
User Roles
Roles are assigned per company. A user can have different roles in different companies.
| Role | Description |
|---|---|
| Admin | Full company control — manage members, roles, categories, settings, and approve or reject any expense |
| Manager | Manage projects and budgets, approve or reject expenses submitted by users |
| User | Submit and manage their own expenses, view project budgets |
| Viewer | Read-only access to company budgets, projects, and reports |
System admins (set via SQL) can create and archive companies and disable or permanently delete any user account, regardless of company membership.
License
Private / internal — all rights reserved.