Hex.pm Version Hex.pm Downloads HexDocs Hex.pm License CI Coverage Status GitHub Release

An embedded OTP accounting engine for Elixir applications.

Logistiki is the accounting execution layer that applications embed. It is not a bank, an ERP, a payment system, or a compliance platform. It is the accounting engine that those applications embed — the way Phoenix is for web, Ecto is for persistence, Oban is for jobs, and Ash is for domains.

The name comes from the Greek word λογιστική — accounting, bookkeeping, but deeper: reasoning, reckoning, calculation, explanation. Logistiki is not merely about storing debits and credits; it is about explaining why accounting events happened.

What Logistiki is

  • Business event processing
  • Datalog-backed accounting knowledge
  • Double-entry accounting
  • Immutable journals and postings
  • Hierarchical business entities
  • Hierarchical virtual accounts
  • Entity-to-account relationship graphs
  • Derived balances, statements, trial balances, general ledger, balance sheet, income statement
  • Audit evidence
  • Deterministic replay
  • Behaviour-based ledger backends
  • A Beancount-backed accounting oracle

What Logistiki is not

  • a bank, ERP, payment system, or compliance platform
  • a payment rail (SWIFT, ISO 20022, ACH, card processing)
  • a KYC/AML/sanctions/PEP screening tool
  • a regulatory reporting or tax-filing engine
  • a Phoenix dashboard (that is future work)

The central architectural principle

Applications never create journals directly. The primary public API is:

Logistiki.process(event)

Applications publish business events. Logistiki decides whether and how those events become accounting entries. Journals and postings are internal accounting artifacts — useful for audit, ops, and admin tooling, but not the normal application-facing API.

%Logistiki.Event.DepositReceived{}
%Logistiki.Event.TransferSettled{}
%Logistiki.Event.FeeAssessed{}
%Logistiki.Event.InterestAccrued{}
%Logistiki.Event.RefundIssued{}

Logistiki turns those events into journals and postings, and the application asks questions afterward:

Logistiki.balance(account)
Logistiki.statement(account)
Logistiki.trial_balance()
Logistiki.general_ledger()
Logistiki.balance_sheet()
Logistiki.income_statement()

Why business events are the primary API

Money never moves because random application code says so. Money moves because a business event happened, the knowledge layer evaluated the rules, an accounting policy was selected, a posting template was resolved, a journal was built, invariants were validated, and a ledger backend executed it. Keeping that chain explicit makes accounting explainable, reproducible, testable, and auditable.

Why Datalog is the knowledge layer

The knowledge layer is powered by ex_datalog. Datalog is not merely a rule selector — it is the accounting knowledge layer. It holds business rules, accounting policies, posting templates, account mappings, constraints, and dimensions as facts and rules, and answers questions like should this event be processed?, which policy applies?, which postings should be generated?, which account does this role resolve to?.

Datalog decides facts and relationships. Elixir materializes journals, postings, and persisted records. Datalog never constructs Elixir structs.

Why journals are internal artifacts

Journals and postings are essential to accounting, but they are not the main application-facing API. They are generated by the runtime from business events, validated by deterministic Elixir invariants, and executed by a ledger backend.

Why balances are projections

Balances are never authoritative stored state. They are projections over immutable postings, computed by Logistiki.Projections.ProjectionEngine. Parent account balances aggregate descendants; entity balances aggregate linked accounts; entity subtree balances aggregate across descendant entities.

How Beancount is used

The Beancount backend (beancount_ex) is the executable accounting specification for Logistiki. On execute_journal/2 the journal is mapped to Beancount directives and verified with Beancount.check/1 before being persisted. The regression suite compares Simulation-backend results against the Beancount oracle. Beancount-specific types never leak into the public API — the Logistiki.Ledger.BeancountMapper converts at the boundary.

How a future native backend fits

A future native Elixir ledger backend (and a Rust backend) must match Beancount behaviour for shared supported features. The Logistiki.Ledger.Behaviour contract and the regression tests guarantee that.

A runnable example

# Configure PostgreSQL (config/dev.exs is provided), then:
mix ecto.setup

# Run the demo scenario end-to-end:
mix run -e 'Logistiki.Demo.run_demo()'

# Or open the interactive Livebook walkthrough (SQLite in-memory, no PostgreSQL needed):
livebook open docs/livebooks/logistiki_demo.livemd
alias Logistiki.Event.DepositReceived

event = %DepositReceived{
  id: "evt_001",
  entity_type: "corporate",
  account_code: "LIABILITIES:CLIENT_DEPOSITS:USD:ACME:OPERATING",
  cash_account_code: "ASSETS:CASH:USD:NOSTRO",
  amount: Decimal.new("1000.00"),
  currency: "USD",
  occurred_at: ~U[2026-07-07 12:00:00Z],
  effective_date: ~D[2026-07-07],
  source_system: "bank_core",
  source_id: "wire_123"
}

{:ok, result} = Logistiki.process(event)
# => the knowledge layer selected :cash_deposit,
#    a journal was built, validated, posted, and audited.

{:ok, [balance]} = Logistiki.balance("ASSETS:CASH:USD:NOSTRO")
# => %Logistiki.Projections.Balance{net: Decimal.new("1000.00"), ...}

Installation

Add logistiki to your list of dependencies in mix.exs:

def deps do
  [
    {:logistiki, "~> 0.1.0"}
  ]
end

Requires Elixir 1.18+ and PostgreSQL.

Optional: SQLite for tests and Livebooks

Add ecto_sqlite3 to use SQLite in-memory instead of PostgreSQL:

def deps do
  [
    {:logistiki, "~> 0.1.0"},
    {:ecto_sqlite3, "~> 0.18"}
  ]
end

Set LOGISTIKI_DB_ADAPTER=sqlite before compilation.

Architecture overview

Business Event
    
    
Event Normalization
    
    
Datalog Fact Generation           ex_datalog
    
    
Business Rule Evaluation         Datalog (blocked / requires_approval)
    
    
Accounting Policy Selection      Datalog (policy/2)
    
    
Accounting Template Resolution   Datalog (template_posting/6, account_role/3)
    
    
Journal Builder                  Elixir
    
    
Posting Builder                  Elixir
    
    
Ledger Invariant Validation      pure Elixir (never Datalog)
    
    
Ledger Backend Execution         Simulation or Beancount
    
    
Projection Generation            balance / statement / trial balance
    
    
Audit Evidence

Documentation

See the docs/ directory for in-depth guides:

Full API documentation is available at HexDocs.

Changelog

See CHANGELOG.md.

Contributing

  1. Fork the repository
  2. Create your feature branch (git checkout -b my-new-feature)
  3. Run mix test to make sure everything passes
  4. Commit your changes (git commit -am 'Add some feature')
  5. Push to the branch (git push origin my-new-feature)
  6. Create a new Pull Request

License

MIT — see LICENSE.