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:
| 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 = truemeans 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
- Navigate to
/finance?tab=accounts— requiresfinance.view. - Click Add Account in the chart tree — requires
finance.edit(server-side<PermissionGate>+requirePermission('finance.edit')insrc/lib/api.ts:20628). - Fill: code, name, type, parent (pick the right group), groupType, opening balance + side if applicable.
- 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 againstOpening Balance Equity(src/lib/api.ts:20663-20684).
8. Renaming, archiving, deleting
Renaming
- Allowed anytime via
PATCH /finance/accounts/:idwithfinance.editpermission (src/lib/api.ts:20687-20716). - The
codecan also be changed. SinceFinanceConfigpoints at accounts by code, renaming the code of a linked account will break runtime resolution untilFinanceConfig.*AccountCodeis updated to match. Rename with care.
Archiving (soft-delete)
- Set
isActive = falsevia the same PATCH. The account stays in the DB, existing postings remain valid, but new UI dropdowns hide it. - Preferred over
DELETEwhenever 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
JournalLinerows (src/lib/api.ts:20881-20891). - The account is referenced by a
FinancialAccountrow (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 matrix —
finance.editas the gatekeeper. docs/PERMISSIONS.md— canonical permission catalog.