Last active
January 4, 2026 17:54
-
-
Save isocroft/9d51dd71c838e7a9e63058e26ead1efe to your computer and use it in GitHub Desktop.
A database schema for a (ChipperCash-like) fin-tech multi-currency wallet app with types, accounts and transactions using either MySQL, SQLite or PostgreSQL as primary database
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| -- MySQL v8.0.16 | |
| -- PostgresSQL v16.9.2 | |
| CREATE DATABASE IF NOT EXISTS `test` | |
| DEFAULT CHARACTER SET utf8 -- utf8mb4 | |
| DEFAULT COLLATE utf8_general_ci; -- utf8mb4_unicode_ci | |
| SET default_storage_engine = INNODB; | |
| CREATE TABLE user_details ( | |
| id bigint unsigned NOT NULL, | |
| email varchar(50) NOT NULL UNIQUE, | |
| first_name varchar(90) NOT NULL, | |
| last_name varchar(90) NOT NULL, | |
| profile_avatar_url varchar(130), -- start with value of NULL and use background job to upload image to `cloudinary` OR `aws-s3` an then later write url value back | |
| gender enum('male', 'female') NOT NULL, | |
| date_of_birth date NOT NULL, | |
| registered_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, | |
| CONSTRAINT userunq UNIQUE (id, first_name, last_name), | |
| PRIMARY KEY (id) | |
| ); | |
| CREATE INDEX idx_namex | |
| ON user_details (id, last_name, registered_at); | |
| CREATE TABLE user_accounts ( | |
| id bigint unsigned NOT NULL, | |
| email varchar(50) NOT NULL UNIQUE, -- data redundancy for easier access | |
| full_name varchar(150) NOT NULL, -- data redundancy for easier access | |
| password varchar(36) NOT NULL, | |
| last_login_at datetime, | |
| is_active tinyint(1) NOT NULL DEFAULT 1, | |
| email_verified tinyint(1) NOT NULL DEFAULT 0, | |
| phone_number varchar(16) UNIQUE NULL, | |
| PRIMARY KEY (id), | |
| FOREIGN KEY (id) REFERENCES user_details(id) ON DELETE CASCADE | |
| ); | |
| CREATE INDEX idx_authx | |
| ON user_accounts (id, email, password); | |
| CREATE TABLE ledger_types ( | |
| id bigint unsigned NOT NULL AUTO_INCREMENT, | |
| operational_attributes json NOT NULL DEFAULT (JSON_OBJECT()), | |
| currency enum('NGN', 'USD', 'KSH', 'GBP') NOT NULL, | |
| PRIMARY KEY (id) | |
| ); | |
| CREATE TABLE ledger_accounts ( | |
| id bigint unsigned NOT NULL, | |
| type_id bigint unsigned NOT NULL, | |
| currency enum('NGN-kobo', 'USD-cents', 'KSH-shillings', 'GBP-shillings') NOT NULL, | |
| account_number varchar(10) NOT NULL, | |
| owner_id bigint unsigned NOT NULL, | |
| account_created_at date NOT NULL, | |
| balance_updated_at datetime NOT NULL, | |
| balance_credit bigint unsigned NOT NULL DEFAULT 0, -- PN Counter modelling (from CRDTs) | |
| balance_debit bigint unsigned NOT NULL DEFAULT 0, -- PN Counter modelling (from CRDTs) | |
| PRIMARY KEY (id), | |
| FOREIGN KEY (type_id) REFERENCES ledger_types(id) ON DELETE CASCADE, | |
| FOREIGN KEY (owner_id) REFERENCES user_details(id) ON DELETE CASCADE | |
| ); | |
| ALTER TABLE ledger_accounts ADD CONSTRAINT chk_balance_debit CHECK (balance_debit < balance_credit) | |
| CREATE TABLE ledger_transactions ( | |
| id bigint unsigned NOT NULL, | |
| type enum('debit', 'credit') NOT NULL, | |
| reference varchar(30) NOT NULL, | |
| narration text, | |
| amount bigint unsigned NOT NULL, | |
| entry_created_at datetime NOT NULL, | |
| idempotency_key varchar(38) NOT NULL, | |
| wallet_id bigint unsigned NOT NULL, | |
| has_pay_gateway_webhook_resolved tinyint(1) NOT NULL DEFAULT 0, | |
| external_transaction_ref varchar(20) NOT NULL, | |
| CONSTRAINT trnxunq UNIQUE (id, type, amount, reference, wallet_id, idempotency_key), | |
| PRIMARY KEY (id), | |
| FOREIGN KEY (wallet_id) REFERENCES ledger_accounts (id) ON DELETE CASCADE, | |
| ); | |
| -- Transactional Outbox Pattern - Outbox Table | |
| CREATE TABLE ledger_account_transactions_outbox_events ( | |
| account_id bigint unsigned NOT NULL, | |
| transaction_id bigint unsigned NOT NULL, | |
| payload json NOT NULL DEFAULT (JSON_OBJECT()), | |
| sent_at timestamp, | |
| event_type enum('account_debited', 'account_credited') NOT NULL, | |
| next_run timestamp, | |
| interval_in_seconds bigint NOT NULL DEFAULT -1, | |
| created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, | |
| PRIMARY KEY (account_id, transaction_id), | |
| FOREIGN KEY (account_id) REFERENCES ledger_accounts(id) ON DELETE CASCADE, | |
| FOREIGN KEY (transaction_id) REFERENCES ledger_transactions(id) ON DELETE CASCADE | |
| ); | |
| CREATE TABLE ledger_account_virtual_cards ( | |
| id bigint unsigned NOT NULL, | |
| wallet_id bigint unsigned NOT NULL, | |
| card_number varchar(16) NOT NULL UNIQUE, | |
| expiry_date varchar(5) NOT NULL, | |
| cvv varchar(3) NOT NULL UNIQUE, | |
| card_status enum('active', 'inactive', 'suspended') NOT NULL DEFAULT 'active', | |
| created_at timestamp DEFAULT CURRENT_TIMESTAMP, | |
| PRIMARY KEY (id), | |
| INDEX wallet_indx (wallet_id), | |
| FOREIGN KEY (wallet_id) REFERENCES ledger_accounts (wallet_id) ON DELETE CASCADE | |
| ); | |
| INSERT INTO user_details (id, first_name, last_name, gender, profile_avatar_url, date_of_birth, email) VALUES | |
| (1, 'Akin', 'Lewi-Soyinka', 'male', 'https://me.assets/image/sBSd65749', '1988-04-11', 'akinbaba345@gmail.com'), | |
| (2, 'Tabitha', 'Ngorh', 'female', 'https://me.assets/image/o84Z8401Rytr', '1994-10-02', 'tabbysweetie20@yahoo.com'), | |
| (3, 'Eze', 'Barnabas', 'male', 'https://me.assets/image/pO74Judc748', '1990-08-25', 'eze4reel@hotmail.com'), | |
| (4, 'Destiny', 'Okondo', 'female', 'https://me.assets/image/placeholder', '1991-09-12', 'destiny.oko1@gmail.com'); | |
| INSERT INTO user_accounts (id, email, password, last_login_at, full_name, email_verified, is_active, phone_number) VALUES | |
| (2, 'tabbysweetie20@yahoo.com', 'xbu648dbs8849', '2025-03-11T00:00:00Z', 'Tabitha Ngorh', 1, 1, '+2348136682310'), | |
| (4, 'destiny.oko1@gmail.com', 'sby378bT%#3bjZ', '2025-04-23T00:00:00Z', 'Destiny Okondo', 1, 1, '08034211563'), | |
| (3, 'eze4reel@hotmail.com', 'cBv$33!LodMre', NULL, 'Eze Barnabas', 1, 1, '09037642119'), | |
| (1, 'akinbaba345@gmail.com', '', NULL, 'Akin Lewi-Soyinka', 0, 0, '+2347050901336'); | |
| INSERT INTO ledger_types (id, operational_attributes, currency) VALUES | |
| (1, '{ "overdraft_enabled": false, "cost_of_transaction": "0%", "activity_alert_channels": ["email"], "max_inflow_limit": 100_000, "min_outflow_limit": 0, "maintenance_charges": ["e-cash_transfer_levy"] }', 'NGN'), | |
| (3, '{ "overdraft_enabled": true, "cost_of_transaction": "10%", "activity_alert_channels": ["sms", "email"], "max_inflow_limit": 3_000_000_000, "min_outflow_limit": 100_000, "maintenance_charges": ["alert_channels", "standing_instruction"] }', 'USD'), | |
| (4, '{ "overdraft_enabled": false, "cost_of_transaction": "0%", "activity_alert_channels": ["email"], "max_inflow_limit": 500_000, "min_outflow_limit": 0, "maintenance_charges": ["alert_channels"] }', 'NGN') | |
| INSERT INTO ledger_accounts (id, type_id, currency, owner_id, account_created_at, balance_updated_at, balance_credit, balance_debit, account_number) VALUES | |
| (1, 1, 'NGN-kobo', 2, '2024-08-18', '2024-09-24T00:00:00Z', 0, 0, '0128993994'), | |
| (2, 1, 'NGN-kobo', 4, '2024-07-21', '2024-11-30T00:00:00Z', 0, 0, '0127833455'), | |
| (3, 3, 'USD-cents', 3, '2025-02-10', '2025-02-10T00:00:00Z', 0, 0, '0136609342'), | |
| (4, 4, 'NGN-kobo', 3, '2024-10-10', '2024-11-30T00:00:00Z', 100000, 0, '0125633801') | |
| INSERT INTO ledger_transactions (id, type, reference, narration, amount, entry_created_at, wallet_id, has_pay_gateway_webhook_resolved, external_transaction_ref, idempotency_key) VALUES | |
| (1, 'credit', 'dF538jKla372mNv90', 'pocket money from sis', 200000, '2024-08-22T00:00:00Z', 1, 1, '', ''), | |
| (2, 'credit', 'pu834kAbX950rTy21', 'birthday gift cash', 560000, '2024-09-15T00:00:00Z', 1, 1, '', ''), | |
| (3, 'debit', 'qW665eHvC437dXz54', 'nil', 150000, '2024-09-24T00:00:00Z', 1, 0, '', ''), | |
| (4, 'credit', 'iJ187lFcO967gIq32', 'mobile tfr to access', 3450000, '2024-11-30T00:00:00Z', 2, 0, '', ''), | |
| (5, 'debit', 'dF538jKla372mNv90', 'pocket money from sis', 200000, '2024-08-22T00:00:00Z', 2, 1, '', ''), -- double-entry book-keeping | |
| (6, 'debit', 'pu834kAbX950rTy21', 'birthday gift cash', 560000, '2024-09-15T00:00:00Z', 2, 1, '', ''), -- double-entry book-keeping | |
| (7, 'credit', 'qW665eHvC437dXz54', 'nil', 150000, '2024-09-24T00:00:00Z', 2, 0, '', ''), -- double-entry book-keeping | |
| (8, 'debit', 'iJ187lFcO967gIq32', 'mobile tfr to access', 3450000, '2024-11-30T00:00:00Z', 4, 0, '', '') -- double-entry book-keeping | |
| -- INSERT INTO ledger_account_transactions_outbox_events (`account_id`, `transaction_id`, `payload`. `sent_at`, `event_type`) VALUES (...) ON CONFLICT (sent_at) DO NOTHING | |
| INSERT INTO ledger_account_transactions_outbox_events (account_id, transaction_id, payload, sent_at, event_type, next_run) VALUES | |
| (1, 1, '{ "type": "credit", "amount": 200000, "owner_id": 2, "currency": "NGN-kobo", "reference": "dF538jKla372mNv90" }', NULL, 'account_credited', NULL), | |
| (2, 5, '{ "type": "debit", "amount": 200000, "owner_id": 4, "currency": "NGN-kobo", "reference": "dF538jKla372mNv90" }', NULL, 'account_debited', NULL), | |
| (1, 2, '{ "type": "credit", "amount": 560000, "owner_id": 2, "currency": "NGN-kobo", "reference": "pu834kAbX950rTy21" }', NULL, 'account_credited', NULL), | |
| (2, 4, '{ "type": "credit", "amount": 3450000, "owner_id": 4, "currency": "NGN-kobo", "reference": "iJ187lFcO967gIq32" }', NULL, 'account_credited', NULL) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.