This directory contains SQL migration files for the Expense Tracker database schema.
Migrations follow the pattern: {version} {description}.sql and {version} {description} - drop.sql
version: Sequential number (001, 002, etc.)description: Brief description of what the migration does.sql: Forward migration (apply changes)- drop.sql: Rollback migration (revert changes)
| # | Migration | Description | Status |
|---|---|---|---|
| 001 | create families table |
Root entity for multi-tenant isolation | ✅ |
| 002 | create users table |
User authentication with bcrypt | ✅ |
| 003 | create accounts table |
Financial accounts (cash/checking/savings) | ✅ |
| 004 | create categories table |
Hierarchical income/expense categories | ✅ |
| 005 | create transactions table |
Core transactions with auto-balance trigger | ✅ |
| 006 | create exchange rates table |
Multi-currency support with rates | ✅ |
| 007 | create audit log table |
Comprehensive audit trail with JSONB | ✅ |
| # | Migration | Description | Status |
|---|---|---|---|
| 009 | demo seed data |
Demo data for portfolio/testing | ✅ |
Demo Credentials:
- Email:
demo@example.com - Password:
Demo123!
001 create families table.sql
002 create users table.sql
003 create accounts table.sql
004 create categories table.sql
005 create transactions table.sql
006 create exchange rates table.sql
007 create audit log table.sql009 demo seed data.sql007 create audit log table - drop.sql
006 create exchange rates table - drop.sql
# ... and so on-- List all tables:
\dt
-- Describe specific table:
\d families
\d transactions
-- List all triggers:
SELECT trigger_name, event_object_table
FROM information_schema.triggers
WHERE trigger_schema = 'public';
-- List all functions:
\df
-- List all views:
\dv- Never modify existing migrations - Create new ones instead
- Always provide rollback (drop) migrations - For easy reverting
- Test migrations on a copy of production data before applying
- Keep migrations atomic - One logical change per migration
- Use transactions - Wrap migrations in BEGIN/COMMIT blocks
- Run migrations in order - Dependencies must be respected
families (root entity)
├── users (authentication, family members)
├── accounts (financial accounts: cash, checking, savings)
├── categories (hierarchical: parent → children)
├── transactions (core financial data)
│ ├── → account_id (which account)
│ ├── → category_id (what category)
│ └── → created_by (which user)
└── audit_log (automatic via triggers)
└── logs all CUD operations
exchange_rates (shared, not family-specific)
└── historical rates for currency conversion
All data is scoped to a family_id to ensure data privacy and isolation between households.
Tables use an is_active flag for logical deletion, preserving historical data.
All CUD operations are automatically logged in audit_log via triggers with before/after JSONB snapshots.
Account balances are automatically calculated via update_account_balance() trigger:
current_balance = initial_balance + SUM(income) - SUM(expense)
- Transactions store both original currency and base currency (RSD)
- Historical exchange rates with daily updates
- Helper function
get_exchange_rate(from, to, date)with fallback
- 40+ indexes for query performance
- DECIMAL(15,2) for money (precise, no rounding errors)
- DECIMAL(15,6) for exchange rates (higher precision)
- JSONB for flexible audit snapshots
- GIN indexes on JSONB for fast searching
| Table | Rows (seed) | Purpose |
|---|---|---|
families |
1 | Root entity, multi-tenant isolation |
users |
2 | Authentication, family members |
accounts |
4 | Financial accounts (cash, bank, savings) |
categories |
19 | Hierarchical expense/income categories |
transactions |
~20 | Core financial transactions |
exchange_rates |
14 | Currency rates (7 days × 2 directions) |
audit_log |
40+ | Automatic audit trail |
| Trigger | Table | Purpose |
|---|---|---|
trigger_*_updated_at |
5 tables | Auto-update updated_at timestamp |
trigger_transactions_update_balance |
transactions | Auto-recalculate account balance |
trigger_audit_* |
4 tables | Auto-log all changes to audit_log |
| Function | Purpose |
|---|---|
update_updated_at_column() |
Update timestamp on record change |
update_account_balance() |
Recalculate account balance based on transactions |
get_exchange_rate(from, to, date) |
Get exchange rate with fallback |
audit_trigger() |
Log changes to audit_log |
| View | Purpose |
|---|---|
v_recent_audit_log |
Audit log with joined family/user names |
Required in .env:
DB_HOST=your_db_host
DB_PORT=5432
DB_USER=your_db_user
DB_PASSWORD=your_db_password
DB_NAME=expense_tracker_dev
DB_SSLMODE=disable-- Check what exists:
\dt
-- Drop specific table if needed:
DROP TABLE tablename CASCADE;-- Check trigger exists:
SELECT trigger_name FROM information_schema.triggers
WHERE event_object_table = 'transactions';-- Recreate view manually if needed
-- See 007 create audit log table.sql for view definitionFor detailed documentation on each migration, see ../docs/:
- Step-by-step migration guides
- Seed data documentation
- Cheatsheets for quick reference
Status: Schema complete and production-ready! 🎉