Skip to content

Search documentation

Search documentation pages, sections, and topics.

On this page

Designing your chart of accounts

Ledfra guarantees every transaction balances. It cannot tell you that you put the money in the wrong account. That is the failure mode nobody warns you about: a chart of accounts can be structurally wrong, balance perfectly to the cent, and still report revenue you never earned. These are the design rules that keep your ledger honest — decide them once, up front, because accounts are hard to change after they have history.

This is a design guide, not a reference: it is about which accounts to create and why. It assumes you already know what an account is and how a transaction balances — if not, read Accounts and Transactions first. Examples use Firecnc, the fictional rental marketplace from the marketplace example.

Start with the five categories

Every ledger has the same five top-level categories. Each has a fixed normal balance — the side that makes it go up. You never choose that per account: it comes from the category you file the account under. Put an account in the right place and its sign is right forever.

CategoryNormal balanceAnswers
AssetsDebitWhat do we have? Bank, Stripe, PayPal balances
LiabilitiesCreditWhat do we owe? Money held for hosts, payables
EquityCreditWhat did owners put in? Paid-in capital
IncomeCreditWhat did we earn? Your fees and commission
ExpensesDebitWhat did we consume? Processing fees, salaries, marketing

Most design mistakes are really filing mistakes — an account in the wrong category. The rest of this page is mostly about the two categories people confuse: income and liabilities.

Only money you keep is income

This is the most common structural mistake, and the most expensive one to unwind. If you collect $230 from a guest and $200 of it belongs to the host, your income is $30 — not $230. The $200 was never yours. It is a debt from the instant you collect it until you pay it out.

text
A guest pays $230. $200 belongs to the host, $30 is your service fee.

❌ Book the whole $230 as revenue, the host's share as a cost
   DEBIT   assets:stripe          USD:230.00
   CREDIT  income:bookings        USD:230.00
   DEBIT   expenses:host-payouts  USD:200.00   (when you pay the host)
   CREDIT  assets:stripe          USD:200.00

✅ Split it the moment you collect it
   DEBIT   assets:stripe          USD:230.00
   CREDIT  income:service-fees    USD:30.00    ← yours
   CREDIT  liabilities:hosts/42   USD:200.00   ← the host's, you just hold it

Here is what makes this sting: both versions report the same $30 profit. Both balance. Nothing errors. But the first reports $230 of revenue and the second reports $30 — and revenue is the number on your board deck, your investor update, and your tax return. You will not notice from the ledger. You will notice from the conversation.

The test
Would you ever have to give this money back, or forward it to someone else? Then it is a liability, not income. Only the slice you keep is income.

Two rules follow directly from this, and both are worth stating on their own:

  • Paying out is not an expense. Paying the host $200 settles a debt — it moves money, it does not consume it. An expenses:host-payouts account double-counts: you already recorded the obligation when you took the booking.
  • A cost you pay has no matching income. The processor charges you $7. That is an expense, full stop. Inventing an income:processing-fees account to cancel it out inflates both revenue and costs while netting to the same profit.

Model every state money passes through

Money in a platform is rarely just "in" or "out". It is usually collected, then held for a while, then owed, then paid. If your ledger jumps straight from collected to paid out, it can answer "what did we pay?" but not "how much of other people's money are we sitting on right now?" — which is the question a regulator, an auditor, or your own finance lead will ask first.

Give each state its own account, and make each transition its own transaction:

The states a host's money passes throughMoney is collected into assets:stripe, held in liabilities:escrow/42 until the stay is confirmed, becomes owed in liabilities:hosts/42, and finally leaves assets:stripe as a weekly payout.bookingstay confirmedweekly payoutCollectedassetassets:stripeHeldliabilityliabilities:escrow/42Owedliabilityliabilities:hosts/42Paid outassetassets:stripe
Each state money passes through gets its own account — and each transition is a transaction.
  • Held (escrow) — collected, but the host has not earned it yet: the stay is not over, or the dispute window is still open.
  • Owed (payable) — confirmed and ready to pay out.

Key escrow per host (liabilities:escrow/42), not per booking. "How much are we holding for this host?" is then a balance you can read, instead of a report you have to build. It also keeps the number of accounts proportional to your hosts rather than your order volume.

Only model states you actually have
If you pay hosts out the moment a guest pays, you do not need an escrow account — don't add one for symmetry. Every state you model is an account to design and a transaction to post. Add them when the money really does sit somewhere.

Name the leaf, not the path

Accounts live in a hierarchy, and the app shows the full path. Repeating the parents in the name is noise you read on every screen and every report line.

text
✅ Expenses › Stripe › Processing fees
❌ Expenses › Stripe › Stripe Processing Fees Expense

✅ Assets › Current assets › Stripe
❌ Assets › Current assets › Current Stripe Assets

If you catch yourself prefixing a name, you probably want a subcategory instead. Group with structure, not with longer strings. This is also what accounting software does, so the people reviewing your numbers read it without translating.

Slugs are the exception: they are identifiers, not labels. Nobody reads them in a report, so keep them explicit and stable (expenses:stripe-fees, liabilities:hosts) rather than pretty. A slug is forever — which is what the next rule is about.

Key per-object accounts by a stable id

A template account gets one account per object, and the id you use becomes part of that account's identity permanently. Ledger accounts carry posted history, so they are not safely renamable.

text
✅ liabilities:hosts/42          ← your database id for the host
❌ liabilities:hosts/jane-doe    ← breaks when Jane changes her name
❌ liabilities:hosts/jane@x.io   ← breaks when Jane changes her email

Use the primary key from your own database — the id you already use for that user, host, or company. If you bill companies rather than people, key on the company. Anything a human can edit is a trap: usernames, emails, display names, handles. They look better in the slug right up until someone changes one and you are holding an account named after a person who no longer exists.

The tradeoff, honestly
Ids are less readable. liabilities:hosts/42 does not tell you who host 42 is, so showing a name means joining back to your own database. That is the price of a key that never breaks, and it is the right trade: display names change, history does not. Put the name in the transaction description or metadata if you want it visible on the transaction itself.

Usernames are safe only if they are immutable in your system — genuinely immutable, not "we don't have that feature yet."

Block overdrafts on accounts that must never go negative

Some accounts have no meaning below zero. A host's payable cannot go negative — you cannot owe them less than nothing. Your PayPal balance cannot go below zero, because the real one cannot.

Every account has an Allow negative balance switch, and it is on by default. Turn it off on accounts like these. Ledfra then rejects any transaction that would overdraw the account, so a bug fails loudly at write time instead of quietly producing a balance that cannot exist.

This does more than catch bugs — it forces you to model funding honestly. If you collect on Stripe but pay out from PayPal, PayPal cannot go negative, so you have to post the real Stripe-to-PayPal transfer before the payout. That transfer happens in the real world. Now it is in your books too.

Balances are checked as you post
The check runs in the order transactions are written, not in date order. When you backfill history, post the funding before the spending — otherwise a payout can be rejected for overdrawing an account that a not-yet-posted deposit would have covered.

Write descriptions a reviewer can scan

Someone reviewing a month of transactions wants to know what kind of transaction each one is. They do not need the booking details — they can open the booking. Describe the transaction type and the counterparty; keep your own identifiers in metadata, where you can filter on them.

text
✅ "Payout to host 42 via Stripe"
✅ "Escrow released to host 42"
❌ "Loft in Berlin Mitte, 3 nights, cleaning included"

Keep the vocabulary small and closed — booking payment, payout, refund, transfer. A short list you reuse becomes a reporting dimension. A free-text field written by five engineers becomes nothing.

Design checklist

Before you post your first real transaction, walk this list:

  • Only money you actually keep sits under Income.
  • No account treats paying a debt as an expense.
  • Money you hold for someone else is a liability, from the moment you collect it.
  • Money that sits somewhere for a while has an account for that state.
  • Every per-object account is keyed by an id that can never change.
  • Account names do not repeat their parents; you group with subcategories.
  • Accounts that cannot go below zero have Allow negative balance turned off.
  • Descriptions say what kind of transaction it is; your ids live in metadata.

Where to go next

See these rules applied end to end in the marketplace example, or explore a ledger that already follows them in the live demo. For the mechanics, read Accounts, Transactions, and Money. When you are ready to try a design, do it against a sandbox first, and post through the API reference.