Browse documentation
Docs/Functionality

Ledger

The Ledger surface keeps double-entry books and draws them as T-accounts, the way an accountant sketches them on T paper. Each account is a T: its title on the crossbar, debits down the…

Ledger

The Ledger surface keeps double-entry books and draws them as T-accounts, the way an accountant sketches them on T paper. Each account is a T: its title on the crossbar, debits down the left, credits down the right, totals underneath, and the balance on whichever side is larger. The books themselves live in two native model tables, so formulas, the Table surface and the table API read the same records.

See the ledger example.

Set up the books

Declare the two tables in Code and point the Ledger at them:

default table Accounts = { schema: "Accounts" }
default table Journal = { schema: "Journal" }

Books!type = "ledger"
Books!config = <toml>
  version = 1
  kind = "ledger"
  title = "General ledger"

  [ledger]
  currency = "USD"
  scale = 2

  [accounts]
  bind = "Accounts"

  [journal]
  bind = "Journal"
</toml>

Open the surface in the workbench. When the tables are new, the Ledger lists the columns it will create and waits for Set up ledger tables. Setup uses the table store's own migrator, so nothing is written until you confirm.

You can also declare the columns and rules in the source itself, as the ledger example does. A deploy then provisions the tables, and the Ledger opens straight onto the books. Declaring columns and write rules describes the keys.

The tables

Accounts is the chart of accounts.

Column Type Purpose
code text, unique, fixed Account number.
name text Title written above the T.
type choice Asset, Liability, Equity, Revenue or Expense.
normal_balance choice Optional Debit or Credit override for contra accounts.
archived_at date and time, soft delete Set when the account is archived.

Journal holds one row per posting line.

Column Type Purpose
entry_id text, fixed Groups the lines of one journal entry.
line whole number, fixed Line order within the entry.
line_key text, unique, fixed Entry id and line number, so an entry cannot be recorded twice.
date date, fixed Accounting date.
memo, reference text, fixed Entry description and source document number.
account text, fixed Code of a current account.
debit_minor, credit_minor whole number, fixed Amount in minor currency units.
reverses text, fixed Entry id that this entry reverses.
voided_at date and time, soft delete Set when the entry is voided.
void_reason text Why the entry was voided.
recorded_at date and time, fixed When the entry was recorded.

A fixed column cannot change once its row exists.

Amounts are whole minor units: with scale = 2, 125.40 is stored as 12540. Whole numbers add up exactly, so debits and credits compare without rounding. scale may be 0 to 6. By default, currency labels amounts. The optional multi-currency setting records original amounts and explicit conversion rates.

Every line repeats its entry's id, date and memo. That lets the Ledger write a whole entry as a single insert, which the table store validates and commits in one transaction. An entry is recorded completely or not at all.

What the table store enforces

The rules are part of the table schema, so the table store applies them to every writer: the Ledger, the Table surface, model rules, imports and the table API. A refused write changes nothing.

  • Posted lines are fixed. Amounts, accounts, dates and memos cannot change once recorded. Correct an entry by voiding or reversing it.
  • Entries balance. Within each entry, the lines that are not voided must have equal debit and credit totals. An unbalanced insert is refused, and so is voiding only some lines of an entry.
  • Accounts must be current. A line must name an existing account code, and a new line cannot use an archived account. The link is kept on both tables: an account code that open lines still name cannot be changed or hard-deleted.
  • Deletes become soft deletes. Deleting journal rows voids them, and deleting accounts archives them. Rows are never removed, and a table with these rules cannot be replaced wholesale.
  • No duplicate entries. line_key is unique, so posting the same entry twice is refused.

Reads through the table API, including the Table surface, skip voided lines and archived accounts unless the request sets includeDeleted. The Ledger reads archived accounts so it can offer to restore them, and reads voided lines when Show voided postings is on.

Post an entry

Choose New entry, then set the date, memo and optional reference. Each line takes an account and either a debit or a credit. The running totals show how far the entry is out of balance, and Post entry stays unavailable until debits equal credits across at least two lines on open accounts.

If the journal does not confirm a post, for example after a lost connection, the draft stays locked. Check and post first looks for the entry's id in the journal. It finishes if the entry landed and posts again only if nothing was recorded. The same entry id is reused, so even a retry racing the first attempt is refused by line_key rather than recorded twice. When the table store refuses the entry, the draft unlocks so you can correct it.

Read the T paper

Each sheet shows up to 12 T-accounts, sorted by code. Filter by name, code or type, and page through sheets. Each T lists its newest postings, 10 by default, drawn oldest first. Totals and balances come from the table store's sums over all of an account's open postings, not just the ones on screen.

A balance on the side opposite the account's normal side is labelled. The footer shows the trial balance: total open debits and credits across the whole journal. An entry written outside the Ledger without matching sides makes the footer report how far the books are out of balance.

Select a posting to open its entry. Every line of that entry is highlighted on every T, which traces where the entry went.

Account history

Show all postings under a T opens that account's complete history, newest first, 100 lines at a time; Load older postings continues. Each line shows the account's balance after it, marked Dr or Cr. The balance starts from the table store's own total and steps back one line at a time, so it is exact without reading the whole account. With Show voided postings on, voided lines appear struck through and do not change the balance. If the journal changes while you page, the history asks you to reload rather than mix two versions.

Void, restore and reverse

Nothing is deleted. Voiding is a soft delete: it records voided_at and a required reason on every line of the entry, and voided lines leave every balance. Show voided postings draws them struck through. Restore entry clears the void. It only changes an entry voided at exactly the time you read, so a concurrent change is reported instead of overwritten.

For a period you have already reported, post a reversing entry instead. It mirrors the entry's lines, debits for credits, on the date you choose, and records the original in reverses.

Accounts

Accounts adds accounts and archives them. The table store rejects a duplicate code. Archiving is a soft delete too: an archived account keeps its postings and balance, drops off the sheet unless Include archived accounts is on, and accepts no new postings until restored.

Schema versions and upgrades

The ledger schema is versioned. Each version only adds columns, choices, unique indexes or write rules; none drops, renames or changes how a column is stored, and none loosens a rule. When a later version adds something, the Ledger lists the additions and waits for Upgrade ledger tables. Your own extra columns, units, indexes, choices and rules are kept.

A ledger at an earlier complete version keeps working. The books open as usual, postings match that version's columns, and the upgrade is offered in a banner above the sheet.

Version 2 adds line_key and the write rules above. The table store checks the existing rows before it records a rule: if an earlier entry does not balance or a line names a missing account, the upgrade stops, names the problem and changes nothing. Void or reverse the offending entry, then upgrade again. Lines recorded before version 2 keep an empty line_key.

Removing or loosening a rule is a destructive migration. A schema edit that leaves a rule out is reported, and the rule stays in force.

If a ledger column exists with a different storage type, the Ledger stops and names the column. For example, debit_minor stored as a currency number cannot become a whole number without rewriting data. Changing that is a destructive migration, which the Ledger never performs. Resolve it in the Table surface or Code first. Schema changes are offered only in the workbench, not in app run mode or print.

Formulas over the books

The tables are ordinary native tables, so model formulas can report on them. Formulas read every stored row, including voided ones, so filter on voided_at as this example does:

AccountBalances = SELECT account, SUM(debit_minor) AS debits, SUM(credit_minor) AS credits
  FROM Journal
  WHERE voided_at IS NULL
  GROUP BY account

Ledger 2.0 options

Each option is independent. Leave it out to keep ordinary single-currency books at schema version 2. Adding an option offers a reviewed additive schema upgrade before that capability can be used; it never silently posts using an older, weaker schema. Existing rows are preserved and remain base-currency entries.

default table ClosedPeriods = { schema: "ClosedPeriods" }

Books!config = <toml>
  version = 1
  kind = "ledger"
  title = "General ledger"

  [ledger]
  currency = "USD"
  scale = 2
  currencies = { EUR = 2, JPY = 0 }

  [accounts]
  bind = "Accounts"

  [journal]
  bind = "Journal"

  [periods]
  bind = "ClosedPeriods"

  [posting]
  identity = true
</toml>

See the multi-currency example. The base currency and every currency's scale are accounting conventions for the existing book: do not change them after posting. Create another book or perform an explicit reviewed data migration instead. Up to 20 foreign currencies may be listed, each with 0 to 6 minor-unit digits.

Original currency and exchange rates

With ledger.currencies, New entry offers a transaction currency and an explicit rate in base-currency units per foreign unit. A USD ledger recording 100 EUR at 1.10 posts 110 USD while keeping the original 100 EUR. Every entry uses one transaction currency; the base-currency journal remains double entry. T-accounts, trial balances and account history show base-currency amounts. Entry details also show the original currency amounts and recorded rates.

Rates are positive decimal text with at most 12 fractional digits. Conversion uses exact decimal ratios and rounds each line to base minor units, with halves away from zero. If converted sides round differently, choose an open rounding account; the preview includes the balancing line. That line has no foreign currency exposure. No live rate feed is used and no rate is inferred.

The selected journal schema adds fixed currency, currency_scale, exchange_rate, foreign_debit_minor and foreign_credit_minor columns. The ordinary debit_minor and credit_minor columns always hold base amounts. The original amounts and rates survive reversals; a reversal exchanges both base and original debit/credit sides. Older base-only rows have empty foreign metadata and are not revalued.

Revaluation

Choose Revalue currencies, select the monetary Asset or Liability accounts, supply closing rates, and set the revaluation date. Account selection is explicit: an Asset account might contain inventory or equipment that should not be revalued with monetary balances. Choose an open Revenue or Expense exchange gain/loss account and then Review revaluation.

The preview reads open journal balances through that date, grouped by account and currency. It compares the converted net foreign balance with its recorded base carrying balance, including prior revaluation adjustments, and proposes a balanced adjustment to the selected gain/loss account. Original exposure is unchanged. Posting again at the same rate produces no adjustment unless the underlying balances changed. Each entry may adjust up to 50 account/currency balances; choose a smaller selection for larger books.

Post revaluation commits the exact preview in one native transaction. The journal and chart revisions must still match the review. A changed book is refused and needs a fresh review. An uncertain response keeps the preview locked; Check and post revaluation reconciles the retained entry id before retrying. A revaluation may be reversed in an open period just like another entry. entry_kind identifies postings, revaluations and closing adjustments.

Closing periods

With periods.bind, a separate native table records through_date, memo, closed_at and closed_by. Its cutoff date is unique, fixed and append only; closures cannot be changed or deleted through table operations. The journal's fixed accounting date gains a native closedThrough write rule pointing at that table. All writers, including imports and model rules, refuse inserting, voiding, restoring, editing or deleting journal lines on or before the latest cutoff, even when a write is balanced. Failed batches change nothing. The next day remains open. Removing the option from the surface does not remove the rule.

Post revaluations and any year-end or other closing adjustments first. The entry composer offers Closing adjustment to identify such a posting; it uses the same balancing and duplicate-protection rules as an ordinary entry. Close period records the explanation and optional author, displays the current cutoff, then requires Review period closure and Confirm period closure. The journal and closure-table revisions are checked together inside the transaction, so intervening writes invalidate the review.

Closing freezes history; it does not reset account balances or automatically transfer income to retained earnings. Corrections use a new reversing entry in an open period. There is no reopen command; closing is a deliberate permanent operation. Times come from the author's device clock.

When a posting or closure acknowledgement fails, the Ledger retains the exact intent and checks storage before retrying. Even a client-error response may follow a completed write; the draft unlocks for correction only after a successful read proves that the intent was not recorded.

Posting identity

With posting.identity = true, new entries and reversals require a posting author label of 1 to 100 characters. Revaluations and period closures also ask for their author's label. The journal keeps it in the fixed posted_by column; entry details label it as an author label. Old entries remain unattributed. This is self-declared attribution, not authenticated identity or a signature. Other writers may supply the column themselves; the surface's required author field does not impose authentication on the table API. Void/restore actions do not replace the original posting author.

Reporting and review

From date and Through date filter both postings and native totals. Leaving From blank gives balances as of Through; setting both gives activity and balances for that range, explicitly labelled. Account history uses the same range. All dates clears the range. Voided entries never affect totals.

Trial balance opens a complete account report independent of the current T-account sheet, account search and type filter. It includes archived accounts with activity. Export trial balance CSV exports that report with currency, date range, debit/credit totals and debit/credit balance columns. Pages must share one journal revision; a changed journal asks for a reload. The export is a captured report, not a live linked spreadsheet. Author-controlled text is quoted and protected from spreadsheet formula interpretation.

Posting, revaluation and closure drafts protect navigation. Discard a draft to switch panels; after an uncertain post, reconcile it before discarding. Review forms lock their inputs so the confirmed operation matches the preview.

Current limits

  • Recorded and void times come from the author's device clock. Author labels are optional and self-declared.
  • The chart of accounts loads up to 5,000 accounts.
  • Imports create plain tables. Add write rules afterwards with a schema upgrade, which checks the imported rows.
  • No automatic retained-earnings close, tax treatment, rate sourcing, consolidation, authenticated attribution or period reopening.
  • One transaction currency per entry, with up to 100 posting lines. Revaluation and trial-balance reads support up to 5,000 account/currency groups.
  • Source/API writers must supply coherent original-currency metadata. The existing native balancing rule enforces base totals, not conversion policy.
  • The Ledger is a workbench surface. It cannot yet be placed inside a Grid View.