grabowski 722bd6c1c2 Add architecture diagrams to README
Three Mermaid diagrams styled for beautiful-mermaid (tokyo-night):
system overview (flowchart), data model (ER diagram), and expense
approval flow (sequence diagram).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 10:57:26 +07:00
2026-04-14 10:57:26 +07:00

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 and are designed to render cleanly through beautiful-mermaid (tokyo-night theme).

System Overview

%%{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

%%{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

%%{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

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.

S
Description
No description provided
Readme 1.1 MiB
Languages
Svelte 49.9%
TypeScript 49.8%
Shell 0.1%