Skip to content

Search documentation

Search documentation pages, sections, and topics.

On this page

How to design your ledger

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 page is the working checklist: category by category, which accounts to create and why. The theory behind it lives in two guides and is worth the read when a rule here feels arbitrary - create the ledger structure teaches the five categories from zero, and model normal debit and credit accounts derives why each one increases on the side it does. Examples use Firecnc, the fictional rental marketplace from the How to build a marketplace ledger.

A new ledger already has the five categories

You do not create them, and you cannot add a sixth. Create a ledger and the chart of accounts already lists all five, empty and waiting for you to file accounts under them. Each one carries a fixed normal balance, the side that makes it go up, and you never choose that per account either - it comes with the category you file under.

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

So the work is never "design the top level". It is filing: every account you add goes under one of these five, and that choice is the design decision. Put an account in the right place and its sign is right forever - which is why most design mistakes are really filing mistakes, the right amount correctly recorded under the wrong root. The rest of this page walks the five in the order worth building them, and spends the most time on the two that get confused - income and liabilities.

Define your asset accounts: one per place money sits

Start with the easy category. List every place your money physically sits, and give each one its own account:

text
assets
├── mercury               your operating bank account
├── stripe                money Stripe holds for you
├── paypal                money PayPal holds for you
└── payouts-in-transit    left Stripe, not landed in a host's bank yet

The rule of thumb: if a real statement exists for it, it gets an account. Your bank issues a statement, Stripe reports a balance, PayPal reports a balance - one account each, so every balance in your ledger can be checked against the source that produced it. Add an in-transit account for money that has left one place and not yet landed in another, because for those days it genuinely sits nowhere a statement can see.

Do not invent structure you do not have. If all your cash sits in one bank account, one asset account is enough. And resist filing things here that are really costs: the boundary between an asset and an expense is whether the value is still worth something next month, and create the ledger structure covers the cases that hesitate, prepaid contracts especially.

Define income and expenses: 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.

So the income side of your tree is usually short: one account per thing you actually charge for (service fees, listing fees, subscriptions). The expense side is whatever you consume to operate - processing fees, hosting, salaries. Two rules keep both sides honest:

  • 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.

Define your liability accounts: 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.

Equity: usually just paid-in capital

For most platforms this is the shortest section of the tree: one account for what the owners and investors put in. When Firecnc raises a Series A, the cash lands in an asset account and the same amount lands in equity:paid-in-capital - the money and the claim on it, recorded together.

Do not create an account for retained earnings. The balance sheet computes it from your income and expenses since inception, so it is always right and never posted. If earning money made you reach for an equity account, the amount probably belongs under income instead.

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 what your code posts against, which is what the next rule is about.

Once a ledger holds more than one currency, put the currency in the slug. assets:stripe:usd sits alongside assets:stripe:eur, and a per-counterparty template needs one per currency. An account holds one currency for life, so these really are different accounts. The suffix is a convention rather than something Ledfra reads, so nothing breaks if you name them another way, but a reader can tell two Stripe balances apart at a glance instead of wondering which is the duplicate.

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. Renaming a slug is survivable now, because the old one is kept as an alias and postings to it keep working, but the alias covers writes only: the API returns the new slug from the moment you rename, and an id you picked badly is one you will be reading in reports for years.

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."

The id takes letters, numbers, hyphens and underscores - an integer, a UUID, a vendor object id, an immutable username. An email is refused rather than discouraged, which is the whole point: the charset makes the expensive mistake impossible. Non-ASCII is refused for a different reason - аcme written with a Cyrillic а looks exactly like acme and would be a separate account, quietly splitting one customer's money across two.

Ids are case-significant and stored exactly as you send them, so hosts/JaneDoe and hosts/janedoe are two accounts. Nothing is folded or rewritten: if your ids come from a system that treats case as meaningful, merging two of them could not be undone once history references the account.

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.

Mark the accounts that hold cash

Two accounts can sit side by side under Assets and mean completely different things. One called stripe holds money you could spend this afternoon. One called receivables holds a promise that somebody will pay you later. Both are assets, both go up on a debit, and nothing in the tree tells them apart - so the cash flow statement cannot work out which of your assets is actually cash. You tell it.

Every asset account you create has a Holds cash switch, and it is off until you turn it on. The test is the one you already used to decide the account existed: if a bank or a processor issues a statement for it, it holds cash.

text
assets
├── mercury                ✅ Holds cash   bank account, a statement exists
├── stripe                 ✅ Holds cash   processor balance, a statement exists
├── payouts-in-transit     ❌              left Stripe, not landed, no statement
└── receivables:hosts/42   ❌              a promise to pay, not a balance

An in-transit account is the interesting one, and leaving it off is the honest answer. No statement shows that balance, so a payout registers as cash leaving on the day it leaves Stripe, and the in-transit account is what the statement says it moved against. Turn the flag on there and the same payout goes quiet until the money lands, which is a fortnight of cash you do not have reported as cash you do.

Three things follow from where the switch appears, and they are worth knowing up front:

  • Assets only, and only accounts you create yourself. The switch is not offered on a liability or an income account, because cash is money you have. It is not offered on an account template either: an account created per host is that host's payable, and the money behind it is sitting in your Stripe balance until you pay it. That balance is the account holding the cash.
  • Moving cash between two flagged accounts does not appear on the statement. A Stripe-to-bank sweep leaves one cash account and enters another, so your total cash never changed and there is nothing to explain. The statement reports what cash was for, not where it currently sits.
  • Nothing else in Ledfra reads the flag. Balances, the balance sheet, the trial balance and the overdraft check all behave identically whether it is on or off. So a wrong flag is invisible everywhere except on one report, which is why it is worth setting when you create the account rather than when you first read that report.
An empty cash flow statement is almost always this
A busy ledger reporting no cash movement has no accounts marked as cash. The statement sums only the accounts carrying the flag, so with none set there are no two ends to reconcile.

Classify debt and long-term assets for cash flow

A cash flow statement splits every movement into three activities, and the words are standard rather than ours to redefine. Operating is money moving because the business does what it does: collecting from customers, paying suppliers, settling what you owe onward. Investing is money spent on or returned from things you own for the long term. Financing is money from or to whoever funds you.

Ledfra assigns this from your tree and is right almost everywhere. Two cases are beyond it, and both are facts about your business rather than about your structure:

  • Debt. A term loan sits under Liabilities next to your host payables and looks identical to one. Drawing it down and repaying it is financing, and repaying principal is not a cost of doing business at all.
  • Long-term assets. Equipment sits under Assets next to your receivables. Buying it is investing, and it never reaches your profit & loss.

Everything else falls out of the five roots, and you can leave it alone. This is what a chart is classified as when you say nothing at all:

CategoryActivity when nothing is set
AssetsOperating
LiabilitiesOperating
EquityFinancing
IncomeOperating
ExpensesOperating

Cash flow activity is set on a category, never on an account, and everything below it inherits: accounts, templates, subcategories, and the ones you add next year. A category further down can override its parent. So the decision is made once - file your borrowings under a Debt category, classify that category as Financing, and every loan you add under it afterwards is classified correctly on the day it is created.

text
Liabilities                     Cash flow activity
├── Payables                    Operating   (inherited)
│   └── Host payables           Operating   (inherited)
└── Debt                        Financing   ← set here, once
    ├── Term loan               Financing   (inherited)
    └── Revolving facility      Financing   (inherited)

The Chart of accounts screen shows the resolved activity on every category, including the five roots, so you can read what the statement will do rather than work it out. There is no unclassified state to fall into: every account resolves through its ancestors and then through its root's default, so the statement always has a section for it.

Reclassifying restates periods you have already reported
Changing a category's activity re-sorts history as well as what you post next. No amount changes and nothing becomes unbalanced - rows move from one section to another, so a statement you exported last quarter and the same statement exported today can legitimately disagree. Classify a branch when you create it, and treat a later change as a correction worth mentioning to whoever read the earlier figures.

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.

Your structure is what your reports look like

Every rule above is really a reporting decision made early. The balance sheet and profit & loss render your categories and accounts directly - they do not reorganize anything on your behalf. So a chart of accounts that files held funds under income produces a P&L that is arithmetically perfect and commercially wrong, and no report will flag it.

This is the part worth sitting with before you post anything real. Double-entry catches arithmetic mistakes. Nothing catches structural ones. The statements simply show you what you told them, which is why the design happens first and the integration second.

Where you build it: the Chart of accounts screen

Everything above is a decision. Chart of accounts, in the ledger sidebar, is where you make it and where you come back to check it. It renders the whole chart as a tree in two columns, split by normal balance: accounts that go up on a debit on the left, accounts that go up on a credit on the right.

text
Normal debit accounts          Normal credit accounts

Assets                         Liabilities
├── Mercury                    └── Payables
├── Stripe                         ├── Host payables    (template)
└── Payouts in transit             └── Refunds due
                               Equity
Expenses                       └── Paid-in capital
├── Processing fees
└── Chargebacks                Income
                               └── Service fees

That split is the reason this is one screen instead of a list. Filing an account under the wrong root is the mistake that balances to the cent and still reports the wrong thing, and here it surfaces as a row sitting in the column you did not expect - a payable on the debit side is visible long before it is expensive. Five roots fit in a single view, which is what makes reading the whole chart before you change it something you actually do rather than something you intend to.

Adding accounts, templates and categories

The + on any category row opens three choices, already scoped to that row:

  • Account - one account you create and name yourself: your bank, a processor balance, a fee account. Elsewhere in these docs this is a control account.
  • Account template - one template, many accounts. Ledfra creates the individual account the first time you post to its slug, so liabilities:hosts/42 exists because host 42 was paid, not because someone remembered to provision it.
  • Category - a group that holds accounts and templates. Nothing posts to a category.

You always add under a category, never at the top level. The five roots are fixed, and the category you pick is what sets the new account's normal balance - which is why the menu names the parent before it offers you the choice. This is the one decision on the screen that nothing downstream can check for you.

Changing a chart that already has history

Move lists only categories under the same top-level group. Moving across groups would change the account's normal balance and quietly rewrite what every entry already posted to it means, so it is not offered at all rather than offered and rejected. Re-filing an account across roots is not a move: it is a new account plus reversing entries.

Archive, do not delete. An account that has ever been posted to cannot be deleted, because its entries are your audit trail. Archiving hides it from reports and leaves the history intact, and you can restore it later. Delete stays available only for things nothing depends on yet: an account with no postings, a template no accounts were created from, an empty category.

Your chart of accounts lives on the production ledger. Sandboxes mirror it automatically and show it read-only, so you try a posting against a sandbox. A structural change is still made once, here, and flows out to them.

Design checklist

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

  • Every place money physically sits has its own asset account.
  • 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.
  • Every bank and processor balance has Holds cash turned on. Nothing else does.
  • Debt and long-term assets sit under a category you classified yourself.
  • Descriptions say what kind of transaction it is. Your ids live in metadata.

Where to go next

  1. How to post first transaction - turn this structure into its first balanced entries.
  2. How to build a marketplace ledger - every rule on this page, applied end to end.
  3. Sandboxes - try a design against throwaway data before committing to it.
  4. What is Ledfra? - the model these rules sit on.
  5. Guide: Create the ledger structure - the five categories from scratch, for a reader meeting them for the first time.
  6. Guide: Why marketplace accounting becomes so hard - why a schema without these rules reports revenue it never earned.

For the mechanics behind the rules, read Accounts, Transactions, and Money, then post through the API reference.