# 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 `pg` driver - **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 --- ## Architecture Diagrams use [Mermaid](https://mermaid.js.org/) and are designed to render cleanly through [beautiful-mermaid](https://github.com/lukilabs/beautiful-mermaid) (tokyo-night theme). ### System Overview ```mermaid %%{init: {'theme':'dark', 'themeVariables': {'primaryColor':'#7aa2f7','lineColor':'#3d59a1','background':'#1a1b26'}}}%% flowchart LR U[User Browser] -->|HTTPS| N[nginx reverse proxy] N -->|127.0.0.1:3000| A[SvelteKit Node app] A --> DB[(PostgreSQL)] A -.optional.-> O[OIDC Provider] subgraph Proxmox VM N A DB end ``` ### Data Model ```mermaid %%{init: {'theme':'dark', 'themeVariables': {'primaryColor':'#7aa2f7','lineColor':'#3d59a1','background':'#1a1b26'}}}%% erDiagram users ||--o{ sessions : has users ||--o{ company_members : "belongs to" companies ||--o{ company_members : has companies ||--o{ projects : contains companies ||--o{ categories : defines companies ||--o{ tags : defines companies ||--o{ budget_allocations : tracks companies ||--o{ company_log : audits projects ||--o{ expenses : contains projects ||--o{ budget_allocations : receives categories ||--o{ expenses : classifies expenses ||--o{ expense_tags : has tags ||--o{ expense_tags : tagged users ||--o{ expenses : "submits / approves" users { text id PK text email UK text password_hash text oidc_subject bool is_system_admin timestamp disabled_at } companies { uuid id PK text name numeric total_budget text currency timestamp deleted_at } company_members { uuid id PK text user_id FK uuid company_id FK enum role "admin|manager|user|viewer" } projects { uuid id PK uuid company_id FK numeric allocated_budget } expenses { uuid id PK uuid project_id FK numeric amount enum status "pending|approved|rejected" } ``` ### Expense Approval Flow ```mermaid %%{init: {'theme':'dark', 'themeVariables': {'primaryColor':'#7aa2f7','lineColor':'#3d59a1','background':'#1a1b26'}}}%% sequenceDiagram actor U as User actor M as Manager participant A as SvelteKit participant DB as PostgreSQL participant L as Audit Log U->>A: POST /projects/:id/expenses/new A->>DB: INSERT expense (status=pending) A->>L: log expense_submitted A-->>U: 302 redirect Note over M: Reviews pending queue M->>A: POST /expenses/:id approve A->>DB: UPDATE status=approved A->>L: log expense_approved A-->>M: success ``` --- ## Prerequisites - Node.js 20 or later - PostgreSQL 14 or later - A dedicated database user with full privileges on the `public` schema --- ## Setup ### 1. Clone the repository ```bash git clone https://git.b4l.co.th/B4L/buildfor_life_budget.git cd buildfor_life_budget ``` ### 2. Install dependencies ```bash npm install ``` ### 3. Create the database ```sql 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 ```bash cp .env.example .env ``` Edit `.env` and fill in the required values (see [Environment Variables](#environment-variables) below). ### 5. Push the database schema ```bash npm run db:push ``` --- ## Running the Dev Server ```bash npm run dev ``` The app is available at [http://localhost:5173](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: ```sql 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 ```bash 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.