# Splitwise Architecture This document outlines the detailed architecture for the Splitwise-style application, focusing on the database design and backend endpoints. The application implements a double-entry bookkeeping system and supports multiple currencies. ## Database Architecture ### Core Entities and Relationships ``` [Users] 1--N [Auth] 1 | N [Contacts] | N [Users] 1--N [Accounts] N--1 [Currencies] 1 1 1 1 | | | | | | | N N | N | [Expenses] | [Transactions]-+ 1 | N N | | | | N | | | [ExpenseParticipants] | | [Settlements] | N [Notifications] ``` Entity Relationship Legend: - `1` and `N` indicate one-to-many relationships - Lines connect related entities - Entities in square brackets `[]` ### Schema Details #### Users ```sql CREATE TABLE users ( id UUID PRIMARY KEY, email VARCHAR(255) UNIQUE NOT NULL, display_name VARCHAR(255) NOT NULL, profile_photo_url TEXT, default_currency_code CHAR(3) NOT NULL DEFAULT 'USD', locale VARCHAR(10) NOT NULL DEFAULT 'en-US', timezone VARCHAR(50) NOT NULL DEFAULT 'UTC', created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(), updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW() ); ``` #### Auth (managed by better-auth) ```sql CREATE TABLE auth ( id UUID PRIMARY KEY, user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, provider VARCHAR(50) NOT NULL, provider_user_id VARCHAR(255), email VARCHAR(255) NOT NULL, password_hash TEXT, refresh_token TEXT, last_login TIMESTAMP WITH TIME ZONE, created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(), updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(), UNIQUE(provider, provider_user_id) ); ``` #### Contacts ```sql CREATE TABLE contacts ( id UUID PRIMARY KEY, user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, contact_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, status VARCHAR(20) NOT NULL CHECK (status IN ('pending', 'accepted', 'declined')), created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(), updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(), UNIQUE(user_id, contact_id) ); ``` #### Currencies ```sql CREATE TABLE currencies ( code CHAR(3) PRIMARY KEY, name VARCHAR(100) NOT NULL, symbol VARCHAR(10) NOT NULL, decimal_digits INT NOT NULL DEFAULT 2, is_active BOOLEAN NOT NULL DEFAULT TRUE ); ``` #### Exchange Rates ```sql CREATE TABLE exchange_rates ( id UUID PRIMARY KEY, from_currency_code CHAR(3) NOT NULL REFERENCES currencies(code), to_currency_code CHAR(3) NOT NULL REFERENCES currencies(code), rate DECIMAL(24, 12) NOT NULL, effective_date DATE NOT NULL, created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(), UNIQUE(from_currency_code, to_currency_code, effective_date) ); ``` #### Accounts (for double-entry bookkeeping) ```sql CREATE TABLE accounts ( id UUID PRIMARY KEY, user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, type VARCHAR(20) NOT NULL CHECK (type IN ('asset', 'liability')), currency_code CHAR(3) NOT NULL REFERENCES currencies(code), created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(), updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(), UNIQUE(user_id, currency_code) ); ``` #### Expenses ```sql CREATE TABLE expenses ( id UUID PRIMARY KEY, title VARCHAR(255) NOT NULL, description TEXT, amount DECIMAL(20, 2) NOT NULL CHECK (amount > 0), currency_code CHAR(3) NOT NULL REFERENCES currencies(code), date TIMESTAMP WITH TIME ZONE NOT NULL, payer_id UUID NOT NULL REFERENCES users(id), receipt_image_url TEXT, created_by UUID NOT NULL REFERENCES users(id), created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(), updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW() ); ``` #### Expense Participants ```sql CREATE TABLE expense_participants ( id UUID PRIMARY KEY, expense_id UUID NOT NULL REFERENCES expenses(id) ON DELETE CASCADE, user_id UUID NOT NULL REFERENCES users(id), share_amount DECIMAL(20, 2) NOT NULL CHECK (share_amount >= 0), share_percentage DECIMAL(5, 2) GENERATED ALWAYS AS ( CASE WHEN (SELECT amount FROM expenses WHERE id = expense_id) > 0 THEN (share_amount / (SELECT amount FROM expenses WHERE id = expense_id)) * 100 ELSE 0 END ) STORED, created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(), updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(), UNIQUE(expense_id, user_id) ); ``` #### Transactions (double-entry bookkeeping) ```sql CREATE TABLE transactions ( id UUID PRIMARY KEY, expense_id UUID REFERENCES expenses(id) ON DELETE SET NULL, settlement_id UUID REFERENCES settlements(id) ON DELETE SET NULL, from_account_id UUID NOT NULL REFERENCES accounts(id), to_account_id UUID NOT NULL REFERENCES accounts(id), amount DECIMAL(20, 2) NOT NULL, currency_code CHAR(3) NOT NULL REFERENCES currencies(code), exchange_rate DECIMAL(24, 12) NOT NULL DEFAULT 1.0, native_amount DECIMAL(20, 2) GENERATED ALWAYS AS (amount * exchange_rate) STORED, date TIMESTAMP WITH TIME ZONE NOT NULL, type VARCHAR(20) NOT NULL CHECK (type IN ('expense', 'settlement')), memo TEXT, created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(), updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(), CHECK (expense_id IS NOT NULL OR settlement_id IS NOT NULL), CHECK (from_account_id != to_account_id) ); ``` #### Settlements ```sql CREATE TABLE settlements ( id UUID PRIMARY KEY, payer_id UUID NOT NULL REFERENCES users(id), receiver_id UUID NOT NULL REFERENCES users(id), amount DECIMAL(20, 2) NOT NULL CHECK (amount > 0), currency_code CHAR(3) NOT NULL REFERENCES currencies(code), date TIMESTAMP WITH TIME ZONE NOT NULL, memo TEXT, created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(), updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(), CHECK (payer_id != receiver_id) ); ``` #### Balances (materialized view or calculated on-the-fly) ```sql CREATE VIEW user_balances AS SELECT u1.id AS user_id, u2.id AS other_user_id, t.currency_code, SUM( CASE WHEN t.from_account_id = a1.id THEN -t.amount WHEN t.to_account_id = a1.id THEN t.amount ELSE 0 END ) AS balance FROM users u1 CROSS JOIN users u2 JOIN accounts a1 ON u1.id = a1.user_id JOIN accounts a2 ON u2.id = a2.user_id JOIN transactions t ON (t.from_account_id = a1.id AND t.to_account_id = a2.id) OR (t.from_account_id = a2.id AND t.to_account_id = a1.id) WHERE u1.id != u2.id AND a1.currency_code = a2.currency_code AND a1.currency_code = t.currency_code GROUP BY u1.id, u2.id, t.currency_code; ``` #### Notifications ```sql CREATE TABLE notifications ( id UUID PRIMARY KEY, user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, type VARCHAR(50) NOT NULL CHECK (type IN ('expense_added', 'expense_edited', 'expense_deleted', 'settlement_received')), content JSONB NOT NULL, is_read BOOLEAN NOT NULL DEFAULT FALSE, created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW() ); ``` ### Double-Entry Bookkeeping Implementation Every financial event in the system (expenses and settlements) is recorded as a transaction with two sides: 1. A debit to one account (increasing an asset or decreasing a liability) 2. A credit to another account (decreasing an asset or increasing a liability) #### Example: Creating an Expense When User A records a $30 expense that they paid for, which is split equally between User A, User B, and User C: 1. User A paid $30 (creates a transaction) - Credit User A's account (they're owed money) +$20 - Debit User B's account (they owe money) -$10 - Debit User C's account (they owe money) -$10 2. Corresponding transactions are created: - Transaction 1: from User B's account to User A's account for $10 - Transaction 2: from User C's account to User A's account for $10 #### Example: Settling a Debt When User B settles their $10 debt to User A: 1. A settlement record is created 2. A transaction is recorded: - Debit User A's account -$10 (they received money) - Credit User B's account +$10 (they paid money) ### Multi-Currency Support The system supports multiple currencies through: 1. A currencies table with ISO currency codes 2. Exchange rates tracking between currency pairs 3. Storing all transactions in their original currency 4. Recording exchange rates used at the time of transaction 5. Converting balances to user's preferred currency on-the-fly When an expense is created in a different currency: 1. The expense is recorded in its original currency 2. Each participant's share is calculated in the original currency 3. Transactions are created in the original currency 4. Exchange rates are applied when viewing balances in a different currency ## Backend API Endpoints ### Authentication (managed by better-auth) ``` POST /api/auth/register POST /api/auth/login POST /api/auth/social/:provider POST /api/auth/social/:provider/callback POST /api/auth/logout GET /api/auth/me POST /api/auth/refresh-token POST /api/auth/forgot-password POST /api/auth/reset-password ``` ### Users and Profiles ``` GET /api/users/profile PUT /api/users/profile PUT /api/users/settings GET /api/users/search?query=:query ``` ### Contacts ``` GET /api/contacts POST /api/contacts GET /api/contacts/:id PUT /api/contacts/:id DELETE /api/contacts/:id ``` ### Currencies and Exchange Rates ``` GET /api/currencies GET /api/currencies/:code GET /api/exchange-rates?from=:code&to=:code&date=:date ``` ### Expenses ``` GET /api/expenses?page=:page&limit=:limit POST /api/expenses GET /api/expenses/:id PUT /api/expenses/:id DELETE /api/expenses/:id POST /api/expenses/:id/receipt ``` ### Expense Participants ``` GET /api/expenses/:expenseId/participants POST /api/expenses/:expenseId/participants PUT /api/expenses/:expenseId/participants/:id DELETE /api/expenses/:expenseId/participants/:id ``` ### Balances ``` GET /api/balances?currency=:code GET /api/balances/:userId?currency=:code GET /api/balances/summary?currency=:code ``` ### Settlements ``` POST /api/settlements GET /api/settlements?page=:page&limit=:limit GET /api/settlements/:id ``` ### Transactions (mostly internal) ``` GET /api/transactions?page=:page&limit=:limit GET /api/transactions/:id ``` ### Notifications ``` GET /api/notifications PUT /api/notifications/:id/read PUT /api/notifications/read-all ``` ## API Request/Response Examples ### Creating an Expense **Request:** ```json POST /api/expenses { "title": "Dinner at Restaurant", "description": "Birthday celebration", "amount": 150.00, "currency_code": "USD", "date": "2023-05-15T19:30:00Z", "payer_id": "550e8400-e29b-41d4-a716-446655440000", "participants": [ { "user_id": "550e8400-e29b-41d4-a716-446655440000", "share_amount": 50.00 }, { "user_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7", "share_amount": 50.00 }, { "user_id": "6a2f41a3-c54c-4b01-90e6-d701748f0851", "share_amount": 50.00 } ] } ``` **Response:** ```json { "id": "f47ac10b-58cc-4372-a567-0e02b2c3d479", "title": "Dinner at Restaurant", "description": "Birthday celebration", "amount": 150.0, "currency_code": "USD", "date": "2023-05-15T19:30:00Z", "payer_id": "550e8400-e29b-41d4-a716-446655440000", "participants": [ { "id": "d9d9e7b6-a5c5-4988-810f-f81b137e31b3", "user_id": "550e8400-e29b-41d4-a716-446655440000", "share_amount": 50.0, "share_percentage": 33.33 }, { "id": "8a8a8a8a-7425-40de-944b-e07fc1f90ae7", "user_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7", "share_amount": 50.0, "share_percentage": 33.33 }, { "id": "9b9b9b9b-c54c-4b01-90e6-d701748f0851", "user_id": "6a2f41a3-c54c-4b01-90e6-d701748f0851", "share_amount": 50.0, "share_percentage": 33.33 } ], "created_at": "2023-05-15T19:35:00Z", "updated_at": "2023-05-15T19:35:00Z" } ``` ### Viewing Balances **Request:** ``` GET /api/balances?currency=USD ``` **Response:** ```json { "balances": [ { "user_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7", "display_name": "John Doe", "profile_photo_url": "https://example.com/photos/john.jpg", "currency_code": "USD", "amount": -50.0, "last_activity": "2023-05-15T19:35:00Z" }, { "user_id": "6a2f41a3-c54c-4b01-90e6-d701748f0851", "display_name": "Jane Smith", "profile_photo_url": "https://example.com/photos/jane.jpg", "currency_code": "USD", "amount": -50.0, "last_activity": "2023-05-15T19:35:00Z" } ], "total_owed_to_you": 100.0, "total_you_owe": 0.0, "net_balance": 100.0 } ``` ### Creating a Settlement **Request:** ```json POST /api/settlements { "payer_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7", "receiver_id": "550e8400-e29b-41d4-a716-446655440000", "amount": 50.00, "currency_code": "USD", "date": "2023-05-16T14:00:00Z", "memo": "Paying back for dinner" } ``` **Response:** ```json { "id": "3a3a3a3a-58cc-4372-a567-0e02b2c3d479", "payer_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7", "payer_name": "John Doe", "receiver_id": "550e8400-e29b-41d4-a716-446655440000", "receiver_name": "Alice Johnson", "amount": 50.0, "currency_code": "USD", "date": "2023-05-16T14:00:00Z", "memo": "Paying back for dinner", "created_at": "2023-05-16T14:02:00Z", "updated_at": "2023-05-16T14:02:00Z" } ``` ## Implementation Considerations ### Double-Entry Bookkeeping Logic 1. **Transaction Creation**: - Every expense and settlement must create balanced transactions - System must ensure debits equal credits for all transactions 2. **Balance Calculation**: - Balances are derived from the transaction ledger - Each user's balance with another user is calculated by summing all transactions between them - Negative balance means the user owes money; positive means they are owed money ### Multi-Currency Implementation 1. **Currency Conversion**: - Use exchange rates from reliable sources (or integrate with exchange rate APIs) - Store historical exchange rates for accurate reporting - Implement conversion functions at the service layer 2. **User Preferences**: - Allow users to set default currency - Provide options to view balances in different currencies 3. **Settlement Handling**: - Support settlements in different currencies - Record the exchange rate used at settlement time - Handle currency conversion fees if applicable ### Security and Data Integrity 1. **Transaction Atomicity**: - Use database transactions to ensure all related changes succeed or fail together - Implement validation to ensure expense shares sum to the total amount 2. **Audit Trail**: - Log all financial operations - Prevent modification of completed transactions - Allow corrections through compensating transactions 3. **Data Validation**: - Validate all currency amounts and exchange rates - Ensure proper decimal precision handling - Implement comprehensive input validation ## Conclusion This architecture leverages double-entry bookkeeping principles to provide a robust foundation for tracking expenses and debts between users. The multi-currency support ensures the application can be used globally, with accurate conversion and balance tracking across different currencies. The system design prioritizes data integrity, ensuring that all financial records are accurate, balanced, and traceable. By implementing this architecture, the Splitwise application will provide users with a reliable platform for managing shared expenses across multiple currencies.