Database Schema

PostgreSQL on Supabase. 20+ tables with Row-Level Security, triggers, functions, and vector search.

1 Entity Relationships

users (root) |---> wallets (1:N) |---> categories (1:N) -- includes budget fields | |---> parent_id → categories (hierarchy) |---> transactions (1:N) | |---> wallet_id → wallets | |---> category_id → categories | |---> merchant_id → merchants | |---> parent_id → transactions (split) |---> contacts (1:N) -- debt tracking via transactions |---> merchants (1:N) | |---> category_id → categories |---> recurring_templates (1:N) | |---> wallet_id → wallets | |---> category_id → categories |---> automation_rules (1:N) | |---> rule_applications transactions |---> chat_messages (1:N) | |---> chat_feedback (1:1 per message) |---> action_history (1:N, max 50) |---> pending_actions (1:N, with TTL) |---> ai_trigger_configs (1:1) |---> ai_notifications (1:N) |---> reminders (1:N) |---> entity_aliases (1:N) |---> user_devices (1:N) |---> conversation_memories (1:N, vector) |---> financial_health_history (1:N) |---> suggestions (1:N) `---> execution_plans (1:N) Global tables (no user_id): mcc_categories -- MCC code to category mapping merchant_patterns -- auto-detection patterns currency_rates -- exchange rate cache docs_feedback -- documentation feedback

2 Core Tables

users

CORE
ColumnTypeNotes
idUUID PKSynced from Supabase Auth
emailTEXT NOT NULL
nameTEXT
default_wallet_idUUIDFK → wallets
default_currencyVARCHAR(3)Default: RUB
is_premiumBOOLEANDefault: false
subscription_typeVARCHAR(20)free / monthly / yearly / lifetime
chat_retention_daysINTEGER0 = unlimited
sync_versionINTEGERFor multi-device conflict resolution
created_at, updated_atTIMESTAMPTZ

wallets

CORE
ColumnTypeNotes
idUUID PK
user_idUUID NOT NULLFK → users (CASCADE)
nameVARCHAR(100)
currencyVARCHAR(3)Default: RUB
balanceNUMERICCached, updated by triggers
iconTEXTAsset icon name
colorVARCHAR(7)Hex color
is_archivedBOOLEAN

categories

CORE
ColumnTypeNotes
idUUID PK
user_idUUID NOT NULLFK → users
nameVARCHAR(100)System/English name
name_ruVARCHAR(100)Russian display name
slugVARCHAR(50)For localization (food, salary)
typeVARCHAR(10)income / expense
parent_idUUIDFK → categories (hierarchy)
icon, colorTEXT, VARCHAR(7)
is_systemBOOLEANCannot be deleted
system_slugVARCHAR(50)Internal ID (debt_gave, debt_took)
budget_amountNUMERICNULL = no budget. Budgets stored directly on categories.
budget_periodVARCHAR(20)weekly / monthly / yearly
budget_currencyVARCHAR(3)Default: user's currency
Budget tracking: categories with budget_amount != NULL act as budgets. current_spent is computed at query time from transactions.

transactions

CORE
ColumnTypeNotes
idUUID PK
user_idUUID NOT NULLFK → users
wallet_idUUIDFK → wallets (SET NULL on delete)
category_idUUIDFK → categories
merchant_idUUIDFK → merchants
typeVARCHAR(10)income / expense / transfer
amountNUMERIC NOT NULLAlways positive
currencyVARCHAR(3)
exchange_rateNUMERICNULL if same currency as wallet
descriptionTEXT
dateTIMESTAMPTZ
parent_idUUIDFK → transactions (split)
is_splitBOOLEAN
contact_nameVARCHAR(100)For debt transactions
debt_due_dateDATE
7 indexes: user, wallet, merchant, date DESC, recurrence, parent, split, orphaned

contacts

CORE
ColumnTypeNotes
idUUID PK
user_idUUID NOT NULLFK → users
nameVARCHAR(100)
phone, emailVARCHAR
cached_balanceNUMERIC+ = they owe me, - = I owe them
due_dateDATEWhen debt should be repaid
reminder_daysINTEGERDefault: 3
is_archivedBOOLEAN
Debt tracking uses transactions with contact_name field linked to contacts

3 Financial Tables

💡
Architecture Notes Budgets are stored as fields on the categories table (budget_amount, budget_period, budget_currency). No separate budgets table. current_spent is computed at query time from transactions.
Goals are implemented as wallets with type='goal' (target_amount, current_amount fields).
Debts are tracked via contacts table + transactions with contact_name field.

recurring_templates

CORE
ColumnTypeNotes
idUUID PK
user_idUUID NOT NULLFK → users (CASCADE)
wallet_idUUIDFK → wallets (CASCADE)
category_idUUIDFK → categories (SET NULL)
amountNUMERIC NOT NULLCHECK > 0
currencyVARCHAR(3)Default: RUB
descriptionTEXT
frequencyVARCHAR(20) NOT NULLdaily / weekly / monthly / yearly
interval_valueINTEGERDefault: 1. E.g. 2 = every 2 weeks
start_dateDATEWhen the recurring schedule started
next_run_dateDATE NOT NULLProcessed by scheduler/cron function
last_run_dateDATE
is_activeBOOLEANDefault: true
cancelled_atTIMESTAMPTZSet when subscription is cancelled
All recurring templates create expense transactions. Processed by process_recurring_templates() DB function.

mcc_categories

SUPPORT
ColumnTypeNotes
idUUID PK
mccVARCHAR(4) UNIQUEMerchant Category Code
name_en, name_ruVARCHAR(200)Localized names
default_category_slugVARCHAR(50)Maps to categories.slug
category_typeVARCHAR(10)income / expense
group_nameVARCHAR(100)MCC group (e.g. "Groceries")
is_activeBOOLEANDefault: true
Global table (no user_id). Maps MCC codes to default categories for auto-categorization.

merchant_patterns

SUPPORT
ColumnTypeNotes
idUUID PK
patternVARCHAR(200) NOT NULLMatch pattern text
pattern_typeVARCHAR(20)exact / prefix / contains / regex
mccVARCHAR(4)Associated MCC code
merchant_nameVARCHAR(200)Resolved merchant name
priorityINTEGERHigher = matched first
is_activeBOOLEANDefault: true
Global table (no user_id). Used for automatic merchant detection from transaction descriptions.

merchants

SUPPORT
ColumnTypeNotes
idUUID PK
user_idUUID NOT NULL
nameVARCHAR(200)Normalized display name. UNIQUE(user_id, name)
raw_patternsTEXT[]Original descriptions that matched
mccVARCHAR(4)Merchant category code
category_idUUIDFK → categories
transaction_countINTEGERUsage frequency

4 AI & Notification Tables

ai_trigger_configs

AI
ColumnTypeNotes
user_idUUID UNIQUE1:1 with users
enabledBOOLEAN
push_enabledBOOLEAN
quiet_hours_start/endTIMEDefault: 23:00 - 08:00
triggersJSONBPer-trigger settings: { limit_warning: { enabled, threshold } }

ai_notifications

AI
ColumnTypeNotes
trigger_typeVARCHAR(50)limit_warning, goal_milestone, salary_detected, etc.
title, messageVARCHAR / TEXT
statusVARCHAR(20)pending / seen / accepted / dismissed / snoozed
priorityVARCHAR(10)low / normal / high / urgent
related_entity_type/idVARCHAR / UUIDbudget, goal, contact, recurring
actionJSONB{ type, payload, label }

conversation_memories

AI / RAG
ColumnTypeNotes
summaryTEXTSummarized conversation
embeddingvector(1536)text-embedding-3-small. HNSW index.
topicsTEXT[]
message_countINTEGER
Requires CREATE EXTENSION IF NOT EXISTS vector;

5 Supporting Tables

📋
Other tables chat_messages, action_history, pending_actions, reminders, notification_milestones, device_tokens, user_devices, entity_aliases, financial_health_history, currency_rates, automation_rules, rule_applications, suggestions, execution_plans, docs_feedback, chat_feedback. Full definitions in Server/sql/schema.sql.

6 Database Functions & Triggers

update_wallet_balance(wallet_id, amount)

Atomically updates wallet balance: balance = balance + amount. Used after transaction creation.

transfer_between_wallets(from, to, amount, user_id, desc)

Atomic transfer with row locking (FOR UPDATE). Validates balance, creates transfer transaction, returns new balances.

process_recurring_templates()

Cron function: creates expense transactions from active recurring templates where next_run_date <= today. Updates wallet balances and advances next run date using interval_value.

update_merchant_usage(merchant_id, raw_pattern)

Increments merchant transaction_count, updates last_used_at, and appends new patterns to raw_patterns array.

cleanup_old_action_history() TRIGGER

After INSERT on action_history: keeps only last 50 records per user, deletes older ones.

delete_user_data(user_id)

GDPR compliance: atomically deletes all user data from all tables in correct FK order (including recurring_templates, contacts, entity_aliases, etc.). Returns deletion report.

💡
Budget tracking Budget current_spent is computed at query time by summing transactions for the budget period. No database trigger or cron function is needed for budget tracking.

7 Row-Level Security

🔒
All tables have RLS enabled Every table uses the policy: auth.uid() = user_id. Users can only access their own data. Server uses service_role key which bypasses RLS for admin operations.

8 Field Naming Conventions

Database (snake_case)Server TS (camelCase)iOS Swift (camelCase)
budget_amountbudgetAmountbudgetAmount
current_spentcurrentSpentcurrentSpent
category_idcategoryIdcategoryId
wallet_namewalletNamewalletName
is_defaultisDefaultisDefault
created_atcreatedAtcreatedAt
next_run_datenextRunDatenextRunDate