Books as queryable data, on datahike.
kontor is a double-entry accounting kernel for Clojure apps. Postings,
balances, periods, sealing, audit, tax — all exposed as data your application
queries with Datalog. Build an invoicing tool, a payroll product, a SaaS-billing
engine, or a country-specific filing pipeline on top — in the same datahike
connection where your business state already lives.
(require '[datahike.api :as d]
'[kontor.book :as book]
'[kontor.l10n-de.preset :as de]
'[kontor.l10n-de.cit-provider :as de-cit]
'[kontor.l10n-de.pnl :as de-pnl]
'[kontor.tax.period-tax-provider :as ptp])
;; In-memory datahike + DE schema + SKR04 chart + KSt/Soli/GewSt
;; statute parameters, in one call.
(def conn (de/create-de-db))
(d/transact conn
[{:kontor.entity/name "Acme UG"
:kontor.entity/code "ACME-UG"
:kontor.entity/country "DE"
:kontor.entity/functional-commodity [:kontor.commodity/symbol "EUR"]}])
;; Post a balanced, sealed, gate-checked entry. Positive amounts
;; are debits, negatives are credits; the legs must sum to zero per
;; (entity × ledger × commodity) or the gate refuses the write.
(book/entry! conn
{:entity [:kontor.entity/code "ACME-UG"]
:commodity :EUR
:journal [:kontor.journal/code "CR"]
:effective-date #inst "2026-06-30"
:narration "Beratung H1 (€40k + 19 % USt)"
:postings [{:account [:kontor.account/path "Umlaufvermögen:Bank"] :amount 47600M}
{:account [:kontor.account/path "Erträge:Erlöse:19%"] :amount -40000M}
{:account [:kontor.account/path "Verbindlichkeiten:Umsatzsteuer:19%"] :amount -7600M}]})
;; Run the DE corporate-income-tax provider over the year.
;; KSt + Soli + GewSt are ADR-101 :provision data; the result
;; matches BMF worked examples to the cent.
(def facts
(ptp/period-tax-facts (de-cit/de-cit-provider {})
{:db (d/db conn)
:entity [:kontor.entity/code "ACME-UG"]
:period {:from #inst "2026-01-01" :to #inst "2027-01-01"}
:tax-unit {:hebesatz 490}
:inputs {:book-profit 25000M}}))
(sort (map #(-> % :liability :amount) (:components facts)))
;; => (3956.25000M 4287.5000M) ; KSt+Soli + GewSt for €25k @ Hebesatz 490Full walkthrough with explanations: doc/quickstart.md.
The same code is regression-tested in
test/kontor/integration/cross_border_scenario_test.clj,
which also covers the cross-border dividend scenario (DE UG → CA personal).
The same book/entry! + *-cit-provider pattern works across all
11 jurisdictions. Only the preset, the chart, and the statute data
change.
(require '[kontor.l10n-us.preset :as us]
'[kontor.l10n-us.cit-provider :as us-cit])
;; In-memory datahike + USD commodity + a US chart of accounts
;; + IRC §11 / §1(h) / §1(j) statute parameters, in one call.
(def us-conn (us/create-us-db))
(d/transact us-conn
[{:kontor.entity/name "Acme Inc."
:kontor.entity/code "ACME-INC"
:kontor.entity/country "US"
:kontor.entity/functional-commodity [:kontor.commodity/symbol "USD"]}])
;; Book a Q2 software-license invoice — no federal sales tax (US
;; sales tax is state-level and out of substrate per ADR-005; consumers
;; plug Avalara / TaxJar via the `TaxRateProvider` protocol).
(book/entry! us-conn
{:entity [:kontor.entity/code "ACME-INC"]
:commodity :USD
:journal [:kontor.journal/code "CR"]
:effective-date #inst "2025-06-30"
:narration "Software-license invoice — Q2 2025"
:postings [{:account [:kontor.account/path "Assets:Bank:Checking"] :amount 500000M}
{:account [:kontor.account/path "Income:Services"] :amount -500000M}]})
;; IRC §11 federal corporate income tax — flat 21 % on book profit.
(ptp/period-tax-facts (us-cit/us-cit-provider {})
{:db (d/db us-conn)
:entity :c-corp
:period {:from #inst "2025-01-01" :to #inst "2026-01-01"}
:inputs {:book-profit 1000000M}})
;; => 1 :cit component, $210,000 (21 % × $1M, matches IRC §11(b))State income tax and state sales tax are deliberately out of the substrate — consumers integrate Avalara, TaxJar, TaxCloud, or their own engines via the protocol seams. Same logic in DE for trade-tax Hebesatz: substrate provides the schedule algebra; consumers supply the per-municipality multiplier.
We use kontor ourselves — for our own business work, and as the
accounting layer for simm.is, where the books are
part of the business simulation rather than a stub.
If you're considering kontor for your project, please try it. The
fastest way to get on the same page is text chat — ping us on the
Clojurians Slack #simmis channel
with what you're trying to do, what worked, and what didn't. A
GitHub issue works too. We'll try to address requirements and
features you need to our abilities. Real use is what hardens the
substrate.
Accounting is a substrate, not an application. Every business app that moves money needs the same primitives — balanced postings, periods you can close, an audit trail that holds up, tax computation that matches the authority's filing. The hard part isn't the bookkeeping; it's keeping all of that queryable alongside the business state, with no bridge to fall through.
kontor puts the substrate in your deps.edn. Your invoice is a :transaction
- a few
:postings in the same datahike connection as your:userand:product. Your "send invoice" function transacts both halves atomically. Your year-end close is a Datalog query over the same indexes that serve your dashboards.
Four properties shape the design:
- 🕰️ Bitemporal by default. Every read takes
:as-of-valid+:as-of-tx. A Y1 misclassification caught 18 months later writes a new fact at the original valid-time; both views — what the books showed at year-end and what they show after the restatement — stay queryable forever. - 🚪 Validation gate, not a contract you can forget. Every business
write routes through middleware enforcing sum-to-zero per
(entity × ledger × commodity), sealing of posted entries, legal-hold, period-locks, and status-machine legality. Invalid tx-data raises a typed exception. The application never has to remember to call validators. - 📜 Tax law is data.
:tax-concept,:provision,:regime, and:parameterare entities you query. One evaluator (apply-provisions) folds the rules of 11 jurisdictions; per-country files match authority-published worked examples to the cent. - 🌐 Runs in the browser, not just the JVM. The substrate is
.cljcand executes on ClojureScript against datahike-cljs — the same code, not a reimplementation. A frontend builds an entry, commits it through the validation gate, and reads back a trial balance or financial statement locally, offline or ahead of a server round-trip, producing numbers identical to the backend. A CI lane runs it on Node every build.
Trans-national substrate — schema-namespaced under :kontor.* so kontor
cohabits with your app's namespaces in one connection.
| Concern | Surface |
|---|---|
| Posting + balance | kontor.posting, kontor.reporting.{balance,ledger,trial,closing} |
| Bitemporal | kontor.bitemporal (with-vt, close-validity!, commit-tx-eid) |
| Sealing + audit | kontor.compliance.{sealing,audit-doc} |
| Compliance | kontor.compliance.{legal-hold,retention,dsar} |
| Status machines | kontor.workflow.status-machine + :approval-policy |
| Multi-entity | kontor.entity/family, kontor.provider.consolidation |
| Reports | kontor.reporting.report/marginalize (σ_E primitive — any axis) |
| Verb facade | kontor.book/{entry,receive,pay,sell,buy,receive-payment,pay-bill,transfer,adjust} |
Tax substrate
| Protocol | Purpose |
|---|---|
TaxRateProvider |
Transactional taxes (VAT / GST / sales tax) |
PeriodTaxProvider |
Period taxes (CIT / PIT / CGT / property / wealth) |
DisposalProvider |
Capital gains substrate |
FxRateProvider |
IAS 21 / ASC 830 rate lookup |
:provision / :parameter / :regime |
Statute-as-data evaluator |
:fiscal-unit |
Group-tax consolidation (DE Organschaft, FR intégration, US §1502, …) |
11 jurisdictions on the same path — DE, FR, CA + QC, JP, BR, IN, US,
AT, AU, CN, MX. Each ships CIT (and PIT / CGT / investment-income where
applicable) as ADR-101 :provision data. 12 CGT providers (same set +
UK). 11 payroll adapters (DE LODAS, US ADP GLI, CA + QC, FR DSN, AU STP
Phase 2, BR eSocial, MX CFDI Nómina, IN TDS+PF+ESI+PT, JP Gensen, CN IIT +
五险一金, AT mBGM + L16). Per-country e-invoice for DE (Factur-X /
XRechnung).
Companion modules — drop into :paths à la carte.
| Module | What |
|---|---|
asset |
Asset register + depreciation books |
lease |
IFRS 16 / ASC 842 lessee accounting |
inventory |
FIFO / LIFO / WAC valuation |
expense |
Per-diem + reimbursement |
procurement |
Drop-ship + substitute + replacement + upgrade |
sales / invoice / collections |
Order → invoice → AR aging |
partner |
Party + person/org + bank-account |
hr |
Personnel substrate (composes with payroll-* adapters) |
disposal |
CGT substrate |
einvoice-de |
Factur-X / XRechnung via Mustang |
bank-{at,ca,de,fr,us} |
Bank-statement CSV importers |
~3,000 tests / 11,500+ assertions / 0 failures. Per-jurisdiction CIT and CGT providers carry worked-example tests citing the authority bulletin or XSD they were sized against.
;; deps.edn
{:deps {org.replikativ/kontor {:mvn/version "LATEST"}}}The repo is a monorepo: the kernel ships from src/, and each companion
under modules/<name>/ adds its own src/ + test/. Consumers who want
only the kernel + a subset of companions trim deps.edn :paths
accordingly.
kontor and your app share one datahike connection by namespacing their
attributes: kernel attrs prefix under :kontor.*
(:kontor.posting/account, :kontor.transaction/journal, …); your app owns
its own namespaces (:invoice/status, :lead/source, …). Posting a sales
invoice writes the business state and the matching :kontor.transaction +
:kontor.postings in one tx — atomic, sum-to-zero validated,
bitemporally indexed.
This is library posture, not framework. The canonical consumer is
beleg (contractor invoicing); a
Clojure SaaS that wants accounting underneath puts kontor in its deps.edn
and writes through kontor.book/entry!.
Six end-to-end Clay notebooks in doc/showcases/:
| # | Scenario | Demonstrates |
|---|---|---|
| 01 | DE GmbH B2B with Factur-X | l10n-de + einvoice-de + collections + sales/invoice posting bridge |
| 02 | US LLC multi-state SaaS | TaxRateProvider + Avalara seam + customer-dispute lifecycle |
| 03 | IN B2B with IRN + TDS | NIC e-invoice clearance + GSTR-1 export shape + reverse-charge intercompany |
| 04 | Multi-entity intercompany | FX translation + elimination + ADR-073 consolidation |
| 05 | Apple 10-K bitemporal restatement | :tx/valid-from + close-validity! against real SEC filings |
| 06 | Multi-year DE GmbH | Misclassified Y1 expense caught in Y2, restated bitemporally |
doc/quickstart.md— 5-minute REPL walkthroughdoc/accounting-model.md— how the verbs translate to debits/credits and the algebra underneathdoc/architecture.md— layer cake + namespace map + module listdoc/decisions.md— distilled architecture decisions (~30 entries)doc/programming.md— the three-axis programming model (transact gate + status machines + bitemporal)
kontor is part of the replikativ ecosystem:
- datahike — durable Datalog database with bitemporal semantics; kontor's substrate.
- beleg — invoicing and CRM for contractors and small businesses; the canonical kontor consumer app.
- konserve — the pluggable key-value store datahike rides on (file, LMDB, S3, JDBC, Redis, IndexedDB, …). Use it directly for any persistent storage need.
- hasch — content-addressable hashing for Clojure data structures.
Issues, PRs, and design discussions all welcome — see
CONTRIBUTING.md for the workflow (test-first; the
*-tx-data + ! discipline; the :kontor.* schema-namespace rule).
Copyright © 2026 Christian Weilbach et al.
Licensed under Apache License 2.0 — see LICENSE.