Skip to content

Chart of Accounts

The authoritative catalog of general-ledger accounts used by every double-entry posting in the app. Covers the Account entity's shape, the five-way primary classification, the seeded canonical chart, the sub-ledger pattern used for per-party accounts, and the rules for adding, renaming, or archiving accounts.

Audience: finance leads who need to know why an account exists; auditors tracing a posting back to its classification; developers adding a new account or a new posting path.


1. The Account entity

A single table — public."Account" — holds the entire chart. Every JournalLine references exactly one Account.id. There is no parallel "ledger account" table; the same row serves both as a classification label and as a posting target.

Shape (composed across migrations):

Column Source migration Purpose
id (text PK) 20260113130121_remote_schema.sql:22 UUID, referenced by JournalLine.accountId.
code (text, unique) 20260113130121_remote_schema.sql:24, uniqueness at :692 Numeric (e.g. 1010) or prefix-suffix (e.g. G-ASSET, CUS-<id>). Stable identifier used by FinanceConfig + runtime helpers.
name 20260113130121_remote_schema.sql:25 Human label shown on reports.
type (enum AccountType) 20260113130121_remote_schema.sql:3, 26 One of ASSET / LIABILITY / EQUITY / INCOME / EXPENSE. Enforced by Postgres enum — you cannot insert a sixth class without a DDL change.
parentId (text, self FK) 20260330170000_account_hierarchy_and_vouchers.sql Pointer to the parent group account. NULL at the root.
isGroup (bool) 20260403070000_account_hierarchy.sql true = branch node (no postings); false = leaf (may receive postings).
groupType (text) 20260403070000_account_hierarchy.sql One of fixed_asset, current_asset, investment, current_liability, long_term_liability, capital, direct_income, indirect_income, direct_expense, indirect_expense. Used for Balance Sheet / P&L grouping.
sortOrder (int) 20260403070000_account_hierarchy.sql Display ordering within its parent group.
isActive (bool) 20260113130121_remote_schema.sql:27 Soft-archive flag.
currency (text) financial controls migrations 3-letter ISO code. Currency-guarded when posting — see double-entry-engine.md.
openingBalance, openingBalanceSide 20260330160000_account_opening_balance_columns.sql One-time opening balance and which side it lands on.
allowDebit, allowCredit, allowNegativeBalance 20260403110000_financial_account_controls.sql Per-account posting controls enforced by validateFinancialAccountPosting() in src/lib/financeOperations.ts:80.
bankName, accountNumber, notes later migrations Metadata for bank/cash leaves.

Two tables, distinct jobs

Account is the GL chart of accounts. A separate FinancialAccount table (supabase/migrations/20260401100000_accounts.sql) represents real-world bank/cash/card/wallet accounts the business actually operates. The two are linked by convention: FinancialAccount rows mirror specific Account leaves (e.g. a JKBANK Current Account FinancialAccount row mirrors leaf 1010 Bank Account in the GL). Do not conflate them; see docs/features/finance/bank-reconciliation.md for how bank-side reconciliation uses FinancialAccount.


2. Primary classification

AccountType is a Postgres enum created in supabase/migrations/20260113130121_remote_schema.sql:3:

create type "public"."AccountType" as enum
  ('ASSET', 'LIABILITY', 'EQUITY', 'INCOME', 'EXPENSE');
Type Normal side Report Examples
ASSET Debit Balance Sheet Cash, Bank, Receivables, Advance to Suppliers, Fixed Assets, GST Input Credit.
LIABILITY Credit Balance Sheet Supplier Payables, Agent Payables, GST Payable, TDS Payable.
EQUITY Credit Balance Sheet Owner's Capital, Partner Capital, Opening-Balance Equity.
INCOME Credit P&L Sales/Booking Revenue, Commission Earned, Closing Stock.
EXPENSE Debit P&L Purchase A/c, Hotel/Food/Ground Expenses, Salaries, Agent Commission Expense.

Do not add a sixth type

AccountType is a hard enum. Adding CONTRA or MEMO is a schema change that cascades into every report, every posting helper, and every balance-side inference in src/lib/financeOperations.ts:107. If you think you need it, you probably want a groupType instead.


3. Hierarchy (tree model)

The chart is a strict tree: every non-root account has exactly one parent via parentId. The canonical rebuild migration supabase/migrations/20260412020000_chart_of_accounts_canonical.sql establishes five roots — one per AccountType — plus Tally-style sub-groups underneath.

graph TD
    A[G-ASSET Assets] --> A1[G-CURRENT-ASSET Current Assets]
    A --> A2[G-FIXED-ASSET Fixed Assets]
    A1 --> A1a[G-CASH Cash in Hand]
    A1 --> A1b[G-BANK Bank Accounts]
    A1 --> A1c[G-SUNDRY-DEBTORS Sundry Debtors]
    A1c --> A1c1[1200 Accounts Receivable]
    A1c --> A1c2[G-AGENT-RECV Agent Receivables]
    A1c2 --> A1c2a[1210 Agent Receivables]
    A1a --> A1a1[1000 Cash]
    A1b --> A1b1[1010 Bank Account]
    L[G-LIABILITY Liabilities] --> L1[G-CURRENT-LIAB Current Liabilities]
    L1 --> L1a[G-SUNDRY-CREDITORS Sundry Creditors]
    L1a --> L1a1[2100 Supplier Payables]
    L1 --> L1b[G-AGENT-PAYABLES Agent Payables]
    L1b --> L1b1[2300 Agent Payables]
    L1 --> L1c[G-DUTIES-TAXES Duties & Taxes]
    L1c --> L1c1[2400 GST Payable]
    L1c --> L1c2[2401 TDS Payable]
  • Root group codes are prefixed G- (e.g. G-ASSET, G-CURRENT-ASSET).
  • Leaf codes are typically 4-digit numeric (1000, 1010, 2400).
  • isGroup = true means the row is a classification bucket, NOT a posting target. validateFinancialAccountPosting() rejects attempts to post a journal line against a group.

The schema does not enforce group-vs-leaf at DB level

Nothing stops a JournalLine from pointing at a row with isGroup = true. The API-layer validator is the only enforcement today. A future hardening step could add a CHECK constraint trigger; tracked as <!-- TODO: verify — add a BEFORE INSERT trigger on JournalLine that rejects accountId where Account.isGroup -->.


4. Seeded canonical chart

The current canonical chart is rebuilt by supabase/migrations/20260412020000_chart_of_accounts_canonical.sql. This migration wipes test journals and re-establishes the chart from scratch on first apply; subsequent applies are idempotent upserts. Later migrations layer additional leaves (1400 GST Input Credit, 2401 TDS Payable).

4.1 Root groups

Code Name Type
G-ASSET Assets ASSET
G-LIABILITY Liabilities LIABILITY
G-EQUITY Capital & Equity EQUITY
G-INCOME Income INCOME
G-EXPENSE Expenses EXPENSE

4.2 Sub-groups

Code Name Under groupType
G-CURRENT-ASSET Current Assets G-ASSET current_asset
G-FIXED-ASSET Fixed Assets G-ASSET fixed_asset
G-CASH Cash in Hand G-CURRENT-ASSET current_asset
G-BANK Bank Accounts G-CURRENT-ASSET current_asset
G-SUNDRY-DEBTORS Sundry Debtors G-CURRENT-ASSET current_asset
G-AGENT-RECV Agent Receivables G-SUNDRY-DEBTORS current_asset
G-CURRENT-LIAB Current Liabilities G-LIABILITY current_liability
G-SUNDRY-CREDITORS Sundry Creditors G-CURRENT-LIAB current_liability
G-AGENT-PAYABLES Agent Payables G-CURRENT-LIAB current_liability
G-DUTIES-TAXES Duties & Taxes G-CURRENT-LIAB current_liability
4001 Direct Income G-INCOME direct_income
5001 Direct Expenses G-EXPENSE direct_expense
6001 Indirect Expenses G-EXPENSE indirect_expense

4.3 Asset leaves

Code Name Purpose
1000 Cash Petty cash in hand. Debit on receipts, credit on cash payouts. Linked to FinanceConfig.cashAccountCode.
1010 Bank Account Default bank account. Linked to FinanceConfig.bankAccountCode.
1198 Account Transaction Clearing Bridge account for FinancialAccountTransaction → GL synchronisation.
1200 Accounts Receivable Customer receivable control account. Parent of per-customer CUS-<customerId> sub-ledgers. Linked via FinanceConfig.receivableAccountCode.
1210 Agent Receivables Agent receivable control. Parent of per-agent AGR-<agentId> sub-ledgers. Linked via FinanceConfig.agentReceivableAccountCode.
1245 Cancellation Refund Receivable Suspense for amounts we expect back on cancellations.
1300 Advance to Suppliers Prepayments before the supplier invoice lands. Linked via FinanceConfig.advanceToSuppliersAccountCode.
1310 Stock-in-Hand Inventory-valuation control. Posted by the opening/closing stock adjustment handler.
1400 GST Input Credit Added by supabase/migrations/20260415100000_gst_input_credit_and_supplier_gst.sql. Carries the recoverable GST on supplier invoices. Linked via FinanceConfig.gstInputCreditAccountCode.
1500 / 1510 / 1520 / 1530 Office Equipment / Vehicles / Furniture & Fixtures / Computer & IT Equipment Fixed-asset leaves.

4.4 Liability leaves

Code Name Purpose
2100 Supplier Payables Supplier control account. Parent of per-supplier SUP-<supplierId> sub-ledgers. Linked via FinanceConfig.supplierPayableAccountCode.
2200 Credit Card Payable Credit card balance carried as current liability.
2300 Agent Payables Agent control account. Parent of per-agent AGP-<agentId> sub-ledgers. Linked via FinanceConfig.agentPayableAccountCode.
2400 GST Payable Output GST awaiting remittance. Linked via FinanceConfig.gstPayableAccountCode.
2401 TDS Payable Added by supabase/migrations/20260418150000_tds_scaffolding.sql. Withheld TDS awaiting Form 281 deposit. Linked via FinanceConfig.tdsPayableAccountCode.

4.5 Equity leaves

Code Name Purpose
3000 Owner's Capital Proprietor capital.
3100 Partner Capital Partner contribution.

ensureOpeningBalanceEquityAccount() in src/lib/api.ts:185 auto-creates G-CAPITAL and an Opening Balance Equity leaf on first use by the opening-balance importer.

4.6 Income leaves

Code Name Purpose
4000 Sales / Booking Revenue Default revenue head for bookings + group invoices. Linked via FinanceConfig.revenueAccountCode.
4100 Commission Earned Agent commission income we receive (not what we pay out).
4500 Closing Stock A/c Closing-stock counter-entry for inventory valuation.

4.7 Expense leaves

Code Name Purpose
5000 Purchase A/c Default supplier-expense bucket. Collapses visa/ticketing/uncategorised supplier expenses that don't have a dedicated head.
5050 Opening Stock A/c Opening-stock counter-entry.
5100 Airline Block Purchases Linked via FinanceConfig.blockPurchaseAccountCode.
5200 Hotel Expenses Linked via FinanceConfig.hotelExpenseAccountCode.
5300 Food / Meal Expenses Linked via FinanceConfig.foodExpenseAccountCode.
5400 Ground Transport Expenses Linked via FinanceConfig.groundExpenseAccountCode.
6100 Salaries & Wages Payroll expense.
6200 Cancellation & Rescheduling Charges Linked via FinanceConfig.cancellationChargesAccountCode.
6300 Agent Commission Expense Commission we pay out to agents. Linked via FinanceConfig.agentCommissionExpenseAccountCode.

FX gain/loss is not yet a dedicated leaf

The canonical chart does not carve out an FX Gain / FX Loss account. JournalEntry.fxRateUsed + JournalLine.originalCurrency capture the conversion audit trail at the line level, but realised FX differences currently accrue into the revenue/expense head they touched. <!-- TODO: verify — if an FX gain/loss head is needed for the statutory auditor, add it under G-INCOME and G-EXPENSE and thread it through FinanceConfig. -->


5. Per-party sub-ledgers (auto-created)

Customer, agent, and supplier postings do NOT land on 1200 / 1210 / 2100 / 2300 directly. The runtime ensures a dedicated sub-account per party, keyed by a coded prefix.

Prefix Parent Ensured by Purpose
CUS-<customerId> 1200 Accounts Receivable resolveCustomerReceivableAccount() (in src/lib/api.ts) One row per customer. Ledger balance = that customer's AR.
AGR-<agentId> G-AGENT-RECV resolveAgentReceivableAccount() Agent receivables.
AGP-<agentId> G-AGENT-PAYABLES Agent payable helper Agent payables (commissions owed).
SUP-<supplierId> 2100 Supplier Payables Supplier payable helper Supplier-specific payable.

The parents are re-linked on every run of the canonical migration (supabase/migrations/20260412020000_chart_of_accounts_canonical.sql:186-189), so a surviving CUS-* / SUP-* / AGR-* / AGP-* row is always re-parented even if an earlier manual edit set parentId = NULL.

Do not post directly to the control account

If a handler posts DR 1200 / CR 4000 for a specific customer invoice, the customer's sub-ledger drill-down will miss the entry and AR aging will under-state their balance. Always route through the resolveCustomerReceivableAccount() helper.


6. Account → event linkage

FinanceConfig (single-row table, id 'default') stores the codes used by every posting path. When the posting helper fires, it resolves Account.id by code lookup, then posts.

Event Debit Credit FinanceConfig pointer
Customer cash receipt 1000 / 1010 CUS-* → parent 1200 cashAccountCode / bankAccountCode + receivableAccountCode
Customer invoice issued CUS-* 4000 Sales / Booking Revenue revenueAccountCode + receivableAccountCode
GST on customer invoice (none — added to receivable) 2400 GST Payable gstPayableAccountCode
Supplier invoice booked 5100/5200/5300/5400/5000 SUP-* → parent 2100 expense codes + supplierPayableAccountCode
GST on supplier invoice 1400 GST Input Credit (added to payable) gstInputCreditAccountCode
Supplier payment SUP-* 1000/1010 supplierPayableAccountCode + cash/bank
TDS withheld on supplier payment SUP-* 2401 TDS Payable tdsPayableAccountCode
Agent commission earned 6300 Agent Commission Expense AGP-* agentCommissionExpenseAccountCode + agentPayableAccountCode
Agent on-account receipt 1000/1010 AGR-* cash/bank + agentReceivableAccountCode
Advance to supplier 1300 Advance to Suppliers 1000/1010 advanceToSuppliersAccountCode
Booking cancellation charge 6200 Cancellation & Rescheduling Charges CUS-* (or refund liability) cancellationChargesAccountCode
Opening balance import per side Opening Balance Equity see ensureOpeningBalanceEquityAccount()

See double-entry-engine.md for the full posting-path enumeration with code references.


7. Adding a user-created account

UI path

  1. Navigate to /finance?tab=accounts — requires finance.view.
  2. Click Add Account in the chart tree — requires finance.edit (server-side <PermissionGate> + requirePermission('finance.edit') in src/lib/api.ts:20628).
  3. Fill: code, name, type, parent (pick the right group), groupType, opening balance + side if applicable.
  4. Submit — the API upserts by code. If a code already exists, the API returns the existing row instead of duplicating (src/lib/api.ts:20634-20637).

Required permission

finance.edit. Held by FINANCE_MANAGER, ACCOUNTANT, and every super-admin role (see docs/PERMISSIONS.md §5.2).

Server-side behaviour

  • Inserts the account row with all control columns at defaults (allowDebit = allowCredit = true, allowNegativeBalance = false).
  • If openingBalance > 0, auto-posts a balanced opening-balance journal against Opening Balance Equity (src/lib/api.ts:20663-20684).

8. Renaming, archiving, deleting

Renaming

  • Allowed anytime via PATCH /finance/accounts/:id with finance.edit permission (src/lib/api.ts:20687-20716).
  • The code can also be changed. Since FinanceConfig points at accounts by code, renaming the code of a linked account will break runtime resolution until FinanceConfig.*AccountCode is updated to match. Rename with care.

Archiving (soft-delete)

  • Set isActive = false via the same PATCH. The account stays in the DB, existing postings remain valid, but new UI dropdowns hide it.
  • Preferred over DELETE whenever the account has ledger activity.

Hard delete

Governed by DELETE /finance/accounts/:id in src/lib/api.ts:20719-20920.

Refuses when:

  • The account has child accounts with parentId = <this id> (src/lib/api.ts:20869-20879).
  • The account has any JournalLine rows (src/lib/api.ts:20881-20891).
  • The account is referenced by a FinancialAccount row (bank/cash/etc.) (src/lib/api.ts:20893-20911).

Force delete (?force=true, src/lib/api.ts:20731+) is a destructive cascade: wipes journal lines/entries, ledger entries, supplier transactions linked through the tree, and any linked FinancialAccount rows. Requires finance.edit and is intended only for rebuild scenarios.

Never hard-delete an account with history outside a rebuild

Deleting a leaf that has ever received a posting breaks the audit trail — every historical journal line FK is gone, every report that joins through it turns up blanks, and bank reconciliation loses its anchor. Prefer archiving.


9. Cross-references

  • Overview — module map, tab structure, permission summary.
  • Double-entry engine — the engine that consumes this chart.
  • Audit trail — what gets logged when an account is created / edited / deleted.
  • Permissions matrixfinance.edit as the gatekeeper.
  • docs/PERMISSIONS.md — canonical permission catalog.