A modern, full-stack expense management application built with Bun and the BHVR (Behavior) pattern. This project demonstrates clean architecture, state machine patterns, and modern web development practices.
- π― Behavior-Driven Design: Implements state machines for expense workflow management
- β‘ Built with Bun: Ultra-fast JavaScript runtime and toolkit
- π REST API: Complete API with CRUD operations and state transitions
- πΎ SQLite Database: Lightweight, file-based database with Bun's native SQLite support
- π¨ Modern UI: Beautiful, responsive web interface
- π Real-time Statistics: Dashboard with expense metrics
- π State Management: Draft β Pending β Approved β Paid workflow
- π₯ Multi-user Support: User roles (user, approver, admin)
- π Category Management: Organize expenses by categories
The project uses a Behavior Pattern to manage expense states and transitions:
draft β pending_approval β approved β paid β archived
β β
rejected ββββββββββββββββ
Each state transition is:
- Guarded: Validates conditions before allowing transitions
- Tracked: Maintains full history of state changes
- Effect-driven: Triggers side effects (notifications, logging, etc.)
expense-manager/
βββ src/
β βββ behaviors/ # Behavior pattern implementation
β β βββ core.ts # Core behavior machine
β β βββ expense.behavior.ts
β βββ db/ # Database layer
β β βββ database.ts # SQLite setup & helpers
β β βββ database-supabase.ts # Supabase setup & helpers
β β βββ migrate.ts # Database migrations
β β βββ seed.ts # Seed data
β βββ routes/ # API routes
β β βββ expenses.ts # Expense endpoints (SQLite)
β β βββ expenses.mariadb.ts # Expense endpoints (MariaDB)
β β βββ expenses.supabase.ts # Expense endpoints (Supabase)
β β βββ categories.ts # Category endpoints (SQLite)
β β βββ categories.mariadb.ts # Category endpoints (MariaDB)
β β βββ categories.supabase.ts # Category endpoints (Supabase)
β β βββ users.ts # User endpoints (SQLite)
β β βββ users.mariadb.ts # User endpoints (MariaDB)
β β βββ users.supabase.ts # User endpoints (Supabase)
β βββ services/ # Business logic
β β βββ expense.service.ts # Expense service (SQLite)
β β βββ expense.service.mariadb.ts # Expense service (MariaDB)
β β βββ expense.service.supabase.ts # Expense service (Supabase)
β β βββ category.service.ts # Category service (SQLite)
β β βββ category.service.mariadb.ts # Category service (MariaDB)
β β βββ category.service.supabase.ts # Category service (Supabase)
β β βββ user.service.ts # User service (SQLite)
β β βββ user.service.mariadb.ts # User service (MariaDB)
β β βββ user.service.supabase.ts # User service (Supabase)
β βββ index.ts # Application entry point (SQLite)
β βββ index.mariadb.ts # Application entry point (MariaDB)
β βββ index.supabase.ts # Application entry point (Supabase)
βββ public/
β βββ index.html # Frontend UI
βββ package.json
βββ supabase_schema.sql # Supabase database schema
βββ tsconfig.json
- Bun (latest version)
curl -fsSL https://bun.sh/install | bash
This project supports multiple database backends:
- Uses Bun's native SQLite support
- Data stored in local
expenses.dbfile - Best for development and single-user deployments
- Uses MySQL-compatible database
- Better for multi-user environments
- Configure connection in
.envfile
- Uses PostgreSQL backend with real-time capabilities
- Cloud-hosted solution with authentication and storage
- Perfect for scalable applications
- Requires Supabase account and configuration
-
Install dependencies:
bun install
-
Choose your database:
For SQLite (default):
bun run db:migrate:sqlite
For MariaDB:
bun run db:migrate
For Supabase:
- First, create a Supabase account at supabase.com
- Create a new project and copy your URL and anon key
- Set up your environment variables:
SUPABASE_URL="your_project_url" SUPABASE_ANON_KEY="your_anon_key"
- Run the database schema by copying the content from
supabase_schema.sqlfile and executing it in the Supabase SQL Editor:- Go to your Supabase dashboard
- Navigate to the "SQL Editor" tab
- Copy the content from
supabase_schema.sqland run it
- No migration command needed for Supabase
-
Seed sample data (optional):
For SQLite:
bun run db:seed:sqlite
For MariaDB:
bun run db:seed
For Supabase:
- Sample data can be added through the Supabase dashboard or API
- To migrate existing data from SQLite, you'll need the Service Role Key:
- Go to your Supabase dashboard
- Navigate to Project Settings > API
- Copy the "Service role secret" (not the anon key)
- Run the migration script:
SUPABASE_SERVICE_ROLE_KEY="your_key" bun run migrate-to-supabase.ts - After migration, you may need to adjust Row Level Security (RLS) policies:
- Go to your Supabase SQL Editor
- Run these commands to disable RLS temporarily:
ALTER TABLE public.users DISABLE ROW LEVEL SECURITY; ALTER TABLE public.categories DISABLE ROW LEVEL SECURITY; ALTER TABLE public.expenses DISABLE ROW LEVEL SECURITY; ALTER TABLE public.expense_history DISABLE ROW LEVEL SECURITY; ALTER TABLE public.receipts DISABLE ROW LEVEL SECURITY;
-
Start the development server:
For SQLite:
bun run dev:sqlite
For MariaDB:
bun run dev
For Supabase:
SUPABASE_URL="your_project_url" SUPABASE_ANON_KEY="your_anon_key" bun run dev:supabase
-
Open your browser: Navigate to
http://localhost:3000
http://localhost:3000/api
| Method | Endpoint | Description |
|---|---|---|
| GET | /expenses |
List all expenses (with filters) |
| GET | /expenses/:id |
Get expense by ID |
| POST | /expenses |
Create new expense |
| PATCH | /expenses/:id |
Update expense |
| DELETE | /expenses/:id |
Delete expense |
| POST | /expenses/:id/submit |
Submit for approval |
| POST | /expenses/:id/approve |
Approve expense |
| POST | /expenses/:id/reject |
Reject expense |
| POST | /expenses/:id/pay |
Mark as paid |
| POST | /expenses/:id/archive |
Archive expense |
| GET | /expenses/:id/history |
Get state history |
| GET | /expenses/stats/summary |
Get statistics |
| Method | Endpoint | Description |
|---|---|---|
| GET | /categories |
List all categories |
| GET | /categories/:id |
Get category by ID |
| POST | /categories |
Create new category |
| PATCH | /categories/:id |
Update category |
| DELETE | /categories/:id |
Delete category |
| Method | Endpoint | Description |
|---|---|---|
| GET | /users |
List all users |
| GET | /users/:id |
Get user by ID |
| POST | /users |
Create new user |
Create an expense:
curl -X POST http://localhost:3000/api/expenses \
-H "Content-Type: application/json" \
-d '{
"title": "Office Supplies",
"description": "Purchased printer paper",
"amount": 45.99,
"currency": "USD",
"category": "<category-id>",
"date": "2025-01-01",
"createdBy": "<user-id>"
}'Submit for approval:
curl -X POST http://localhost:3000/api/expenses/<expense-id>/submit \
-H "Content-Type: application/json" \
-d '{
"submittedBy": "<user-id>"
}'Approve expense:
curl -X POST http://localhost:3000/api/expenses/<expense-id>/approve \
-H "Content-Type: application/json" \
-d '{
"approvedBy": "<approver-id>"
}'The Behavior Pattern is a state machine implementation that provides:
type ExpenseState =
| "draft"
| "pending_approval"
| "approved"
| "rejected"
| "paid"
| "archived";interface ExpenseEvents {
SUBMIT: { submittedBy: string; timestamp: Date };
APPROVE: { approvedBy: string; timestamp: Date };
REJECT: { rejectedBy: string; reason: string; timestamp: Date };
// ...
}transitions: {
draft: {
SUBMIT: {
target: "pending_approval",
guard: (context, event) => {
return context.data.amount > 0 && context.data.title.length > 0;
}
}
}
}effects: {
pending_approval: {
onEnter: async (context) => {
// Send notification, webhook, etc.
console.log(`Expense ${context.data.id} submitted`);
}
}
}- users: User accounts with roles
- categories: Expense categories
- expenses: Main expense records
- expense_history: State transition log
- receipts: File attachments (planned)
bun run dev # Start development server with watch mode
bun run start # Start production server
bun run db:migrate # Run database migrations
bun run db:seed # Seed database with sample dataCreate a .env file:
PORT=3000
DATABASE_PATH=./expenses.db
NODE_ENV=development- Responsive Design: Works on desktop and mobile
- Real-time Updates: Automatic refresh after actions
- Filtering: Filter by state and category
- Statistics Dashboard: View expense metrics
- Modal Forms: Clean UI for creating expenses
- Action Buttons: Context-aware actions based on state
For production deployment, consider:
- Authentication & authorization
- Input validation & sanitization
- Rate limiting
- HTTPS/TLS
- Environment variable management
- Database backups
- Error handling & logging
bun run startFROM oven/bun:1
WORKDIR /app
COPY . .
RUN bun install
RUN bun run db:migrate
RUN bun run db:seed
EXPOSE 3000
CMD ["bun", "run", "start"]Contributions are welcome! Please feel free to submit a Pull Request.
MIT License - feel free to use this project for learning or production.
For questions or issues, please open an issue on the repository.
Happy Expense Tracking! π°